OneBlog开源博客-详细介绍如何实现freemarker自定义标签
前言
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自定义标签的更多相关文章
- 推荐一款自研的Java版开源博客系统OneBlog
OneBlog 一款超好用的Java版开源博客 Introduction 简介 OneBlog 一个简洁美观.功能强大并且自适应的Java博客.使用springboot开发,前端使用Boot ...
- 使用linux服务器安装wordpress博客详细教程
前言 最近读了<软技能:代码之外的生存指南>,这本书给了我很大的启示.之前虽然知道作为一个程序员,应该拥有自己的博客,以便于提升自己的知名度,但是并没有了解的过于详细.这本书描写博客的作用 ...
- Java开源博客My-Blog(SpringBoot+Docker)系列文章
My Blog 1.Docker+SpringBoot+Mybatis+thymeleaf的Java博客系统开源啦 2.My-Blog搭建过程:如何让一个网站从零到可以上线访问 3.将数据的初始化放到 ...
- Java 开源博客 Solo 1.8.0 发布 - 改进文件上传
本次发布主要是更新了编辑器,使其更好地支持文件上传.(1.8.0 版本变更记录请看这里) 我们的 Markdown 编辑器: 另外,我们对 HTTPS 的支持也更完善了,欢迎大家试用! 简介 Solo ...
- Java开源博客My-Blog之mysql容器重复初始化的严重bug修复过程
写在前面的话 <Docker+SpringBoot+Mybatis+thymeleaf的Java博客系统开源啦> <Java开源博客My-Blog之docker容器组件化修改> ...
- Java 开源博客 Solo 1.6.0 发布 - 新后台
简介 Solo 是一款一个命令就能搭建好的 Java 开源博客系统,并内置了 15+ 套精心制作的皮肤.除此之外,Solo 还有着非常活跃的社区,文章分享到社区后可以让很多人看到,产生丰富的交流互动. ...
- 基于开源博客系统(jpress)搭建网站
基于开源博客系统(jpress)搭建网站 JPress 使用 Java8 开发,基于流行的JFinal和Jboot框架. 目前JPress已经内置的文章和页面其实是两个模块,可以移除和新增其他模块,因 ...
- 基于开源博客系统(mblog)搭建网站
基于开源博客系统(mblog)搭建网站 上一章讲了基于jpress部署的博客系统,这一章了解一下 mblog这个开源的基于springboot的博客系统,相比与jpress 的热度fork数量要少一些 ...
- Java程序员博客系统推荐!我调研了100来个 Java 开源博客系统,发现这 5 个最好用!
大家好!我是 Guide 哥,Java 后端开发.一个会一点前端,喜欢烹饪的自由少年. 最近想倒腾一下博客,看了很多现成的比较成熟的开源博客系统,自己也简单从下面几个维度总结对比了一下: star数量 ...
随机推荐
- day1(ModelViewSet序列化限流排序)
1.DRF初始化 1.认证 2.权限 3.限流 4.序列化 5.分页 6.版本 7.过滤 8.排序 1.1安装DjangoRestFramework pip install djangoresfra ...
- requests请求高德地图api
高德地图通过GET方式发送url请求数据.url里需要包含开发者key以及一些请求的具体参数.(详情可见高德官网)高德返回的数据默认为JSON格式,方便处理. 顺带来回忆一下requests模块的一些 ...
- python核心高级学习总结6------面向对象进阶之元类
元类引入 在多数语言中,类就是一组用来描述如何生成对象的代码段,在python中同样如此,但是在python中把类也称为类对象,是的,你没听错,在这里你只要使用class关键字定义了类,其解释器在执行 ...
- PyQt(Python+Qt)学习随笔:QTreeWidget的topLevelItemCount属性
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QTreeWidget的topLevelItemCount属性是一个只读属性,用于保存树型部件中顶层 ...
- DVWA SQL Injection High
High 虽然是high等级,但是通过源码审计发现与low等级一样,没有对传入的值做任何过滤,唯一不同的就是点击连接后打开了另外一个对话框,用户在新打开的页面输入 其余的步骤与low级别的一样:htt ...
- 查询满足条件的最新数据(逐步优化,mysql、达梦数据库)
1.条件:报警信息表sensor_warning 2.需求: 查询当前车厢的.不同设备的.所有处理未完成的.不同报警原因的.时间最新的数据集合,最后按设备id或报警时间排序 3.原始sql,不满足实际 ...
- Intellij IDEA新导入项目运行出现Error:(60, 47) java: -source 1.5 中不支持 diamond 运算符 (请使用 -source 7 或更高版本以启用 diamond 运算符)
后台窗口报错如下: 问题原因 项目jdk版本配置不正确. 解决方案 ①File ->Project Structure ② ③之后还要检查一下这里 Settings-->Build,Exe ...
- js原生方法promise的实现
一会儿就要回家过年了,再来手写一个promise吧,要不等着下班真的煎熬... <!DOCTYPE html> <html lang="en"> <h ...
- Panda Global获悉,美国承诺4年内明确区块链数字资产监管方式!
近日,美国商品期货交易委员会(CFTC)宣布,在4年内将会全面把加密货币监管列为优先事项.Panda Global从7月8日公布的新战略中获悉,此次CFTC公布了自己接下来的新框架,并且在框架中承诺: ...
- Windows的API功能查询
在逆向分析时,一些Windows的API函数往往是我们的突破口.但这些函数很难记得一清二楚,以下是我的查找办法,做个小结. 官网 https://docs.microsoft.com/en-us/wi ...