项目背景
公司开发新的项目,选择了分布式系统架构,同时项目中有大量图文展示的需求,考虑到开发效率和性能问题,需要集成cms的功能,程序实现后台录入数据静态化。
技术架构
Java开发、jdk1.8,项目整体的技术架构大致是这样的:
前端采用的bootstrap、extJs,权限采用shiro框架,搜索采用solr或者lucene(这个暂时没有实现)。
过往问题
在这个项目之前,公司有另一套cms系统,做为公司主要经营业务的基础,它有很多优点,最主要的优点是定制性强,能适应各种前端版式的页面。另一个优点是开发速度特别快,基于模版标签开发,屏蔽了代码级的复杂度和开发人员水平的差距。
弱点有三个
一、在于标签库的实现,完全是基于正则替换,然后处理标签表述的逻辑进行查库、处理。标签没有实现语法树,也没有上下文,导致了多个标签函数嵌套的时候,某些时候无法正确实现替换逻辑。
二、没有应用更多的缓存技术(只实现了jvm内的HashMap缓存),数据量大的任务,生成速度在查库上,有明显的瓶颈。
三、生成失败的模版,没有给出足够明确的错误提示,辅助使用者调试错误。
基本思路
整个大体流程
整个生成是基于freemarker模版引擎,项目初期,曾经对比过velocity与freemarker,因为freemarker最后一次更新时间比较近,最终选择了freemarker,其实两者应该差不多。
关键点
先晒一下目前的类结构
一、自定义标签与自定义函数
freemarker支持实现TemplateMethodModelEx接口的自定义函数,及实现TemplateDirectiveModel的自定义标签。上面代码,fun包下面的就是自定义函数,tags包下面的是自定义标签。
两者的区别,
1、从调用方法来看,
arts(10, 10, ”, ”)
这种是自定义函数<@arts menu="15" pagesize="15" sort="" sql="";arts, page></@arts>
这种是自定义标签
2、自定义函数有返回值,可以再套入别的函数进行计算或者迭代。自定义标签没有返回值,它会返回几个变量,作用域只在标签体内部。
3、自定义标签传入的参数有Environment、TemplateDirectiveBody等等,即可以获取其他位置定义的模版变量,又可以追加变量,还可以输出字符串文本到最终生成的页面上。自定义函数,只能仅仅返回处理后的返回值。
二、freemarker生成静态页到指定路径的方法
这个问题,基本上查api或者别的博客就能找到具体的方法,但是有些资料可能不准确,这里我直接把我实现了的核心代码贴出来。我使用的freemarker版本是2.3.23。
//成员变量
private static Configuration config = new Configuration(Configuration.VERSION_2_3_25);
//config设置
config.setLocale(Locale.CHINA);
config.setDefaultEncoding("utf-8");
config.setEncoding(Locale.CHINA, "utf-8");
config.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_25));
StringTemplateLoader stringLoader = new StringTemplateLoader();
//content是模版内容的字符串 stringLoader.putTemplate("siteTemplate",content);
config.setTemplateLoader(stringLoader);
try(FileWriter out = new FileWriter(filePath.toFile());) {
Template template = config.getTemplate("siteTemplate","utf-8");
config.setCustomAttribute("customDataModel", dataModel);
template.process(dataModel, out);
out.flush();
} catch (Exception e) {
//异常处理
}
三、列表页分页,如何根据一个模版,动态生成出一或多个分页面
这个问题包括很多细节,比如生成后的地址规则。假设目标地址是news_index.html,那么第二页就不能用这个地址,我的处理方式是news_index_x.html,x代表第几页,可以是2~n。但是第一页就是原地址。
最关键
如果你也需要基于freemarker实现这个功能,大概机会发现,最关键的问题,是如何在生成第一页的时候,知道还有第二页需要生成。因为产生多个分页面,一般是在自定义标签里面,比如有一个循环数据列表的标签,发现一页10条,无法显示完,怎样告知生成阶段的代码(就是上面那一段)。这是两个完全不同的代码区域和处理阶段。
参考了另一款开源cms,我采取了一种有点技巧性的做法。核心思想就是,上面说到自定义标签,可以在生成后的页面上,任意输出字符串。假设标签中判断还有下一页,需要生成,就在页面上输出一段注释,本页面生成完以后,重新读取生成的页面,发现有包含此种注释,则继续生成下一页。
下面贴一些代码
//输出还有下一页的标识
env.getOut().write(LBLCommon.getHasNextPage(totalRecord, totalPage, pageIndex));
public static String getHasNextPage(int recordCount, int totalPage, int pageindex) {
return hasNextPage+"_"+recordCount+"_"+totalPage+"_"+pageindex+"-->";
}
//生成静态化页面 this.writeUseFreeMarker(name, content, url, dataModel);
//分页机制,查询是否需要分页————————————————————
this.morePageProcess(name, dataModel, url, rootPath, content, entry);
//处理分页
private void morePageProcess(String name, Map<String, Object> dataModel, String url, String rootPath, String content, Map.Entry<String, String> entry) throws Exception{
String html = this.readFile(url);
LBLPagerModel pager = checkHasNextPage(html);
if (pager != null) {
//重设置一个基础的总记录数
dataModel.put("totalRecord", pager.getRecordCount());
for (int i = 2; i<= pager.getTotalPage(); i ++) {
//设置页码
setPageIndex(dataModel, i);
url = rootPath + LBLCommon.getPageUrl(entry.getValue(), i);
this.writeUseFreeMarker(name, content, url, dataModel);
}
}
}
3
整个静态化的部分,还有很多别的细节和坑,比如标签参数的转型问题,生成静态页过程中模版exception的catch和反馈,查询生成队列情况的监控模块(基于mongo),标签设计,自动生成、定时生成、过滤生成…
总结
静态化的功能,与业务结合的很紧密,好的标签设计非常重要,这个可能得靠多踩坑才能提升一点躲开雷区的能力。另外,这次的开发过程,也让我更深刻的认识到,深思熟虑,先慢后快,不写一行垃圾代码,才是最节约时间的开发模式。