2013 年,我在 BluLogix 写全栈 Java,用 Google Web Toolkit 编译到 JavaScript,接上 Spring Security 做 auth,为电信运营商交付一个云计费平台。TypeScript 再过一年才会发布 1.0。React 还是 Facebook 内部的东西。Node.js 是人们写博客文章的东西,不是人们在上面交付计费系统的东西。

那时我不知道,我正在比那成为默认值早了整整十年地做类型安全的全栈开发。

GWT 到底是什么

Google Web Toolkit(GWT)是个大胆的想法:用 Java 写你的前端,跑 GWT 编译器,另一侧出来优化过的 JavaScript。不是薄薄的封装层。不是某种 JSX 类比物。Java——类、泛型、接口、完整的类型系统——通过交叉编译变成在浏览器里跑的 JavaScript。

这能工作,是因为 Google 需要它工作。Gmail 是在 GWT 上构建的。Google Docs 是在 GWT 上构建的。Google 在编译器上投入了严肃的工程——死代码消除、代码分割、针对不同浏览器目标的排列优化——而且产出了出乎意料地高效的输出。生成的 JS 针对每个浏览器做了压缩和排列,所以 IE 拿到一个构件,Firefox 拿到另一个,Chrome 拿到另一个。你不需要关心这些。编译器处理了。

对我的团队来说价值主张是直接的:保护你后端的同一套类型系统在保护你的前端。 如果 API 返回一个 BillingCycle 对象,UI 代码收到一个 BillingCycle 对象,所有字段都有类型,编译器在构建时捕获每个不匹配。没有 JSON 形状不一致。没有”我以为那个字段是 String”。没有因为有人重命名了后端 DTO 里的属性却忘了更新前端 fetch 调用而在运行时发现的惊喜。

2013 年,这是真正不寻常的。其他所有人都在写 jQuery 回调、抱佛脚。

你为静态类型付出的代价

GWT 不是免费的。编译器很慢。不是”多了几秒钟”那种慢——在一个中等大小的 GWT 应用上,编译周期要跑两到五分钟。增量 dev 模式(你在托管浏览器里跑应用、热替换 Java 字节码)有帮助,但不稳定:dev 模式和生产编译有时会以微妙的方式出现分歧,你发布了一个在 dev 里完美运行、但在 dev 浏览器从未测试过的某个排列版本里出问题的东西。

widget 模型是 Java 风格的对象层次结构:GwtWidgets 继承自抽象基类、事件处理器接口、映射到 Java 类的 UiBinder XML 模板。这是 2013 年 Java 企业文化应用在 UI 上。如果你来自 Swing 或 JSF 会觉得很自然。来自任何其他地方都会觉得很陌生。

CSS 是一场真实的战斗。GWT 的 CssResource 系统给你类型安全的 CSS 类名——你的 Java 代码会引用 myStyle.myClassName() 而不是原始字符串,所以重命名 CSS 类会产生编译错误而不是静默失效。这理论上真的很好。实践中,围绕它的工具粗糙到我花了大量时间调试为什么一个样式没有生效,结果发现我引用了错误的 CssResource 类型。

Spring Boot 出现的时候感觉是一份礼物

我大约从 2012 年开始用 Spring Framework——Spring MVC 做 REST 端点,Spring Security 做 auth,Spring WS 做 SOAP 服务(是的,SOAP,这是面向电信的企业 Java)。这是 XML 配置 Spring 的时代:applicationContext.xml 文件有 500 行 bean 定义、命名空间声明和属性占位符。你和你的 application context 有很深的私人关系。你叫得出每个 bean 的名字。

Spring Boot 1.0 在 2014 年 4 月发布。那时我已经在 AFAQ 了,我们很早就采用了——早到还没有 400 个 Stack Overflow 答案可以参考每个遇到的配置问题。

Boot 做出的转变不是增加功能。而是消除仪式感。自动配置意味着框架从你的 classpath 猜测合理的默认值并连接你实际需要的东西。@SpringBootApplication 替换了 XML 迷宫。内嵌 Tomcat 意味着你跑 java -jar 而不是管理容器 deploy。application.properties 替换了三分之一的 XML。

我第一次从零跑起一个 Spring Boot 应用,在 10 分钟内有了一个可用的 REST 端点——不算读文档的时间——我就明白有什么东西变了。技术上不是革命性的。哲学上截然不同。

在 AFAQ 用 GWT + Spring Boot 构建 EHR

在 AFAQ(2014-2017),我在构建一个 EHR/EMR 套件——基于 Web、与 ERP 模块完全集成、部署在 VA VistA 的组件之上。技术栈:前端 GWT,后端 Spring Boot + Java EE,VistA MUMPS 组件在一个服务层之后(那一侧有另一篇完整的文章)。

GWT 和 Spring Boot 的具体组合在医疗场景下实际上相当好。原因是:医疗 UI 是复杂的、有状态的、高风险的。 患者病历视图有嵌套的标签面板、实时化验结果 feed、会话期间更新的用药列表、富文本的问诊笔记,以及在某些东西变化时必须触发的提示徽章。GWT 的 Java UI 模型比 jQuery 面条代码更好地处理了这些。当数据是 AllergyRecordMedicationOrder 对象而不是任意 JSON 时,类型安全是有意义的。

Spring Boot 的有主见的默认值在 AFAQ 也有帮助,因为团队不大,我们需要快速推进。约定优于配置让我们能写业务逻辑,而不是争论 bean 生命周期。

痛点在构建上。前端完整的 GWT 编译,加上后端 Spring Boot 的 Maven 构建,在 2014 年的硬件上,是一个从干净检出到可部署构件需要 15 分钟的过程。我们必须对不破坏构建保持严格的纪律,因为”我合并了一个错误的东西”到”我知道它坏了”的反馈循环至少是一刻钟。

GWT 为什么消失了,什么取代了它

GWT 在 2012-2013 年达到顶峰,然后 JavaScript 快速变好了。TypeScript 出现,给 JavaScript 世界带来了 GWT 一直在卖的静态类型。React 给它带来了组件模型。Webpack 给它带来了代码分割。构建工具显著改进。关键的是:每个 Web 工程师都已经会 JavaScript 了。能写 GWT Java 前端代码的人是”Java 工程师”和”愿意写前端的人”的交集——一个出了名小的韦恩图。

Google 逐渐减少了对 GWT 的投入。社区接管了它。GWT 的新开发现在是利基中的利基:还有医疗计费系统在跑它,从没迁移过的企业 Java 公司,以及少数开源项目。

Spring Boot 则走向了相反的方向。它吃掉了 Java 后端世界。如果你 2024 年在写一个新的 Java 服务,你几乎肯定在用 Spring Boot。它成了一个很少有框架能做到的那种默认选择。

讽刺的是,技术栈里不那么新颖的那部分(Java 后端自 1997 年就存在了)反而是持久的那个。真正领先时代的那部分——类型安全的编译式前端 Java——输给了它所竞争的生态系统,等那个生态系统成熟之后。

我想对碰上 GWT 怀旧墙的人说

如果你曾经在 GWT 里工作,现在对它有点怀念:你实际怀念的是一致类型系统跨栈的感觉。那种感觉现在可以通过 TypeScript + tRPC,或者完整 Java 后端加生成的 TypeScript 客户端,或者如果你感觉勇敢的话 Kotlin + Compose Multiplatform 来获得。工具链好太多了。

但你没有错:GWT 确实第一个解决了那个真实的问题。它用了大量 Java、一个非常慢的编译器和一个看起来像 Swing 的 widget 模型解决了它。2013 年那是正确的权衡。2024 年你有更好的选择——而那些更好的选择之所以存在,大部分是因为 GWT 存在在先,证明了价值,付出了实验税,让所有人可以在此基础上构建更干净的东西。

我偶尔还会想到 GWT 的 UIBinder XML 模板。不是带着好感。但带着一种特定的敬意——留给那些真的很难、而且我还是把它发布到生产的东西的那种。

Spring Boot 我每天都在想。它就在我现在的技术栈里。