页面片段“.NET研究”缓存(一)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:   一般,页面上会分为很多部分,而不同的部分更新的频率是不一样的。如果对整个页面采用统一的缓存策略则不太合适,  而且很多系统的页面左上角都有一个该死的“Welcome XXX”。这种特定于用户的信息我们是不能缓存的。

  一般,页面上会分为很多部分,而不同的部分更新的频率是不一样的。如果对整个页面采用统一的缓存策略则不太合适,

  而且很多系统的页面左上角都有一个该死的“Welcome XXX”。这种特定于用户的信息我们是不能缓存的。对于这些情况我们就需要使用片段缓存了。对页面不同的部分(片段)施加不同的缓存策略,而要使用片段缓存,首先就得对页面进行切分。土一点的办法可以用iframe,用iframe将页面划分为一块块的,不过我总觉得iframe是个邪恶的东西。好点的办法可以用Ajax单独的请求这个片段的内容然后再填充,看起来挺美好的。不过使用Ajax也有一些限制:

  1、如果页面上有许多片段,使用太多的这种技术,会有很多请求发送到服务器,HTTP对同一个域名有连接的限制,这样会降低并发连接的效率。

  2、如果说第一个不是什么问题,那么还有一点可能对用户体验不友好。比如有一个片段可能响应慢点,造成页面闪烁。不过如果前面两点都可以克服,这个上海徐汇企业网站设计与制作方案还是可以的。可恶的是我们的客户(此处省略500字),说他们的大多数用户处于一个禁用JavaScript的环境里。好吧,这个方案也不能使用了。如是我们进行了一系列其他关于片段缓存的尝试:

  我们的系统使用的是Spring+Hibernate+Oracle技术,模板引擎使用的是Apache Velocity。假设下面的片段是我们要缓存的内容:

 
 
< ul >
#foreach($book in $books)
< li >< a href ="/book/books/$book.id" > $book.name </ a > --- < a href ="/book/books/edit/$book.id" > Edit </ a > -- < a href ="/book/books/delete/$book.id" > Delete </ a ></ li >
#end
</ ul >

  显示一个图书列表。对这个页面改动最小的办法是加上一个标签,被这个标签包围的片段就是缓存的:

 
   
#cache
< ul >
#foreach($book in $books)
< li >< a href ="/book/books/$book.id" > $book.name </ a > --- < a href ="/book/books/edit/$book.id" > Edit </ a > -- < a href ="/book/books/delete/$book.id" > Delete </ a ></ li >
#end
</ ul >
#end
由于一个页面可能有很多片段,不同的片段肯定要用不同的cache key,所以这个标签应该还能传入一个cache key。当呈现这个页面,到解析这个标签的时候我们就用这个cache key去缓存中取,如果取到了我们就直接将缓存的东西输出,

  而不再需要解析这个图书列表了。

  有了这个想法,我们就需要找到如何让Velocity解析我们的标签的方案。很好,Velocity是支持自定义标签的:

 
 
public class Cache extends Directive {
@Override
public String getName() {
return " cache " ;
}

@Override
public int getType() {
return BLOCK;
}
@Override
public boolean render(InternalContextAdapter context, Writer writer, Node node)
throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {
Node keyNode
= node.jjtGetChild( 0 );

String cacheKey
= (String) keyNode.value(context);
String cacheHtml
= cacheHtml = (String) CacheManager.getInstance(). get (cacheKey);
if (StringUtils.isEmpty(cacheHtml)) {
Node bodyNode
= node.jjtGetChild( 1 );
Writer tempWriter
= new StringWriter();
bodyNode.rende上海网站建设r(context, tempWriter);
cacheHtml
= tempWriter.toString();
CacheManager.getInstance().
set (cacheKey, cacheHtml);
}
writer.write(cacheHtml);
return true ;
}
}

  关于Velocity的自定义标签的使用我会在后面稍作解释。

  最主要的逻辑在render方法里,我们先根据cache key去缓存里取,如果没取到再使用代码render,然后render的结果放到缓存中。很典型的缓存使用场景是不。再来看看控制器端得代码:

 
 
@Controller
@RequestMapping(
" /books " )
public class BookController {
private BookDAO bookDAO;
@Autowired
public BookController(BookDAO bookDAO) {
this .bookDAO = bookDAO;
}

@RequestMapping(value
= { "" , " index.html " }, method = RequestMethod.GET)
public ModelAndView index() {
return new ModelAndView( " list " , " books " , bookDAO.findAll());
}
}

  控制器很简单,调用DAO,将所有图书列出来即可。正在我们高兴这么棘手的问题被解决的时候,问题来了:

  我们的缓存是为了什么?总不是为了节约Velocity解析的时间吧。我想大家应该都知道,最主要的还是为了节约这次bookDAO.findAll()查询数据库的时间。但是回过头看看我们的方案。不管我们的cache命没命中,这个bookDAO.findAll()都会执行一次,因为控制器的执行是在视图render之前发生的。我们唯一节省的是Velocity解析的时间,杯具。

  找到了问题的答案,寻找解决办法就容易了。我们要做的就是在缓存没有命中的时候才执行查询,那么这个数据查询就必须放到cache标签内部做。但是我们的cache标签可不是为了一个片段啊,有很多片段,而各种片段取数据的方上海企业网站设计与制作式却不同。

  嗯,你还记得接口么?还记得计算机里所有的问题都可以通过中间层解决的这个名言么?按照这个思路我们如此设计cache标签:

 
 
#cache("book_list",$dataProvider)
< ul >
#foreach($book in $books)
< li >< a href ="/book/books/$book.id" > $book.name </ a > --- < a href ="/book/books/edit/$book.id" > Edit </ a > -- < a href ="/book/books/delete/$book.id" > Delete </ a ></ 上海徐汇企业网站制作"color: #800000;">li >
#end
</ ul >
#end

  我们传入一个dataProvider对象进来,而这个dataProvider是控制器里传入进来的,一个专门用来取数据的:

 
 
public class BookController {

private DataProvider dataProvider;
@Autowired
public BookController(BookDataProvider dataProvider) {
this .dataProvider = dataProvider;
}

@RequestMapping(value
= { "" , " index.html " }, method = RequestMethod.GET)
public ModelAndView index() {
return new ModelAndView( " list " , " dataProvider " , dataProvider);
}
}

  控制器还是一如既往的简单,我们再来看看cache标签的实现:

 
 
public class Cache extends Directive {
@Override
public String getName() {
return " cache " ;
}

@Override
public int getType() {
return BLOCK;
}

@Override
public boolean render(InternalContextAdapter context, Writer writer, Node node)
throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {
Node keyNode
= node.jjtGetChild( 0 );

String cacheKey
= (String) keyNode.value(context);
String cacheHtml
= cacheHtml = (String) CacheManager.getInstance(). get (cacheKey);
if (StringUtils.isEmpty(cacheHtml)) {
Node dataProviderNode
= node.jjtGetChild( 1 );
DataProvider dataProvider
= (DataProvider) dataProviderNode.value(context);
Map
< String, Object > map = dataProvider.load();
for (String key : map.keySet()) {
context.put(key, map.
get (key));
}

Node bodyNode
= node.jjtGetChild( 3 );
Writer tempWriter
= new StringWriter();
bodyNode.render(context, tempWriter);
cacheHtml
= tempWriter.toString();
CacheManager.getInstance().
set (cacheKey, cacheHtml);
}
writer.write(cacheHtml);
return true ;
}
}

  我们在标签内部取到外部传入的dataProvider,它实现了一个接口DataProvider,然后在标签内部进行数据的查询。

  然后将查询的数据put到velocity的context中,然后再次render,将render的结果放到缓存。这下好了,控制器里只需要向视图传递一个可以取数据的对象就可以了,cache标签内部会进行判断。DataProvider和BookDataProvider的代码:

 
 
public interface DataProvider {
Map
< String,Object > load();
}
@Service
public class BooksDataProvider implements DataProvider{
private BookDAO b上海闵行企业网站设计与制作ookDAO;

@Autowired
public BooksDataProvider(BookDAO bookDAO){
this .bookDAO = bookDAO;
}
public Map < String, Object > load() {
Map
< String,Object > result = new HashMap < String,Object > ();
result.put(
" books " ,bookDAO.findAll());
return result;
}
}

  现在我们要改造的就是对于每个不同的缓存片段写一个DataProvider的实现,而实际上这个实现原来已经有了:

  就是原来控制器内那部分代码。比如BooksDataProvider实际上就是原来BookController内的代码。通过这种方式,我们基本上就将一个页面划分为很多用cache包围的小片段了,只要划分出来了那么你就可以对不同

  的片段采用不同的缓存策略。代码的结构还算清晰。

  下面我稍微介绍一下Velocity自定义标签的使用

  Velocity自定义标签

  每个自定义标签都从Directive派生下来,我们要覆盖几个方法。

  1、getName,返回一个字符串,这个就是你在velocity模板里使用的那个标签的名字:#cache。

  2、标签的类型,像我们的cache这种#cache…#end的叫块级标签,那么返回的就是一个BLOCK(常量1)。还有一个类型是LINE(2),那就没有那个#end了。

  3、render方法,这是最主要的,你可以覆盖一些行为。

  取外部传入的参数

  那么在标签内部如何取得外部传入的参数呢?其实它的行为和xml path的操作方式差不多。在render方法的参数里有一个Node,这个就是标签自身。我们可以通过node.jjtGetChild(index)来取得各种参数。以0开始,如果是BLOCK类型的标签,那么最后一个就是标签包围的内容了(比如我们的cache标签)。然后我们可以通过node的value取到参数的值。取值的时候还将context传入进去了,这说明这个值是可计算的。比如现在有这么一个需求,我们的图书列表是分页的,但是只缓存第一页,后面的不缓存。那我们就期望能传入

  一个表达式,让cache标签自己计算一把,如果这个表达式为true则缓存,否则不缓存:

 
   
#cache( " book_list " ,$pageIndex == 1 ,$dataProvider)
...
#end
那么在cache标签内部呢:
 
 
Node needCacheNode = node.jjtGetChild( 1 );
Boolean needCache
= (Boolean) needCacheNode.value(context);

String cacheHtml 上海企业网站制作pan>= StringUtils.EMPTY;
if (needCache) {
cacheHtml
= (tring) CacheManager.getInstance().get(cacheKey);
}

  这样就可以计算出$pageIndex==1这个表达式的结果(当然,$pageIndex这个变量是需要传入进来的)。

  好了,编写好自定义标签的代码我们可以使用了。而并不是我们写好这代码往那儿一丢就可以使用了,还需要一个配置环节:

 
 
< bean class ="org.springframework.web.servlet.view.velocity.VelocityConfigurer" >
< property name ="resourceLoaderPath" value ="/WEB-INF/templates/" />
< property name ="velocityProperties" >
< props >
< prop key ="userdirective" > com.yuyijq.web.Cache </ prop >
</ props >
</ property >
</ bean >

  好了,这么一个利用Velocity的片段缓存就完成了。但是这种方式也存在一些问题,给我们带来了一些bug。

  1、有的时候我们会在片段里set一些变量,然后在这个片段外使用。但是现在使用了cache之后,cache之后的是HTML

  文本,这些变量全部消失了,那么片段外也取不到这些变量的值了。那我们就需要仔细搜查Velocity模板,将这些变量的使用全部移动到片段内部。如果是一开始就设计了这个片段缓存还好,我们可以注意这个问题。但问题是现在是项目的中途提出的,系统中velocity模板成千上万,我们每缓存一个片段就要仔细检查一番,而且Velocity模板没有测试(当然可以写测试,但很麻烦)。这个过程全部靠人肉,所以出bug的几率会很高。

  2、还是一样,改动比较大,不仅模板,后面的控制器也需要修改。不过貌似也没什么更好的方法,要使用片段缓存貌似改动是避免不了的。

  成熟的方案

  片段缓存应该是大型系统里经常采用的,那么就应该有一些成熟的方案。我们为何不寻找那些成熟的方案要重新制造轮子呢。这个方案就是ESI,在下一篇文章中我会介绍结合Varnish和ESI来做片段缓存。

  页面片段缓存(二)

目录
相关文章
|
5月前
|
开发框架 JavaScript 前端开发
揭秘:如何让你的asp.net页面变身交互魔术师——先施展JavaScript咒语,再引发服务器端魔法!
【8月更文挑战第16天】在ASP.NET开发中,处理客户端与服务器交互时,常需先执行客户端验证再提交数据。传统上使用ASP.NET Button控件直接触发服务器事件,但难以插入客户端逻辑。本文对比此法与改进方案:利用HTML按钮及JavaScript手动控制表单提交。后者通过`onclick`事件调用JavaScript函数`SubmitForm()`来检查输入并决定是否提交,增强了灵活性和用户体验,同时确保了服务器端逻辑的执行。
64 5
|
8月前
|
开发框架 .NET 中间件
七天.NET 8操作SQLite入门到实战 - (2)第七天Blazor班级管理页面编写和接口对接
七天.NET 8操作SQLite入门到实战 - (2)第七天Blazor班级管理页面编写和接口对接
176 7
|
4月前
|
缓存 JavaScript
vue使用keep-alive实现页面前进刷新,后退缓存,完美运行无bug
vue使用keep-alive实现页面前进刷新,后退缓存,完美运行无bug
649 1
|
5月前
|
开发框架 前端开发 .NET
七天.NET 8操作SQLite入门到实战 - (3)第七天Blazor学生管理页面编写和接口对接
七天.NET 8操作SQLite入门到实战 - (3)第七天Blazor学生管理页面编写和接口对接
|
5月前
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
|
7月前
|
机器学习/深度学习 JSON 测试技术
CNN依旧能战:nnU-Net团队新研究揭示医学图像分割的验证误区,设定先进的验证标准与基线模型
在3D医学图像分割领域,尽管出现了多种新架构和方法,但大多未能超越2018年nnU-Net基准。研究发现,许多新方法的优越性未经严格验证,揭示了验证方法的不严谨性。作者通过系统基准测试评估了CNN、Transformer和Mamba等方法,强调了配置和硬件资源的重要性,并更新了nnU-Net基线以适应不同条件。论文呼吁加强科学验证,以确保真实性能提升。通过nnU-Net的变体和新方法的比较,显示经典CNN方法在某些情况下仍优于理论上的先进方法。研究提供了新的标准化基线模型,以促进更严谨的性能评估。
189 0
|
8月前
|
存储 缓存 JavaScript
vue中缓存页面数据(刷新不丢失)
vue中缓存页面数据(刷新不丢失)
556 1
|
8月前
|
机器学习/深度学习 算法 数据可视化
MATLAB基于深度学习U-net神经网络模型的能谱CT的基物质分解技术研究
MATLAB基于深度学习U-net神经网络模型的能谱CT的基物质分解技术研究
|
15天前
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
|
4月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
53 7