- 调研类型:
Freemarker,Thymeleaf,Beetl,Velocity
- 调研方向:
性能,活跃度,各自优缺点,应用实例
2.1、性能报告:
Jdk:1.8
Cpu: 8核12线程
Jvm : -Xms512m -Xmx512m
Benchmark | Version | Mode | Score | Score Error (99.9%) | Unit |
Beetl | 3.10.0.RELEASE | thrpt | 206504.2209 | 3396.544778 | ops/s |
Freemarker | 2.3.31 | thrpt | 6442.518356 | 965.471192 | ops/s |
Thymeleaf | 3.1.0.M2 | thrpt | 4668.987054 | 906.704589 | ops/s |
Velocity | 1.7 | thrpt | 24738.62415 | 251.33537 | ops/s |

2.2、活跃度:


springbo
ot1.5以后就已经停止对velocity的支持了,不仅是springboot。spring5也不再支持velocity了
2.4、各自优缺点:
模板 | 优势 | 缺点 | 安全性 | 官网 | |
Beetl | - 性能优越
- 已开源11年,一直在维护,比较稳定
| - 比较小众,应用量无从考证社区已作废
| Xss攻击:需要单独开发过滤器,工作量大:https://cxymm.net/article/weixin_33775582/91969237 | https://www.kancloud.cn/xiandafu/beetl3_guide/1992542 | |
FreeMarker | 1、应用广泛 2、有社区,1999年Freemarker 1 发布,2002 年重写并发布Freemarker 2,至今已经20年;语法简单,功能丰富,文档完整 3、语法比较简单,html语法,js,css都适用 | | Xss攻击防范:默认在模板的头部加上<#escape x as x?html>在尾部加上</#escape>,对模板中所有的变量进行html转义;<#noautoesc>标签取消转义; | http://freemarker.foofun.cn/app_faq.html | |
Thymeleaf | - springboot推荐,有专业团队开发维护
- 语法简单
| 性能差 | Xss攻击防范:默认会转义返回结果 | https://www.docs4dev.com/docs/zh/thymeleaf/3.0/reference/using_thymeleaf.html#introducing-thymeleaf | |
2.5、应用实例:
<!--freemarker--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!-- thymeleaf模板依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--ibeetl--> <dependency> <groupId>com.ibeetl</groupId> <artifactId>beetl-framework-starter</artifactId> <version>1.1.68.RELEASE</version> </dependency> |
thymeleaf |
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>静态页展示!</title> </head> <body> <table class="display table table-bordered" id="hidden-table-info" border="1"> <thead> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>学号</td> </tr> </thead> <tbody> <tr class="gradeX" th:each="stu:${stus}"> <td th:text="${stuStat.count}"></td> <td th:text="${stu.name}"></td> <td th:text="${stu.age}">Trident</td> <td th:text="${stu.number}">Trident</td> </tr> </tbody> </table> </body> </html> |
@Controller public class ThymeleafController { @RequestMapping("/thymeleaf") public String thymeleaf(Model model) { List<Student> stus = new ArrayList<>(); //表格内容的遍历 for (int i = 0; i < 1000; i++) { Student stu = new Student(); stu.setName("thymeleaf" + i); stu.setAge(i); stu.setNumber(i); stu.setBirthday(new Date()); stus.add(stu); } model.addAttribute("stus", stus); return "thymeleaf"; } } |
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context;
import java.io.*; import java.util.Map;
/** * @ClassName ThymeleafUtil * @Description * @Date 2022/6/22 **/ public class ThymeleafUtil { private static Logger logger = LoggerFactory.getLogger(ThymeleafUtil.class);
/** * @param templateFile 模板名称 * @param data 数据 * @param suffix 模板后缀 * @param path 静态html文件保存路径 * @return 文件名称 */ public static String createHtml(String templateFile, Map data, String path) { TemplateEngine templateEngine = SpringContextHolder.getBean(TemplateEngine.class); Writer out = null; try { //文件递归创建生成文件目录 File realDirectory = new File(path); if (!realDirectory.exists()) { realDirectory.mkdirs(); } //生成文件名 String fileName = UUIDUtil.randomUUID() + ".html"; //初始化一个IO流 out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(path + fileName)), "UTF-8")); // 模板渲染出所要的内容 Context context = new Context(); context.setVariables(data); templateEngine.process(templateFile,context,out);
return path+fileName;
} catch (Exception e) { logger.error("生成html模板失败",e); throw new RuntimeException("生成html模板失败"); } finally { try { out.flush(); out.close(); } catch (IOException e) { logger.error("输出流关闭失败",e); } } } } |
spring: thymeleaf: cache: false prefix: classpath:/templates/ encoding: UTF-8 #编码 suffix: .html #模板后缀 mode: HTML #模板 |
Freemarker |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>静态页展示!</title> </head> <body> <table> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>学号</td> </tr> <#list stus as stu> <tr> <td>${stu_index + 1}</td> <td <#if stu.name =='freemarker小明'>style="background:blue;"</#if>>${stu.name}</td> <td>${stu.age}</td> <td>${stu.number}</td> </tr> </#list> </table> </body> </html> |
@Controller public class FreemarkerController { @Autowired private Configuration configuration; @GetMapping("/freemarker") public String test1(Map map) throws IOException, TemplateException { List<Student> stus = new ArrayList<>(); //表格内容的遍历 for (int i = 0; i < 1000; i++) { Student stu = new Student(); stu.setName("freemarker" + i); stu.setAge(i); stu.setNumber(i); stu.setBirthday(new Date()); stus.add(stu); } // 向数据模型放数据 map.put("stus", stus); return "freemarker"; } } |
import freemarker.template.Configuration; import freemarker.template.Template; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import java.io.*; import java.util.Map; import java.util.Objects;
import static freemarker.template.Configuration.VERSION_2_3_30;
/** * FreeMarker生成HTML模板
*/ public class FreeMarkerUtil {
private static Logger logger = LoggerFactory.getLogger(FreeMarkerUtil.class);
/** * @param templateFile 模板名称 * @param data 数据 * @param suffix 模板后缀 * @param path 静态html文件保存路径 * @return 文件名称 */ public static String createHtml(String templateFile, String suffix, Map data, String path) { Configuration cfg = SpringContextHolder.getBean(Configuration.class); Writer out = null; try { //根据模板名称获取模板文件 Template template = cfg.getTemplate(templateFile + suffix); if (Objects.isNull(template)) { logger.error("模板文件不存在"); throw new RuntimeException("模板文件不存在"); } //文件递归创建生成文件目录 File realDirectory = new File(path); if (!realDirectory.exists()) { realDirectory.mkdirs(); } //生成文件名 String fileName = UUIDUtil.randomUUID() + ".html"; //初始化一个IO流 out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(path + fileName)), "UTF-8")); // 模板渲染出所要的内容 template.process(data, out);
return path+fileName;
} catch (Exception e) { logger.error("生成html模板失败",e); throw new RuntimeException("生成html模板失败"); } finally { try { out.flush(); out.close(); } catch (IOException e) { logger.error("输出流关闭失败",e); } } } } |
spring: freemarker: #指定HttpServletRequest的属性是否可以覆盖controller的model的同名项 allow-request-override: false #req访问request request-context-attribute: request #后缀名freemarker默认后缀为.ftlh,当然你也可以改成自己习惯的.html suffix: .ftlh
#设置响应的内容类型 content-type: text/html;charset=utf-8 #是否允许mvc使用freemarker enabled: true #是否开启template caching cache: false #设定模板的加载路径,多个以逗号分隔,默认: [“classpath:/templates/”] template-loader-path: classpath:/templates/ #设定Template的编码 charset: UTF-8 |
beetl |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>静态页展示!</title> <style type="text/css"> /*<![CDATA[*/ body { color: #333333; line-height: 150%; } thead { font-weight: bold; background-color: #CCCCCC; } .odd { background-color: #FFCCCC; } .even { background-color: #CCCCFF; } .minus { color: #FF0000; } /*]]>*/ </style> </head> <body> <table> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>学号</td> </tr> <% for(stu in stus) { %> <% if(stuLP.index%2==0) { %> <tr class="even"> <% }else{ %> <tr class="odd"> <% } %> <td>${stuLP.index}</td> <td>${stu.name}</td> <td>${stu.age}</td> <td>${stu.number}</td> <td>${stu.birthday,"yyyy-MM/dd"}</td> <td>${date()} </td> </tr> <% } %> </table> </body> </html> |
@Controller public class BeetlController { @GetMapping("/test") public String test(HttpServletRequest request) throws IOException { List<Student> stus = new ArrayList<>(); //表格内容的遍历 for (int i = 0; i < 20; i++) { Student stu = new Student(); stu.setName("beetl" + i); stu.setAge(i); stu.setNumber(i); stu.setBirthday(new Date()); stus.add(stu); } request.setAttribute("stus", stus); return "beetl.html"; } } |
import org.beetl.core.GroupTemplate; import org.beetl.core.Template; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import java.io.*; import java.util.Map; import java.util.Objects;
/** * @ClassName BeetlUtil * @Description
* @Date 2022/6/22 **/ public class BeetlUtil { private static Logger logger = LoggerFactory.getLogger(BeetlUtil.class);
/** * @param templateFile 模板名称 * @param data 数据 * @param suffix 模板后缀 * @param path 静态html文件保存路径 * @return 文件名称 */ public static String createHtml(String templateFile, String suffix, Map data, String path) { Writer out = null; try { //根据模板名称获取模板文件 GroupTemplate gt = SpringContextHolder.getBean(GroupTemplate.class); Template template = gt.getTemplate(templateFile + suffix); if (Objects.isNull(template)) { logger.error("模板文件不存在"); throw new RuntimeException("模板文件不存在"); } //文件递归创建生成文件目录 File realDirectory = new File(path); if (!realDirectory.exists()) { realDirectory.mkdirs(); } //生成文件名 String fileName = UUIDUtil.randomUUID() + ".html"; //初始化一个IO流 out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(path + fileName)), "UTF-8")); // 模板渲染出所要的内容 template.binding(data); template.renderTo(out);
return path+fileName;
} catch (Exception e) { logger.error("生成html模板失败",e); throw new RuntimeException("生成html模板失败"); } finally { try { out.flush(); out.close(); } catch (IOException e) { logger.error("输出流关闭失败",e); } } } } |
注意:beetl使用时还须增加如下代码配置:
import org.beetl.core.resource.ClasspathResourceLoader; import org.beetl.ext.spring.BeetlGroupUtilConfiguration; import org.beetl.ext.spring.BeetlSpringViewResolver; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class BeetlConf {
@Value("${beetl.templatesPath}") String templatesPath;//模板根目录 ,比如 "templates" @Bean(name = "beetlConfig") public BeetlGroupUtilConfiguration getBeetlGroupUtilConfiguration() { BeetlGroupUtilConfiguration beetlGroupUtilConfiguration = new BeetlGroupUtilConfiguration(); //获取Spring Boot 的ClassLoader ClassLoader loader = Thread.currentThread().getContextClassLoader(); if(loader==null){ loader = BeetlConf.class.getClassLoader(); } //beetlGroupUtilConfiguration.setConfigProperties(extProperties);//额外的配置,可以覆盖默认配置,一般不需要 ClasspathResourceLoader cploder = new ClasspathResourceLoader(loader, templatesPath); beetlGroupUtilConfiguration.setResourceLoader(cploder); beetlGroupUtilConfiguration.init(); //如果使用了优化编译器,涉及到字节码操作,需要添加ClassLoader beetlGroupUtilConfiguration.getGroupTemplate().setClassLoader(loader); return beetlGroupUtilConfiguration;
}
@Bean(name = "beetlViewResolver") public BeetlSpringViewResolver getBeetlSpringViewResolver(@Qualifier("beetlConfig") BeetlGroupUtilConfiguration beetlGroupUtilConfiguration) { BeetlSpringViewResolver beetlSpringViewResolver = new BeetlSpringViewResolver(); beetlSpringViewResolver.setContentType("text/html;charset=UTF-8"); beetlSpringViewResolver.setOrder(0); beetlSpringViewResolver.setConfig(beetlGroupUtilConfiguration); return beetlSpringViewResolver; }
} |
beetl: enabled: true suffix: .html templatesPath: templates beetl-beetlsql: dev: true #监控模板变更 |