ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

FreeMarker模板引擎:Java动态内容生成实战指南

FreeMarker模板引擎:Java动态内容生成实战指南 1. FreeMarker模板引擎概述FreeMarker是一款基于Java的模板引擎它通过分离业务逻辑与展示逻辑实现了MVC架构中的视图层解耦。我第一次接触FreeMarker是在2012年参与一个电商平台项目时当时需要动态生成数千种商品详情页。传统JSP方案在应对这种大规模页面渲染时显得力不从心而FreeMarker以其简洁的语法和高效的渲染性能完美解决了这个问题。与JSP、Thymeleaf等其他模板技术相比FreeMarker的核心优势在于纯文本输出不局限于HTML可生成XML、JSON、纯文本甚至源代码逻辑与展示分离模板中只包含显示逻辑业务计算完全由Java代码处理零依赖不强制要求Servlet容器可在任何Java环境中运行高性能编译后的模板会缓存重复渲染时几乎无性能损耗实际项目中常见误区很多新手会把业务逻辑写在模板里这违反了FreeMarker的设计哲学。正确的做法是所有数据准备都在Java端完成模板只负责展示。2. 环境搭建与基础配置2.1 项目依赖引入对于Maven项目在pom.xml中添加dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version2.3.31/version /dependency如果是Gradle项目implementation org.freemarker:freemarker:2.3.312.2 核心配置类详解典型配置示例Configuration cfg new Configuration(Configuration.VERSION_2_3_31); cfg.setDirectoryForTemplateLoading(new File(/templates)); cfg.setDefaultEncoding(UTF-8); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); cfg.setLogTemplateExceptions(false); cfg.setWrapUncheckedExceptions(true);关键配置参数说明参数推荐值作用templateUpdateDelay5 (秒)模板文件检查间隔localizedLookupfalse禁用本地化查找booleanFormattrue,false布尔值格式化dateFormatyyyy-MM-dd日期格式datetimeFormatyyyy-MM-dd HH:mm:ss日期时间格式3. FTL模板语言深度解析3.1 基本语法结构一个完整的FTL模板示例#-- 这是注释 -- !DOCTYPE html html head title${user.name}的主页/title /head body #if user.gender male 尊敬的先生 #elseif user.gender female 尊敬的女士 #else 尊敬的客户 /#if ul #list products as product li${product.name} - ¥${product.price?string(0.00)}/li /#list /ul #include footer.ftl /body /html3.2 数据模型处理技巧复杂对象处理示例MapString, Object root new HashMap(); root.put(user, userService.getCurrentUser()); root.put(products, productService.getFeaturedProducts()); // 处理日期 root.put(now, new Date()); // 自定义方法 root.put(highlight, (str) - strong str /strong);模板中使用${highlight(product.name)} 最后更新${now?string(yyyy-MM-dd HH:mm)}3.3 高级指令详解3.3.1 宏定义与调用#macro productCard product div classcard img src${product.imageUrl} alt${product.name} h3${product.name}/h3 p价格${product.price?string(0.00)}/p #if product.stock lt; 10 p classwarning库存紧张/p /#if /div /#macro productCard productfeaturedProduct /3.3.2 嵌套内容处理#macro bordered div styleborder: 1px solid #ccc #nested /div /#macro bordered 这里的内容会被嵌套在边框div中 /bordered4. 实战应用场景4.1 Web页面渲染Spring Boot集成配置Configuration public class FreemarkerConfig { Bean public FreeMarkerConfigurationFactoryBean getFreeMarkerConfiguration() { FreeMarkerConfigurationFactoryBean bean new FreeMarkerConfigurationFactoryBean(); bean.setTemplateLoaderPath(classpath:/templates); bean.setDefaultEncoding(UTF-8); return bean; } }Controller示例Controller public class ProductController { GetMapping(/product/{id}) public String productDetail(PathVariable Long id, Model model) { model.addAttribute(product, productService.getById(id)); return product/detail; } }4.2 邮件模板生成邮件模板示例email_template.ftl#assign currencyFormat 0.00 尊敬的${user.name} 感谢您的订单 #${order.id}详情如下 #list order.items as item ${item.quantity} x ${item.productName} ¥${item.totalPrice?string(currencyFormat)} /#list 总计¥${order.totalAmount?string(currencyFormat)}生成代码public String generateOrderEmail(Order order) throws Exception { Configuration cfg new Configuration(Configuration.VERSION_2_3_31); cfg.setClassForTemplateLoading(getClass(), /templates/email); MapString, Object model new HashMap(); model.put(user, order.getUser()); model.put(order, order); Template template cfg.getTemplate(order_confirmation.ftl); StringWriter writer new StringWriter(); template.process(model, writer); return writer.toString(); }4.3 代码生成器实现数据库表转Java实体类生成器package ${packageName}; import java.util.Date; #if hasBigDecimal import java.math.BigDecimal; /#if public class ${className} { #list fields as field private ${field.javaType} ${field.name}; /#list #list fields as field public ${field.javaType} get${field.name?cap_first}() { return this.${field.name}; } public void set${field.name?cap_first}(${field.javaType} ${field.name}) { this.${field.name} ${field.name}; } /#list }5. 性能优化与最佳实践5.1 模板缓存策略// 生产环境推荐配置 cfg.setCacheStorage(new StrongCacheStorage()); cfg.setTemplateUpdateDelay(3600); // 1小时检查一次更新5.2 错误处理方案全局异常处理器cfg.setTemplateExceptionHandler((ex, env, out) - { if (env.isInAttemptBlock()) { throw ex; } out.write([渲染错误: ex.getMessage() ]); log.error(模板渲染错误, ex); });5.3 安全防护措施防止XSS攻击#-- 不安全的方式 -- ${userInput} #-- 安全的方式 -- ${userInput?html} #-- 处理JSON输出 -- ${userInput?json_string}6. 常见问题排查指南6.1 模板加载失败典型错误Could not resolve template: template.ftl解决方案检查模板路径是否配置正确确认文件扩展名是否为.ftl检查文件读取权限6.2 变量解析异常错误示例The following has evaluated to null or missing: user.name处理方案#-- 安全访问方式 -- ${user.name!} ${user.name!默认值} ${(user.name)!} #-- 条件判断 -- #if user?? user.name?? ${user.name} /#if6.3 日期格式化问题正确用法${someDate?string(yyyy-MM-dd)} ${someDate?datetime} ${someDate?time}时区处理cfg.setSQLDateAndTimeTimeZone(TimeZone.getDefault());7. 高级特性探索7.1 自定义指令开发实现分页指令public class PaginationDirective implements TemplateDirectiveModel { Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) { int current Integer.parseInt(params.get(current).toString()); int total Integer.parseInt(params.get(total).toString()); int size Integer.parseInt(params.get.get(size).toString()); int pages (int) Math.ceil((double)total / size); StringBuilder html new StringBuilder(); html.append(div class\pagination\); for (int i 1; i pages; i) { if (i current) { html.append(span class\current\).append(i).append(/span); } else { html.append(a href\?page).append(i).append(\).append(i).append(/a); } } html.append(/div); env.getOut().write(html.toString()); } }注册指令cfg.setSharedVariable(pagination, new PaginationDirective());模板中使用pagination currentpageNum totaltotalCount sizepageSize /7.2 模板继承机制基础模板base.ftl!DOCTYPE html html head title#block title默认标题/#block/title #block head/#block /head body #include header.ftl div classcontent #block content 默认内容 /#block /div #include footer.ftl /body /html子模板home.ftl#import base.ftl as layout layout.base #block title首页/#block #block head link relstylesheet href/css/home.css /#block #block content h1欢迎来到首页/h1 p当前用户${user.name}/p /#block /layout.base7.3 国际化支持多语言模板方案# messages_en.properties welcomeWelcome product.nameProduct Name # messages_zh.properties welcome欢迎 product.name商品名称Java端配置cfg.setLocalizedLookup(true); cfg.setEncoding(Locale.CHINA, UTF-8); cfg.setEncoding(Locale.US, UTF-8);模板中使用#import /spring.ftl as spring spring.message welcome/ #-- 带参数的消息 -- spring.messageArgs product.price, [product.price]/8. 项目实战电商系统模板设计8.1 商品详情页架构目录结构templates/ ├── layout/ │ ├── base.ftl │ ├── header.ftl │ └── footer.ftl ├── macro/ │ ├── product.ftl │ └── review.ftl └── product/ ├── detail.ftl └── list.ftl商品详情页核心逻辑#import ../layout/base.ftl as layout #import ../macro/product.ftl as p layout.base #block title${product.name} - 商品详情/#block #block content div classproduct-detail p.productGallery imagesproduct.images / div classinfo h1${product.name}/h1 div classprice span classcurrent¥${product.price?string(0.00)}/span #if product.originalPrice?? del¥${product.originalPrice?string(0.00)}/del /#if /div p.skuSelector skusproduct.skus / div classactions button idaddToCart加入购物车/button /div /div p.productTabs descriptionproduct.description attributesproduct.attributes reviewsproduct.reviews / /div /#block /layout.base8.2 订单打印模板PDF生成方案public byte[] generateOrderPdf(Order order) throws Exception { // 生成HTML String html generateOrderHtml(order); // 使用Flying Saucer转换为PDF ITextRenderer renderer new ITextRenderer(); renderer.setDocumentFromString(html); renderer.layout(); ByteArrayOutputStream outputStream new ByteArrayOutputStream(); renderer.createPDF(outputStream); renderer.finishPDF(); return outputStream.toByteArray(); }订单模板关键设计style .order-header { margin-bottom: 20px; } .table { width: 100%; border-collapse: collapse; } .table th, .table td { border: 1px solid #ddd; padding: 8px; } .text-right { text-align: right; } /style div classorder-header h2订单号${order.id}/h2 p日期${order.createTime?string(yyyy-MM-dd HH:mm)}/p /div table classtable thead tr th商品/th th单价/th th数量/th th小计/th /tr /thead tbody #list order.items as item tr td${item.productName}/td td¥${item.unitPrice?string(0.00)}/td td${item.quantity}/td td¥${item.totalPrice?string(0.00)}/td /tr /#list /tbody tfoot tr td colspan3 classtext-right总计/td td¥${order.totalAmount?string(0.00)}/td /tr /tfoot /table9. 调试与测试方案9.1 单元测试模板使用JUnit测试模板渲染public class TemplateTest { private Configuration cfg; Before public void setup() throws IOException { cfg new Configuration(Configuration.VERSION_2_3_31); cfg.setDirectoryForTemplateLoading(new File(src/main/resources/templates)); cfg.setDefaultEncoding(UTF-8); } Test public void testProductTemplate() throws Exception { MapString, Object model new HashMap(); model.put(product, new Product(iPhone 13, 5999.00)); Template template cfg.getTemplate(product.ftl); StringWriter writer new StringWriter(); template.process(model, writer); String result writer.toString(); assertTrue(result.contains(iPhone 13)); assertTrue(result.contains(5999.00)); } }9.2 日志调试技巧启用调试日志logback.xml配置示例logger namefreemarker levelDEBUG/关键日志信息解读TemplateLoader模板加载路径Creating cache for template模板缓存记录Starting to process template模板处理开始Rendered template渲染完成统计9.3 在线测试工具开发阶段快速测试方案RestController public class TemplateTestController { PostMapping(/api/template/test) public String testTemplate(RequestBody TemplateTestRequest request) { try { Configuration cfg new Configuration(Configuration.VERSION_2_3_31); cfg.setTemplateLoader(new StringTemplateLoader()); ((StringTemplateLoader)cfg.getTemplateLoader()) .putTemplate(test, request.getTemplate()); Template template cfg.getTemplate(test); StringWriter writer new StringWriter(); template.process(request.getModel(), writer); return writer.toString(); } catch (Exception e) { return Error: e.getMessage(); } } }10. 扩展与集成方案10.1 与Spring Boot深度集成自动配置扩展Configuration AutoConfigureAfter(FreeMarkerAutoConfiguration.class) public class FreemarkerExtConfig { Autowired private freemarker.template.Configuration configuration; PostConstruct public void init() throws TemplateModelException { // 添加共享变量 configuration.setSharedVariable(appVersion, 1.0.0); // 注册静态工具类 configuration.setSharedVariable(StringUtils, new StaticModels().getTemplateModel(StringUtils.class)); } }10.2 分布式环境方案Redis模板缓存实现public class RedisTemplateCache implements CacheStorage { private final RedisTemplateString, Object redisTemplate; public RedisTemplateCache(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } Override public void put(String key, Object value) { redisTemplate.opsForValue().set(freemarker: key, value); } Override public Object get(String key) { return redisTemplate.opsForValue().get(freemarker: key); } Override public void remove(String key) { redisTemplate.delete(freemarker: key); } Override public void clear() { SetString keys redisTemplate.keys(freemarker:*); if (keys ! null) { redisTemplate.delete(keys); } } }10.3 微服务架构下的模板管理模板服务设计方案Service public class TemplateService { Autowired private TemplateRepository templateRepository; private final ConcurrentMapString, Template templateCache new ConcurrentHashMap(); public String processTemplate(String templateName, MapString, Object model) throws Exception { Template template templateCache.computeIfAbsent(templateName, k - { String content templateRepository.getContent(templateName); return new Template(templateName, content, new Configuration(Configuration.VERSION_2_3_31)); }); StringWriter writer new StringWriter(); template.process(model, writer); return writer.toString(); } Scheduled(fixedRate 60000) public void refreshCache() { templateRepository.getModifiedTemplates().forEach(name - { templateCache.remove(name); }); } }在实际项目中使用FreeMarker多年后我发现最关键的实践是保持模板的简洁性。模板应该只关注展示逻辑所有业务计算都应该在Java端完成。对于复杂的展示逻辑应该通过自定义指令或宏来实现复用而不是在每个模板中重复编写相同的代码。另外良好的模板目录结构和命名规范能显著提高项目的可维护性。
返回列表