生产环境不解析模板文件,不解释属性表达式,也不靠反射去找模型访问器。这套 Thymeleaf、Spring MVC 加 htmx 的组合,走的是另一条执行路径:在原生 Java 后端里,用静态编译出来的渲染代码干活。
模板编译器处理 Thymeleaf 语法的一个受支持子集,产出能直接访问模型的 Java 渲染器。Spring MVC 风格的控制器决定返回哪个页面或片段,ParparVM 把这些渲染器和后端其余部分一起编译成原生代码。返回给浏览器的,仍然是普通 HTML。
性能账算得很具体:一个模型属性变成 Java 的 getter 或字段访问,一个循环变成生成的控制流。原生编译器看到的这段代码,和控制器、服务放在一起。
配合 htmx,服务端只返回页面上发生变化的那一部分。静态渲染和更小的响应各自解决不同的成本,两者叠加使用。数据库操作、网络延迟和页面本身,依然决定端到端的响应时间。
JSP 留下的那点有用东西
JSP 曾把页面编译成 Java servlet 代码,Tomcat 的 Jasper 文档里有描述。原生编译并不在 JSP 的契约里。真正有用的想法是:模板可以变成运行时已经知道怎么执行的代码。
旧的工作流适合一台能 telnet 进去、就地改文件的长跑服务器。开发时也想改得快,但到了生产,要的是一个构建产物,它和旁边的 Java 代码经过同一套检查。
Thymeleaf 本身支持模板缓存,所以说每次请求都在解析一个新文件并不准确。这里的选择更进一步:请求路径上根本没有模板解释器。改一个模板,意味着重新构建应用。
控制器还是控制器的样子
可运行的示例目录带一个内存库存。它的列表方法很短:
- @GetMapping("/products")映射路径;
- 方法里先调用 populate 把目录复制进模型;
- 普通请求返回整页,来自 htmx 的请求返回名为 catalog 的片段。
这个返回值指向一个已编译的视图,不会打开请求方提供的路径。列表方法加了 synchronized,因为这个小例子把可变数据放在内存里,重启就会重置目录。持久化的应用会交给服务和数据库处理。
模板里声明了 products 的 Java 类型。编译器解析对应的 Java getter 或 public 字段,生成直接访问。改了属性名却没更新模板,构建会失败,并给出文件名和源码位置。这种失败发生在部署之前,是有用的。
动态文本会被转义。URL 表达式在 HTML 属性转义之前先对参数编码。原始 HTML 插入和任意方法调用,不在第一版表达式语言的范围里。
不建第二个前端也能更新区块
目录里的“Add product”链接按普通导航工作,同时带上 htmx 增强。服务端可以返回编辑器片段,替换掉工作区。示例里包含一份本地固定的 htmx 脚本、共享的布局片段,以及禁用 JavaScript 时也能用的表单。
表单绑定用专门的 DTO。BindingResult 要紧跟在该参数之后,校验失败时返回编辑器。返回带 200 状态码的表单错误,htmx 就能正常显示它们。不填名字直接提交,返回的编辑器片段会在字段旁带上错误提示。
现有的安全链负责跨站请求伪造(CSRF)防护。生成的表单和会修改数据的 htmx 控件都带上可用的令牌。声明一个 HTML 控制器,并不会自动创建这套安全配置,示例里的 WebSecurity 展示了怎么配。
语言变小是一笔交易
这是 Thymeleaf 风格的子集,不是 Thymeleaf 库。没有 Spring 表达式语言,没有自定义方言支持,没有任意方法调用,也没有运行时模板编辑。表单 DTO 支持标量值,不绑定整张 ORM 实体图。
这些限制换来的是带类型的属性访问,以及一个作为普通编译产物随应用发布的渲染器。代价是移植现有模板可能要花功夫。稳妥的起点是一个小的管理页面和它的表单错误,而不是一上来就啃大站点。
在 scripts/backend-mvc 下,装好当前检出的后端和插件,执行 mvn package,再跑 codenameone-maven-plugin 的 backend 目标,打开 localhost:8080/products 即可。示例的 README 里有原生打包命令,以及覆盖普通表单、htmx、转义和 CSRF 拒绝的冒烟测试。实现由 PR #5974 引入。
开发循环要短,生产二进制要在第一个用户到来之前就知道怎么渲染每一个页面。
热门跟贴