前言

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. Python基础概念学习遇到的问题

    Python运算符的优先级是怎样的? Python中的迭代是什么意思? Python中的列表解析和列表推导是一回事吗? Python中可迭代对象是什么? Python中容器指的是什么? 关于Pytho ...

  2. 第15.2节 PyCharm支持Python解释器的配置调整

    上节介绍了PyCharm的安装与简单使用,本节介绍PyCharm相关的配置调整,以支持在PyCharm环境下集成Python解释器进行程序的编译. 一. 工程配置调整 在执行文件前,可能需要对PyCh ...

  3. Mysql 逻辑架构图及日志系统

    我们经常能看到如下的逻辑架构图,但是往往不能进行很好的记忆,看过就忘记了,也不知道它的实现方式.今天通过简单的画图来简单了解一下mysql到底是如何执行一个select语句,如何update一条语句. ...

  4. (转)oracle体系结构

    对于一门技术的学习,尤其是像Oracle database这种知识体系极其庞杂的技术来讲,从宏观上了解其体系结构是至关重要的.同时,个人认为,未必是专业DBA人员才需要了解其体系结构(固然对于数据库专 ...

  5. ASP自动刷新页面的实现方法总结

    1) <meta http-equiv="refresh" content="10"> 10表示间隔10秒刷新一次 2) <script> ...

  6. APIO2020 粉刷墙壁

    考场想了 5.5 h,第一部分分死活打不出来,做到崩盘,现在重做,感觉自己就是一个sb,放学在地铁上一眼就会了.哎. 可以把一个要求看作一个长度为 \(m\) 的区间:\([l, l + m - 1] ...

  7. 转:解释lsh

    Locality sensitive hashing - LSH explained The problem of finding duplicate documents in a list may ...

  8. 精尽Spring MVC源码分析 - 文章导读

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  9. python 通过pip freeze、dowload打离线包及自动安装【适用于保密的离线环境】

    python的pip是其包管理工具,相当方便好用.本文只介绍pip 如何通过其freeze命令打离线包,及其离线包的安装脚本.这个知识点,特别适用于不适合连通互联网,设备需要物理隔绝,保密要求严格的客 ...

  10. python爬虫--用xpath爬豆瓣电影

    步骤 将目标网站下的页面抓取下来 将抓取下来的数据根据一定规则进行提取   具体流程 将目标网站下的页面抓取下来 1. 倒库 import requests 2.头信息(有时候可不写) headers ...