news 2026/10/24 1:35:10

Spring Boot 2 手动渲染 Thymeleaf:摆脱MVC,用模板引擎生成HTML

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 2 手动渲染 Thymeleaf:摆脱MVC,用模板引擎生成HTML

先说一个实际场景。很多人初学 Spring Boot 的时候,thymeleaf基本就是跟着一起用:写一个 Controller,返回字符串,模板丢在resources/templates下面,然后框架自动帮你找到对应的 HTML 文件完成渲染。但有一次我接手一个老项目,里面有一个定时任务模块,需要在每天凌晨给几百个用户发送日报邮件,邮件正文是一个相当复杂的HTML 表格,数据要动态生成。第一反应是不是用thymeleaf的邮件扩展?但项目里压根没引入专门模板,我只是想把模板内容当成一个字符串处理,然后拼装成 HTML 塞进MimeMessage里。这时“手动渲染”就派上用场了——不依赖 MVC 层的自动视图解析,直接拿到模板引擎实例,传入模板字符串和变量,手动调用渲染方法。这篇文章我不想讲 Spring MVC 那套自动视图解析,而是聚焦在SpringBoot2 手动渲染 thymeleaf这个相对冷门但极其实用的技术上,适合做定时任务、消息推送、邮件生成、或者任何需要把静态模板和动态数据分离的项目场景。

1. 为什么需要手动渲染而不是走自动视图解析

先说清楚一个概念:Spring Boot 默认的视图解析链路,核心是ThymeleafAutoConfiguration,它会自动配置好SpringTemplateEngine以及ThymeleafViewResolver。当你在 Controller 里面return "hello"的时候,框架走的是internalResourceViewResolver + ThymeleafViewResolver这一整套 MVC 流程。

这套流程的问题在哪里?它和 HTTP 请求、Servlet 容器绑定死。但现实中有些场景根本不需要一个完整的 HTTP Response 视图:

  • 定时任务中生成 HTML 邮件
  • 消息队列里消费数据后动态渲染页面片段
  • 报表模块需要把同一个模板渲染成多个版本并存储到文件系统
  • 单元测试里渲染模板字符串并校验生成的 HTML 内容

在这些场景下,你没法靠框架自动去解析视图名,因为压根没有请求进来。你需要的只是一个“参数进、字符串出”的渲染动作,这就是手动渲染。类比一下:自动视图解析是“你去饭店点菜,服务员帮你把菜端到桌面上”;手动渲染是“你直接进入厨房,跟厨师说‘把你做的这道菜打包给我’”。前者是完整的服务流程,后者只取了其中一部分能力。

手动渲染最核心的价值在于:解耦模板渲染与 Web 层。你的模板还是.html文件,你的变量还是Map或者一个对象,只是调用方从 MVC View 变成了你自己的业务代码。

2. 环境准备与依赖配置

要在 Spring Boot 2 中手动渲染 Thymeleaf,环境准备并不复杂。我基于SpringBoot 2.7.x版本演示,因为这是目前生产环境最常见的一个 2.x 系列版本,不过代码逻辑向下兼容。

2.1 Maven 依赖

首先你依然需要引入thymeleaf的 starter 依赖,因为即使手动渲染,你也需要一整套 Thymeleaf 的表层能力。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>

如果你仅仅只是为了邮件渲染,不想要连带出整个 Web MVC 的自动配置,也可以只引入thymeleaf-spring5(Spring Boot 2 对应 Spring 5):

<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> <version>3.0.15.RELEASE</version> </dependency>

这里有个小坑:spring-boot-starter-thymeleaf会自动引入spring-boot-starter-web相关的内容,如果你确认自己不需要完整 Web MVC,只是想在某个独立的服务中去渲染模板,那建议不要使用 starter,自己显式引入thymeleaf-spring5即可。不过绝大多数情况下,你的项目本来就是一个 Web 项目,直接引用 starter 并不冲突。

2.2 基础配置项

在手动渲染模式下,我们依然可以复用application.yml中 Thymeleaf 的配置:

spring: thymeleaf: prefix: classpath:/templates/ suffix: .html mode: HTML encoding: UTF-8 cache: false

需要注意:手动渲染的时候,模式的合法性验证仍然会加载。比如 HTML5 模式下,如果模板是无标签的纯文本片段,渲染就会报错。后面踩坑部分我会专门提到。

2.3 如何确认当前 Spring Boot 版本中的 Thymeleaf 版本

这一点值得一提。Spring Boot 2.x 的各个小版本对应 Thymeleaf 版本变化不小,比如 Boot 2.0 对应 Thymeleaf 3.0.9,Boot 2.7 对应 Thymeleaf 3.0.15。手动渲染时如果引用了thymeleaf的扩展工具类(比如#dates、#numbers的处理逻辑),不同 Thymeleaf 版本之间的语法基本兼容,但是在表达式工具对象上有少量新增方法,建议以 Spring Boot 官方 BOM(Bill of Materials)为准,不要自己手动指定 Thymeleaf 版本来“升级”,否则容易和 Spring 5 的集成层冲突。

3. 手动渲染的核心 API 与两种配置方式

手动渲染的本质就是三步:拿到模板解析器、构建模板引擎、调用 process 方法。但细节藏在配置方式里,这里我讲两种常用方式,各有各的适用场景。

3.1 方式一:直接使用 Thymeleaf 原生 API

如果你不需要与 Spring 的容器做过多的整合,只想要一个轻量的渲染工具,可以直接用 Thymeleaf 原生类。这个方式不需要任何 Spring 自动配置,完全是独立运行。

ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); resolver.setPrefix("templates/"); resolver.setSuffix(".html"); resolver.setTemplateMode(TemplateMode.HTML); resolver.setCharacterEncoding("UTF-8"); SpringTemplateEngine engine = new SpringTemplateEngine(); engine.setTemplateResolver(resolver); String template = "<p th:text=\"${name}\">默认名字</p>"; Context context = new Context(); context.setVariable("name", "程序员鱼皮"); String result = engine.process(template, context);

但这里我不建议直接用裸的TemplateEngine,而是用SpringTemplateEngine,因为这个类继承了SpringStandardDialect,它支持的表达式语法更丰富,比如能使用 SpringEL 的一些特性,以及在表达式里通过@beanName直接调用 Spring 容器中的 Bean。如果你只做简单的字符串替换,类名差异感知不明显,但是一旦涉及到 Spring 的MessageSource、ConversionService,区别就很大。

3.2 方式二:注入 Spring Boot 自动配置好的引擎

Spring Boot 在ThymeleafAutoConfiguration中已经默认帮你建好了一个SpringTemplateEngine实例,配置前缀是基于spring.thymeleaf这个命名空间。我们完全可以直接@Autowired它,然后自己指定 Template 的位置。问题在于:自动配置的引擎默认的解析器是SpringResourceTemplateResolver,它的前缀和后缀是绑定在配置文件里的。

@Service public class ThymeleafKit { @Autowired private SpringTemplateEngine templateEngine; public String render(String templatePath, Map<String, Object> variables) { Context context = new Context(); context.setVariables(variables); return templateEngine.process(templatePath, context); } }

实际使用验证下来,你会发现方式二有一个很舒服的点:你不需要自己创建和配置 resolver,也不需要关心字符编码,因为自动配置已经读取了application.yml里的spring.thymeleaf.encoding=UTF-8。但对应的也有人觉得不够爽的地方:默认解析器只能解析classpath:/templates/下的文件。如果你想把模板放在数据库或者别的自定义存储下,方式二就只能靠扩展实现了。

3.3 两种方式的选型建议

维度方式一(原生 API)方式二(自动注入)
依赖 Spring 容器不依赖,可独立运行必须依赖 Spring 容器
配置来源全部自己写代码配置沿用application.yml
模板存储位置自定义能力强(可改 resolver 改变加载源)默认定位classpath:/templates/,扩展需覆盖 Bean
Spring 集成(MessageSource、ConversionService)需要自己装配自动支持
适合场景独立工具类、非 Spring 环境、特殊存储常规 Web 项目,推荐优先使用

有一点需要特别提醒:就算使用方式二,你仍然需要创建org.thymeleaf.context.Context对象,千万不要用org.springframework.ui.Model直接传进去。这两个上下文结构完全不同,用错了会出现变量全部取不到的现象。原因很简单:SpringTemplateEngine.process(String template, IContext context)接收的是Thymeleaf 的 IContext接口实现,虽然SpringTemplateEngine内部也提供了process方法重载来接收Model对象,但这时变量的获取逻辑走的是另一条路径,建议别给自己挖坑,统一用Context处理。

4. 完整实操案例:把定时邮件模板改成手动渲染

一个很刚的实战场景来了。我上一家公司做的是 SaaS 后台,每周一早上要自动给运营发送上周数据汇总邮件。邮件正文是 HTML,里面包含一个数据表格,还要把每个模块的最新状态用不同颜色标出来。直接拼接字符串太痛苦,还容易漏转义,于是我用 Thymeleaf 模板重写整个流程。

4.1 项目目录结构

src/main/resources/ └── templates/ └── mail/ └── weekly-report.html src/main/java/com/example/mail/ ├── MailRenderService.java ├── WeeklyReportJob.java └── model/ └── ReportData.java

4.2 邮件模板

weekly-report.html内容大致是这样的:

<!DOCTYPE html> <html lang="zh-CN" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>每周数据汇总</title> </head> <body> <h2 th:text="${title}">默认标题</h2> <p>生成时间:<span th:text="${#dates.format(now, 'yyyy-MM-dd HH:mm:ss')}">默认时间</span></p> <table border="1"> <thead> <tr> <th>业务模块</th> <th>本周新增用户</th> <th>上周新增用户</th> <th>环比增长</th> </tr> </thead> <tbody> <tr th:each="item : ${reportList}" th:style="${item.growth >= 0} ? 'background-color: #e1f3d8' : 'background-color: #ffdcdc'"> <td th:text="${item.moduleName}">模块名</td> <td th:text="${item.currentCount}">0</td> <td th:text="${item.lastCount}">0</td> <td th:text="${#numbers.formatPercent(item.growth / 100.0, 1)}">0%</td> </tr> </tbody> </table> <p>详细数据见附件。</p> </body> </html>

这个模板里我用了:

  • ${title}:普通变量
  • ${#dates.format(now, 'yyyy-MM-dd HH:mm:ss')}:日期格式化工具对象
  • ${#numbers.formatPercent(item.growth / 100.0, 1)}:百分比格式化工具对象
  • th:each:循环遍历列表
  • th:style:根据条件动态拼接样式

以上全部是 Thymeleaf 标准方言的能力,在手动渲染模式下语法和自动视图解析模式完全一致,不存在什么“手动模式只能做简单替换”的说法。

4.3 Java 渲染服务

@Service public class MailRenderService { private final SpringTemplateEngine templateEngine; public MailRenderService(SpringTemplateEngine templateEngine) { this.templateEngine = templateEngine; } public String renderWeeklyReport(String title, List<ReportData> reportList) { Context context = new Context(); context.setVariable("title", title); context.setVariable("now", new Date()); context.setVariable("reportList", reportList); return templateEngine.process("mail/weekly-report", context); } }

注意这里templateEngine.process("mail/weekly-report", context)中的第一个参数,自动配置下的SpringResourceTemplateResolver会自动拼接前缀classpath:/templates/和后缀.html,所以传入字符串不要带完整路径和后缀,否则会拼出一个找不到的文件路径。我见过很多人在这一步踩坑:以为手动渲染就一定要写“templates/mail/weekly-report.html”,结果系统提示找不到模板。为什么?因为Resolver对if (templateName.startsWith("/"))这类情况也有特殊处理,最好就传干净的模板路径。

4.4 定时任务使用

@Component public class WeeklyReportJob { private final MailRenderService mailRenderService; private final MailSender mailSender; public WeeklyReportJob(MailRenderService mailRenderService, MailSender mailSender) { this.mailRenderService = mailRenderService; this.mailSender = mailSender; } @Scheduled(cron = "0 0 9 * * MON") public void sendReport() { List<ReportData> dataList = loadReportData(); String htmlContent = mailRenderService.renderWeeklyReport("上周业务数据", dataList); // 正常发送邮件,htmlContent 直接作为邮件正文 mailSender.sendHtml("ops@example.com", "每周数据汇总", htmlContent); } private List<ReportData> loadReportData() { List<ReportData> list = new ArrayList<>(); list.add(new ReportData("用户增长", 1200, 1000, 0.2)); list.add(new ReportData("付费转化", 300, 350, -0.14)); list.add(new ReportData("内容创作", 4567, 4321, 0.05)); return list; } }

跑出一个真实的邮件效果,HTML 会被邮件客户端正确解析成表格背景色交替的样式。这个案例最直接的收益是:再改邮件样式的时候,我不需要去改 Java 代码,改.html模板文件然后重新部署即可。

提示:如果是发送 HTML 邮件,必须保证<html>根标签存在,并且xmlns:th需要声明。有些邮件客户端兼容性不佳,建议表结构保持简单,内部样式优于外部样式表,Thymeleaf 内联样式可以预判。

5. 手动渲染中的变量注入与表达式工具补充

前面案例里用到的Context是一个轻量级的变量容器。我自己的经验里,很多从 JSP 转过来的人容易把 Thymeleaf 的 Context 和 Servlet 的 request/session 去做类比,但其实它只是一个Map<String, Object>的包装器。在你往里面塞变量的时候,有几个容易忽略的细节值钱,这里集中讲。

5.1Context与WebContext的取舍

手动渲染中我优先推荐使用org.thymeleaf.context.Context,而不是 Web 环境下的WebContext。WebContext是 Thymeleaf 为了解决 Web 请求中可以直接获取HttpServletRequest属性、HttpSession属性以及application属性的场景而存在的。但它和 Servlet API 强耦合,如果只是生成邮件,用完就丢,没有必要引入这么重的上下文。

同时注意:在 Spring Boot 2 的 Thymeleaf 3.x 中,WebContext已经标记为 deprecated,新的使用方式是WebSession+WebExchange的组合概念,但对于手动渲染非 Web 场景,直接忽略即可。

5.2 变量名不能使用复合表达式

下面这种写法是错误的:

context.setVariable("name.list", "foo");

变量名包含.会让模板解析器误认为是对象属性访问,你在模板里写${name.list}的时候,它尝试解析name这个变量然后访问其list属性,而不是直接取名为name.list的变量。这种隐蔽问题排查起来特别费时,所以建议变量名统一使用驼峰命名,不要使用任何特殊符号。

5.3 充分使用工具对象

Thymeleaf 自带一批工具对象,手动渲染模式下同样可用:

工具对象说明示例
#dates日期格式化${#dates.format(now, 'yyyy-MM-dd')}
#calendars日历对象格式化${#calendars.monthName(...)}
#numbers数字格式化${#numbers.formatInteger(num, 3)}
#strings字符串操作${#strings.toUpperCase(str)}
#lists列表操作${#lists.size(list)}
#maps集合操作${#maps.containsKey(map, 'key')}
#sets集合操作${#sets.contains(set, item)}
#bools布尔运算${#bools.isTrue(bool)}

熟悉这些工具对象的最大好处是模板里尽量少写th:if判断冗余逻辑。比如要拼接人员姓名中的省略号,直接用${#strings.abbreviate(name, 14)}比在 Java 里处理完再传变量更灵活。

5.4 内联表达式与手动渲染的配合

有些特殊业务中,模板变量是动态生成的页面组件数据,我会直接在模板中使用内联表达式:

<p>[[${user.name}]]</p>

这个写法在手动渲染中完全可用,因为最终process就是把文本渲染为纯字符串。但要注意一个细节:[[...]]内联表达式默认会做 HTML 转义。如果你存的是富文本 HTML 并且信任其来源,需要写成[(${content})]这个不支持转义的语法。在邮件模板中,正文内容来自后台编辑器,一般不信任用户输入,我用[[${content}]]防止脚本注入到邮件里。

6. 实测中的意外坑:字符编码、模板片段与缓存问题

手动渲染这条路看着平坦,真正趟进去会有不少暗坑。我把真实场景里踩过的几个典型问题整理出来,这些在普通博客里很少会专门提到。

6.1 中文乱码,明明配置了 UTF-8 却仍然乱码

你可能会觉得手动渲染是纯 Java 代码调用,不像 Servlet 有写在代码前面的request.setCharacterEncoding("UTF-8")的问题。实测下来,发生乱码通常有三个层次的问题:

第一层:TemplateResolver的setCharacterEncoding没有设置。如果你是方式一(原生 API)自己创建了一个ClassLoaderTemplateResolver,必须显式设置:resolver.setCharacterEncoding("UTF-8"),否则默认使用系统编码,Windows 下大概率是 GBK,中文模板乱码没商量。

第二层:模板文件本身的编码就不是 UTF-8。IDE 默认的编码可能跟随系统地区设置,如果你是 Windows 中文环境,默认GBK,因此即使 Java 层设置了 UTF-8,文件本身就乱了,渲染出来自然也是乱码。检查文件中文字符是否显示正常,必要时使用“File Encoding”统一改成 UTF-8 保存。

第三层:输出端编码错误。如果你把渲染后的字符串直接写入文件或数据库,也可能出现乱码。例如要把字符串String html = templateEngine.process(...)写入文件,必须指定写入文件的字符集:

Files.write(Paths.get("D:/report.html"), html.getBytes(StandardCharsets.UTF_8));

很多人忘记了文件写入这一步单独指定,导致页面上看代码没问题、存储到文件后打开却是乱码。

6.2 模板文件的th:inline="javascript"与手动渲染的边界

有时候我们会使用 Thymeleaf 的 JavaScript 内联能力:

<script th:inline="javascript"> var user = [[${user}]]; </script>

这种语法在自动视图解析中工作良好,但在手动渲染时有个隐藏要求:你必须在 Context 中传入一个合法的 Java 对象,而且 Thymeleaf 会调用它的 toString 方法生成 JSON 风格输出。如果你传过去的对象没有 getter 方法,可能会被渲染成空对象{},别问我为什么知道。解决方案是传一个简单 POJO,或者手动把对象转换成 JSON 字符串后再传入。

6.3 缓存问题:为什么改了模板却没生效

Spring Boot 2 中spring.thymeleaf.cache默认是true,生产环境这样没问题,但是你在本地调试手动渲染代码时,改了模板刷新却看不到效果,会相当难受。解决方案有两层:

本地环境把cache设成false:

spring: thymeleaf: cache: false

但如果模板文件是从自定义存储加载的,那还要检查你的 resolver 是否开启缓存。方式一原生 API 创建的 resolver,默认也是启用缓存(setCacheable(true)),而手动渲染工具类往往是一个长生命周期组件,你如果不想每次重新加载模板,也可以主动缓存;但如果你还在开发阶段,记得把 cache 关掉,否则你会陷入“代码改了、日志打了、渲染结果没变化”的常见死循环。

6.4 Fragment 片段渲染

手动渲染还能处理一个特别细节的需求——只渲染模板的一部分。利用 Thymeleaf 的 fragment 能力,你可以把邮件模板拆分成头部、主体、尾部三个代码片段,在生成不同邮件时复用。

模板文件email-layout.html:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="header(title)"> <h3 th:text="${title}">默认标题</h3> </div> <div th:fragment="footer(company)"> <p th:text="'Copyright ' + ${company} + ' 保留所有权利'">版权声明</p> </div> </body> </html>

渲染时指定 fragment:

String footer = templateEngine.process("mail/email-layout :: footer('某公司')", context);

这里::是标记片段选择符。我被这个功能惊艳过:处理不同客户邮件时,可以只渲染同一个模板的某个局部模块,拼接出最终内容,而不用写一堆if/else逻辑。

提示:如果你用SpringTemplateEngine渲染一个 fragment 字符串,而这个 fragment 的独立内容不是一个完整 html 文档,TemplateMode.HTML也能正常解析,这是它的容错性优势。但如果你用.txt模板则必须设置TemplateMode.TEXT,否则渲染结果可能会包含莫名其妙的多余标签。

6.5 表达式中的 Bean 调用

这是SpringTemplateEngine比原生TemplateEngine更适合项目集成的核心原因。在手动渲染时我可以直接在模板里调用 Spring 管理的 Bean 方法,比如在模板中拼接当前用户的展示名:

<p th:text="${@userService.getDisplayName(userId)}">调用Bean方法</p>

使用方式就是在表达式里写@beanName然后跟上方法调用。这一点在纯原生TemplateEngine中是不支持的,除非额外添加SpringStandardDialect。非常适合在做报表、发通知时,直接在模板中调用充血模型方法,减少在 Java 代码里预先处理一堆变量的繁琐。

7. 封装一个可靠的渲染工具类

最后,实战中用得多还是封装一个独立的渲染工具类,把上面讨论的几种方式融合起来。我这里提供一个生产项目可用的简易版本,同时兼顾了自定义模板存储和缓存控制。

其实我不太喜欢用静态方法,因为SpringTemplateEngine从使用逻辑上是一个重量级组件,初始化成本不小,但它在 Spring 容器中默认是单例的,所以直接注入使用最优雅。下面是我经常用的一个封装:

@Component public class ThymeleafRenderUtils { private final SpringTemplateEngine templateEngine; public ThymeleafRenderUtils(SpringTemplateEngine templateEngine) { this.templateEngine = templateEngine; } /** * 渲染指定路径模板 * * @param templatePath 模板路径,相对于 classpath:/templates/ * @param variables 变量集合 */ public String render(String templatePath, Map<String, Object> variables) { Context context = new Context(); context.setVariables(variables); return templateEngine.process(templatePath, context); } /** * 渲染指定模板片段 */ public String renderFragment(String templatePath, String fragmentSelector, Map<String, Object> variables) { Context context = new Context(); context.setVariables(variables); return templateEngine.process(templatePath + " :: " + fragmentSelector, context); } }

这个工具类本身不复杂,但要注意的一点是:如果你继承它并且自己扩展了renderFragment,模板路径拼上:: selector时,必须确保fragmentSelector内没有空格引起歧义,同时 fragment 表达式中如果包含参数,建议用单引号包住字符串字面量。

基于我的个人经验,贴一段使用方式:

@RestController public class DemoController { private final ThymeleafRenderUtils renderUtils; public DemoController(ThymeleafRenderUtils renderUtils) { this.renderUtils = renderUtils; } @GetMapping("/api/demo/render") public String demo() { Map<String, Object> vars = new HashMap<>(); vars.put("message", "手动渲染成功"); return renderUtils.render("demo/render-demo", vars); } }

这里返回的是 String 类型,配合@ResponseBody就是纯文本输出,不会经过视图解析器二次处理。如果你不小心返回了一个模板路径,框架还可能会去解析视图,产生预期之外的异常。

8. 手动渲染的性能优化与命名规范建议

既然手动渲染常用于高频率的邮件、通知推送,那性能不可回避。分享一些实测过的优化思路。

8.1 模板缓存不必全关

虽说开发阶段建议关闭缓存方便调试,但一旦进入生产,建议将spring.thymeleaf.cache设为true。Thymeleaf 的模板缓存是解析之后的 AST(抽象语法树)缓存,重复渲染同一个模板文件时速度能提升好几个数量级。如果模板数量不大,缓存命中率几乎是 100%,内存开销也有限。还有一种折中方案:自定义 resolver 时设置setCacheTTLMs()来控制缓存清理周期,兼顾模板更新与性能。

8.2 变量对象的 getter 效率

Thymeleaf 通过反射访问 POJO 的 getter 方法来获取属性值。如果我们传入的是某些远程服务代理对象,每取一次属性都有可能触发远程调用,渲染时间会让人崩溃。在邮件报表场景中,建议先组装好一个独立的 DTO 对象,属性全部是内存取值,而不是直接传入 ORM 实体或远程代理对象,否则你会在性能日志中看到process方法耗时动辄好几秒。

8.3 命名规范上的个人建议

  • 模板文件一律小写,使用中划线分隔,如weekly-report.html,不要混用驼峰和中文命名。
  • 变量命名采用驼峰。
  • Controller 中尽量不要直接调用templateEngine.process,而是封装成 Service 层能力,分离关注点。
  • Context创建尽量局部化,避免复用同一个Context实例在多个线程中使用,因为它是非线程安全的。正确做法是每次渲染都new Context(),成本很低。

最后再分享一个心得:手动渲染 Thymeleaf 的能力比想象中还要大。不仅邮件,我用它做过导出 HTML 格式的月度报表转移到另一个系统展示,也用它在单元测试中对比渲染结果快照。它和自动视图解析并不冲突,两者更像是“一个管请求视图、一个管内容生产”。在 Spring Boot 2 项目中,只要你的业务有“根据模板 + 数据生成 HTML 字符串”的需求,无论这个需求发生在定时任务、MQ 消费者、还是某个服务层的工具方法中,手动渲染都是直接且可靠的答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 1:34:38

SECS/GEM事件机制详解:从S6F11订阅到EAP工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:32:44

ExtractorSharp实操指南:5分钟玩转NPK文件编辑与游戏资源修改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:32:43

TS视频格式深度解析:从流式传输原理到FFmpeg转封装实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:32:38

PCA9422 PMIC与PIC32MX664F064L电源管理方案设计与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:32:26

Clonezilla Live 2.7.0实战指南:UEFI/BitLocker/LVM全场景裸机克隆

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:31:57

商业银行IT系统全景解析:业务术语、系统边界与日终流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华