前言

OneBlog中使用到了springboot + freemarker的技术,同时项目里多个controller中都需要查询一个公有的数据集合,一般做法是直接在每个controller的方法中通过 model.addAttribute("xx",xx);的方式手动设置,但这样就有个明显的问题:重复代码。同一个实现需要在不同的controller方法中设置,除了重复代码外,还会给后期维护造成不必要的麻烦。在以往的jsp项目中,可以通过taglib实现自定义标签,那么,在freemarker中是否也可以实现这种功能呢?今天就尝试一下在freemarker中如何使用自定义标签。

TemplateDirectiveModel

在freemarker中实现自定义的标签,主要就是靠 TemplateDirectiveModel类。如字面意思:模板指令模型,主要就是用来扩展自定义的指令(和freemarker的宏类似,自定义标签也属于这个范畴)

1 public interface TemplateDirectiveModel extends TemplateModel {
2 void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException;
3 }

TemplateDirectiveModel是一个接口,类中只有一个execute方法供使用者实现,而我们要做的就是通过实现execute方法,实现自定义标签的功能。当页面模板中使用自定义标签时,会自动调用该方法。

先来看一下execute方法的参数含义

env : 表示模板处理期间的运行时环境。该对象会存储模板创建的临时变量集、模板设置的值、对数据模型根的引用等等,通常用它来输出相关内容,如Writer out = env.getOut()。
params : 传递给自定义标签的参数(如果有的话)。其中map的key是自定义标签的参数名,value值是TemplateModel实例【1】。
loopVars : 循环替代变量 (未发现有什么用,希望知道的朋友能指教一二)
body : 表示自定义标签中嵌套的内容。说简单点就是自定义标签内的内容体。如果指令调用没有嵌套内容(例如,就像<@myDirective />或者<@myDirective>),那么这个参数就会为空。

【1】:TemplateModel是一个接口类型,代表FreeMarker模板语言(FTL)数据类型的接口的公共超接口,即所有的数据类型都会被freemarker转成对应的TemplateModel。通常我们都使用TemplateScalarModel接口来替代它获取一个String 值,如TemplateScalarModel.getAsString();当然还有其它常用的替代接口,如TemplateNumberModel获取number等

类型 FreeMarker接口 FreeMarker实现
字符串 TemplateScalarModel SimpleScalar
数值 TemplateNumberModel SimpleNumber
日期 TemplateDateModel SimpleDate
布尔 TemplateBooleanModel TemplateBooleanModel.TRUE
哈希 TemplateHashModel SimpleHash
序列 TemplateSequenceModel SimpleSequence
集合 TemplateCollectionModel SimpleCollection
节点 TemplateNodeModel NodeModel

实现自定义标签

前面了解了 TemplateDirectiveModel的基本含义和用法,那么,接下来我们就以OneBlog中的例子来简单解释下如何实现自定义标签。

ps:为了方便阅读,本例只摘出了一部分关键代码,详细内容,请参考我的开源博客:https://gitee.com/yadong.zhang/DBlog

一、创建类实现TemplateDirectiveModel接口

 1 @Component
2 public class CustomTagDirective implements TemplateDirectiveModel {
3 private static final String METHOD_KEY = "method";
4 @Autowired
5 private BizTagsService bizTagsService;
6
7 @Override
8 public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
9 if (map.containsKey(METHOD_KEY)) {
10 DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25);
11 String method = map.get(METHOD_KEY).toString();
12 switch (method) {
13 case "tagsList":
14 // 将数据对象转换成对应的TemplateModel
15 TemplateModel tm = builder.build().wrap(bizTagsService.listAll())
16 environment.setVariable("tagsList", tm);
17 break;
18 case other...
19 default:
20 break;
21 }
22 }
23 templateDirectiveBody.render(environment.getOut());
24 }
25 }

二、创建freemarker的配置类

 1 @Configuration
2 public class FreeMarkerConfig {
3
4 @Autowired
5 protected freemarker.template.Configuration configuration;
6 @Autowired
7 protected CustomTags customTags;
8
9 /**
10 * 添加自定义标签
11 */
12 @PostConstruct
13 public void setSharedVariable() {
14 /*
15 * 向freemarker配置中添加共享变量;
16 * 它使用Configurable.getObjectWrapper()来包装值,因此在此之前设置对象包装器是很重要的。(即上一步的builder.build().wrap操作)
17 * 这种方法不是线程安全的;使用它的限制与那些修改设置值的限制相同。
18 * 如果使用这种配置从多个线程运行模板,那么附加的值应该是线程安全的。
19 */
20 configuration.setSharedVariable("zhydTag", customTags);
21 }
22 }

三、ftl模板中使用自定义标签

 1 <div class="sidebar-module">
2 <h5 class="sidebar-title"><i class="fa fa-tags icon"></i><strong>文章标签</strong></h5>
3 <ul class="list-unstyled list-inline">
4 <@zhydTag method="tagsList" pageSize="10">
5 <#if tagsList?exists && (tagsList?size > 0)>
6 <#list tagsList as item>
7 <li class="tag-li">
8 <a class="btn btn-default btn-xs" href="${config.siteUrl}/tag/${item.id?c}" title="${item.name?if_exists}">
9 ${item.name?if_exists}
10 </a>
11 </li>
12 </#list>
13 </#if>
14 </@zhydTag>
15 </ul>
16 </div>

自定义标签的使用方法跟自定义宏(macro)用法一样,直接使用`<@标签名>${值}</@标签名>`即可。

注:ftl中通过@调用自定义标签时,后面可以跟任意参数,所有的参数都可以在execute方法的第二个参数(map)中获取,由此可以根据一个特定的属性开发一套特定的自定义标签,比如OneBlog中通过method参数判断调用不同的处理方式。

四、扩展FreeMarkerConfig

上面提到的自定义标签,都是通过 <@tagName>xxx</@tagName>方式调用的,那么针对我们系统中一些类环境变量的数据(全局的配置类属性等)如何像使用普通的el表达式一般直接通过${xx}获取呢? 看代码:

 1 @Configuration
2 public class FreeMarkerConfig {
3
4 @Autowired
5 protected freemarker.template.Configuration configuration;
6 @Autowired
7 private SysConfigService configService;
8
9 /**
10 * 添加自定义标签
11 */
12 @PostConstruct
13 public void setSharedVariable() {
14 try {
15 configuration.setSharedVariable("config", configService.get());
16 } catch (TemplateModelException e) {
17 e.printStackTrace();
18 }
19 }
20 }

如此而已,在使用的时候我们可以直接在页面上通过${config.siteName}调用config的参数即可。

五、可能遇到的问题

针对上面两种标签( 类宏模式类el表达式模式),会有一个问题存在,如下图

在程序启动时会初始化FreemarkerConfig类(@PostConstruct),并且当且仅当程序启动时才会初始化一次。像 zhydTag这种自定义标签,因为是将整个自定义标签类(CustomTag)保存到了共享变量中,那么在使用自定义标签时,实际还是调用的相关接口获取数据库,当数据库发生变化时,也会同步更新到标签中;而像 config这种类el表达式的环境变量(如图,value的类型是一个StringModel),只会在程序初始化时加载一次,在后续调用标签时也只是调用的 SharedVariable中的config副本内容,并不会再次访问接口去数据库中获取数据。这样就造成了一个问题:当config表中的数据发生变化时,在前台通过${config.siteName}获取到的仍然是旧的数据

六、解决问题

在OneBlog中,我是通过实现一个简单的AOP,去监控、对比config表的内容,当config表发生变化时,将新的config副本保存到freeamrker的 SharedVariable中。如下实现

 1 /**
2 * 用于监控freemarker自定义标签中共享变量是否发生变化,发生变化时实时更新到内存中
3 *
4 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
5 * @version 2.0
6 * @date 2018/5/17 17:06
7 */
8 @Slf4j
9 @Component
10 @Aspect
11 @Order(1)
12 public class FreemarkerSharedVariableMonitorAspects {
13
14 private static volatile long configLastUpdateTime = 0L;
15 @Autowired
16 protected freemarker.template.Configuration configuration;
17 @Autowired
18 private SysConfigService configService;
19
20 @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.GetMapping)" +
21 "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)")
22 public void pointcut() {
23 // 切面切入点
24 }
25
26 @After("pointcut()")
27 public void after(JoinPoint joinPoint) {
28 Config config = configService.get();
29 if (null == config) {
30 log.error("config为空");
31 return;
32 }
33 Long updateTime = config.getUpdateTime().getTime();
34 if (updateTime == configLastUpdateTime) {
35 log.debug("config表未更新");
36 return;
37 }
38 log.debug("config表已更新,重新加载config到freemarker tag");
39 configLastUpdateTime = updateTime;
40 try {
41 configuration.setSharedVariable("config", config);
42 } catch (TemplateModelException e) {
43 e.printStackTrace();
44 }
45 }
46 }

当然, 虽然OneBlog中是使用的AOP方式解决问题,我们使用过滤器、拦截器也是一样的道理,

代码调优

上面介绍的编码实现方式,我们必须通过 switch...case去挨个判断实际的处理逻辑,在同一个标签类中有太多具体标签实现时,就显得比较笨重。因此,我们简单的优化一下代码,使它看起来不是那么糟糕并且易于扩展。

一、首先,分析代码,将公共模块提取出来。

TemplateDirectiveModel类的 execute方法是每个自定义标签类都必须实现的,并且每个自定义标签都是根据 method参数去使用具体的实现,这一块我们可以提成公共模块:

 1 /**
2 * 所有自定义标签的父类,负责调用具体的子类方法
3 *
4 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
5 * @version 1.0
6 * @website https://www.zhyd.me
7 * @date 2018/9/18 16:19
8 * @since 1.8
9 */
10 public abstract class BaseTag implements TemplateDirectiveModel {
11
12 private String clazzPath = null;
13
14 public BaseTag(String targetClassPath) {
15 clazzPath = targetClassPath;
16 }
17
18 private String getMethod(Map params) {
19 return this.getParam(params, "method");
20 }
21
22 protected int getPageSize(Map params) {
23 int pageSize = 10;
24 String pageSizeStr = this.getParam(params, "pageSize");
25 if (!StringUtils.isEmpty(pageSizeStr)) {
26 pageSize = Integer.parseInt(pageSizeStr);
27 }
28 return pageSize;
29 }
30
31 private void verifyParameters(Map params) throws TemplateModelException {
32 String permission = this.getMethod(params);
33 if (permission == null || permission.length() == 0) {
34 throw new TemplateModelException("The 'name' tag attribute must be set.");
35 }
36 }
37
38 String getParam(Map params, String paramName) {
39 Object value = params.get(paramName);
40 return value instanceof SimpleScalar ? ((SimpleScalar) value).getAsString() : null;
41 }
42
43 private DefaultObjectWrapper getBuilder() {
44 return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25).build();
45 }
46
47 private TemplateModel getModel(Object o) throws TemplateModelException {
48 return this.getBuilder().wrap(o);
49 }
50
51
52 @Override
53 public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
54 this.verifyParameters(map);
55 String funName = getMethod(map);
56 Method method = null;
57 try {
58 Class clazz = Class.forName(clazzPath);
59 method = clazz.getDeclaredMethod(funName, Map.class);
60 if (method != null) {
61 Object res = method.invoke(this, map);
62 environment.setVariable(funName, getModel(res));
63 }
64 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
65 e.printStackTrace();
66 }
67 templateDirectiveBody.render(environment.getOut());
68 }
69
70 }

BaseTag作为所有自定义标签的父类,只需要接受一个参数:clazzPath,即子类的类路径(全类名),在实际的 execute方法中,只需要根据制定的 method,使用反射调用子类的相关方法即可。

二、优化后的标签类

 1 /**
2 * 自定义的freemarker标签
3 *
4 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
5 * @version 1.0
6 * @website https://www.zhyd.me
7 * @date 2018/4/16 16:26
8 * @since 1.0
9 * @modify by zhyd 2018-09-20
10 * 调整实现,所有自定义标签只需继承BaseTag后通过构造函数将自定义标签类的className传递给父类即可。
11 * 增加标签时,只需要添加相关的方法即可,默认自定义标签的method就是自定义方法的函数名。
12 * 例如:<@zhydTag method="types" ...></@zhydTag>就对应 {{@link #types(Map)}}方法
13 */
14 @Component
15 public class CustomTags extends BaseTag {
16
17 @Autowired
18 private BizTypeService bizTypeService;
19
20 public CustomTags() {
21 super(CustomTags.class.getName());
22 }
23
24 public Object types(Map params) {
25 return bizTypeService.listTypeForMenu();
26 }
27
28 // 其他自定义标签的方法...
29 }

如上,所有自定义标签只需继承BaseTag后通过构造函数将自定义标签类的className传递给父类即可。增加标签时,只需要添加相关的方法即可,默认自定义标签的method就是自定义方法的函数名。

例如:<@zhydTag method="types" ...>就对应 CustomTags#types(Map)方法

如此一来,我们想扩展标签时,只需要添加相关的自定义方法即可,ftl中通过method指定调用哪个方法。

关注我的公众号

OneBlog开源博客-详细介绍如何实现freemarker自定义标签的更多相关文章

  1. 推荐一款自研的Java版开源博客系统OneBlog

    OneBlog 一款超好用的Java版开源博客      Introduction 简介 OneBlog 一个简洁美观.功能强大并且自适应的Java博客.使用springboot开发,前端使用Boot ...

  2. 使用linux服务器安装wordpress博客详细教程

    前言 最近读了<软技能:代码之外的生存指南>,这本书给了我很大的启示.之前虽然知道作为一个程序员,应该拥有自己的博客,以便于提升自己的知名度,但是并没有了解的过于详细.这本书描写博客的作用 ...

  3. Java开源博客My-Blog(SpringBoot+Docker)系列文章

    My Blog 1.Docker+SpringBoot+Mybatis+thymeleaf的Java博客系统开源啦 2.My-Blog搭建过程:如何让一个网站从零到可以上线访问 3.将数据的初始化放到 ...

  4. Java 开源博客 Solo 1.8.0 发布 - 改进文件上传

    本次发布主要是更新了编辑器,使其更好地支持文件上传.(1.8.0 版本变更记录请看这里) 我们的 Markdown 编辑器: 另外,我们对 HTTPS 的支持也更完善了,欢迎大家试用! 简介 Solo ...

  5. Java开源博客My-Blog之mysql容器重复初始化的严重bug修复过程

    写在前面的话 <Docker+SpringBoot+Mybatis+thymeleaf的Java博客系统开源啦> <Java开源博客My-Blog之docker容器组件化修改> ...

  6. Java 开源博客 Solo 1.6.0 发布 - 新后台

    简介 Solo 是一款一个命令就能搭建好的 Java 开源博客系统,并内置了 15+ 套精心制作的皮肤.除此之外,Solo 还有着非常活跃的社区,文章分享到社区后可以让很多人看到,产生丰富的交流互动. ...

  7. 基于开源博客系统(jpress)搭建网站

    基于开源博客系统(jpress)搭建网站 JPress 使用 Java8 开发,基于流行的JFinal和Jboot框架. 目前JPress已经内置的文章和页面其实是两个模块,可以移除和新增其他模块,因 ...

  8. 基于开源博客系统(mblog)搭建网站

    基于开源博客系统(mblog)搭建网站 上一章讲了基于jpress部署的博客系统,这一章了解一下 mblog这个开源的基于springboot的博客系统,相比与jpress 的热度fork数量要少一些 ...

  9. Java程序员博客系统推荐!我调研了100来个 Java 开源博客系统,发现这 5 个最好用!

    大家好!我是 Guide 哥,Java 后端开发.一个会一点前端,喜欢烹饪的自由少年. 最近想倒腾一下博客,看了很多现成的比较成熟的开源博客系统,自己也简单从下面几个维度总结对比了一下: star数量 ...

随机推荐

  1. moviepy音视频剪辑:输出文件前预览剪辑和视频帧的preview和imdisplay方法

    ☞ ░ 前往老猿Python博文目录 ░ 一.引言 由于moviepy合成剪辑后,输出需要较多的时间,那么在输出前预览一下有助于提高处理效率.moviepy提供了预览的工具,这个工具是基于pygame ...

  2. MySQL入门看这一篇就够了

    MySQL JavaEE:企业级Java开发 web阶段 分为1.前端(页面,展示数据库中的数据) 2.后台(连接点:链接数据库JDBC.Mybatis,链接前端(控制视图跳转,给前端传递数据)) 3 ...

  3. Day5 【Scrum 冲刺博客】

    每日会议总结 昨天已完成的工作 方晓莹(PIPIYing) 搭建与后台对接的代理服务器 对接个人中心接口 方子茵(Laa-L):暂无 黄芯悦(Sheaxx) 完善投诉反馈页面 完善车位管理页面 舒雯钰 ...

  4. 手动 jq 触发 动态的 layui select change 事件

    var s= $('#province').val(); //先获取   默认选中的第一个 option 的值 ( value)    var select = 'dd[lay-value=' + s ...

  5. 网络QoS的平衡之道——音视频弱网对抗策略介绍

    作者:网易智企云信资深音视频引擎开发工程师 王兴鹤 随着AI和5G的到来,音视频应用将变得越来越广泛,人们对音视频的品质需求也越来越高,视频分辨率已经从高清发展为超高清.VR,视频帧率也已出现60fp ...

  6. 笔记-[ZJOI2014]力

    [ZJOI2014]力 \[\begin{split} E_j=&\sum_{i=1}^{j-1}\frac{q_i}{(i-j)^2}-\sum_{i=j+1}^{n}\frac{q_i}{ ...

  7. deepstrem编译缺少gst/gst.h解决方案

    Deepstream在编译程序的程序的显示缺少gst/gst.h 具体情况是Deepstream运行已编译好的deepstream-app可以正常运行,但对源码编译的时候出现以述情况,初步分析是我们安 ...

  8. Servlet中获取请求参数问题

    1.GET方法,可以通过getParamter方法反复获取同一个变量的数据: 2.POST方法,需要注意请求类型(content-Type)是否是application/x-www-form-urle ...

  9. 汉化gitlab

    一.,基于 Larry Li 版汉化指南 修改 (以9-0-stable-zh分支为例) 源码安装汉化 推荐按照 gitlab-ce 源代码中 doc/install/installation.md ...

  10. Java中CAS原理分析(volatile和synchronized浅析)

    CAS是什么? CAS英文解释是比较和交换,是cpu底层的源语,是解决共享变量原子性实现方案,它定义了三个变量,内存地址值对应V,期待值E和要修改的值U,如下图所示,这些变量都是在高速缓存中的,如果两 ...