如何在 2016 年不讨厌 Spring

工程 | Phil Webb | 2015 年 11 月 29 日 | ...

在感恩节周末,一篇名为 Sam Atkinson 的文章《我为何讨厌 Spring》 在 Twitter 上流传开来。倾听批评意见总是很有趣,可以看看我们能做些什么来改进 Spring,Spring Boot 的很大一部分就是听取了 人们谈论他们在框架中遇到的问题 后诞生的。

在这篇博文中,我将尝试回应 Sam 文章中讨论的一些担忧,并描述我个人的 Spring “最佳实践”。不过,在深入探讨细节之前,值得思考一下为什么框架中仍然存在一些非最优的做事方式。

向后兼容性的诅咒

在 Spring 开发中最令人头疼的方面之一(除了有人写文章说他们有多讨厌它)是,特性往往有很长的生命周期。Spring 对向后兼容性有着坚定的承诺,这意味着一旦添加了某个特性,就很难从根本上改变它。开发框架带来了一般应用程序开发者不会遇到的独特挑战。YAGNI _(You ain't gonna need it,你不会需要它)_ 和激进重构等敏捷技术更难应用。我们遵循 SMNI _(Someone might need it,某人可能需要它)_ 原则,并谨慎进行重构。在 Java 中还没有 lambda、泛型甚至注解时就存在的特性仍然受到支持。现在很容易说 XML 不是连接 bean 的好方法,但很难说应该完全删除它。我个人很看重我能够轻松升级旧的 Spring 应用程序并逐步迁移到新技术这一事实。

同样要记住,就像商业软件一样,开源软件也资源有限,面临时间压力。有时作为开发者,我们也会犯错误,最终需要与它们共存一段时间。

过大,难以失败?

Spring 本质上是一个集成框架。它为许多不同的技术提供了一致的编程模型。公平地说,Spring 的确可能偏大,但如果不...集成许多技术,根本不可能提供与许多技术的集成。虽然它看起来令人不知所措,但一旦你理解了核心概念,就很容易只挑选你需要的那部分 Spring。如果你有一些特定的目标需要实现,spring.io 上的指南 可以作为有用的起点。Spring Boot starter 列表 或使用 start.spring.io 也是将 Spring 限制在特定领域的好方法。

依赖管理

典型的基于 Spring 的应用程序会依赖许多第三方库。过去,很难确切知道各种库的哪些版本能够良好协作。如果你今天开始一个新项目,我强烈建议使用 Spring Boot 的父 POMSpring Platform。它们都提供了一组精心策划的依赖,这些依赖已知能够良好协作。

我们甚至开发了一个 Gradle 插件,让你在使用 POM 时即使没有 Maven 父依赖特性的优势也能使用托管依赖 部分。

如果你由于某种原因无法使用托管依赖,你仍然可以参考 Spring Boot 文档中 已知能够良好协作的兼容版本列表

注入

在你的 bean 中总是使用 基于构造函数的依赖注入。对于强制依赖,总是使用 断言。关于为什么基于字段的注入是邪恶的更多背景知识,你可以阅读 Oliver Gierke 的这篇文章

像往常一样,这条规则有一个例外,当你 使用 SpringJUnit4ClassRunner 进行测试时,可以使用基于字段的注入。

泄漏的 Spring

我见过一些人认为应用程序代码绝不应该依赖于框架。也就是说,除了配置代码之外,任何地方出现 org.springframework 导入都是不好的。虽然你确实可以做到这一点(例如对 @Inject 等标准注解提供了广泛支持),但我倾向于不那么严格。我更喜欢在这里应用二八原则。我认为依赖于框架类和注解是可以的,但如果我要脱离 Spring,我不希望重写超过 20% 的代码。

我真正想要的是易于测试的应用程序代码。我应该能够手动创建我的业务服务实例并进行测试,而无需完整的 ApplicationContext

配置

我个人喜欢保持我的 Spring 配置相当轻量。我喜欢使用组件扫描来查找应用程序 bean,并且喜欢将 @Configuration 用于那些偏离 Spring Boot 默认自动配置的框架特定配置。如今我完全不倾向于使用 XML 配置。

有些人认为为所有定义的 bean 进行显式配置更好,但在实践中我倾向于发现这是一种阻碍。遵循这种方法的应用程序往往有一个单一的 @Configuration 类,它最终了解系统中的所有内容,并包含大量小型 @Bean 方法。我也发现注解的自文档性质非常有用。我可以轻松搜索 @Service 并找到所有我的服务 bean。

Sam 的文章中写道:“...注解仍然像魔法。直到你运行应用程序,你才知道它是否正确连接。” 但我看不出这与一个 手动连接所有东西 的类有什么太大区别。你仍然需要在某个时候运行整个应用程序来确保它正常工作。

分层

任何规模较大的系统都需要考虑架构分层。几乎所有我的 Spring 应用程序最终都底部有一个“领域”层,顶部有一个“Web”层。我发现最好是强制严格的分层分离,并保持依赖关系朝一个方向。用 @ResponseStatus 注解领域服务异常非常容易,但在实践中我很少发现这是一个好主意。同样,在 @Entity 上添加一些 Jackson 注解以便它可以被转换为 JSON 也很容易,但这通常会在长期运行中导致问题。

我推荐使用 Structure 101SonarQube 等工具,它们可以检查你的代码是否存在包依赖混乱和设计问题。

Spring MVC

我通常认为最好将业务逻辑放在 Spring MVC @Controller bean 之外。我喜欢将 Spring MVC 视为一个小小的适配器层。它负责接收 HTTP 输入、验证它、将其传递给服务然后返回响应。我经常会在 web 层添加中介 @Service bean 来协调对业务服务的调用。这通常使得测试变得容易得多,尤其是在使用 MVC 测试框架 时。

日志记录

Java 中的日志记录实在不应该这么复杂。有太多的日志库可供选择,以及太多的不同组合,很容易让人陷入困境。幸运的是,对于典型的 Spring Boot 应用程序,你真的不需要担心太多。我的建议始终是:

  • 在你的代码中使用 SLF4J 进行日志记录。
  • 如果可能,坚持使用 Spring Boot 默认选择的 logback
  • 只记录到控制台,并使用 Splunk 等工具进行捕获和存储。

测试

在 Spring 中进行单元测试的黄金法则是:让 Spring 远离你的单元测试!你应该能够在不启动应用程序上下文的情况下对大部分 bean 进行单元测试。测试领域关于 mock 存在很多争论,但我通常认为明智地使用 Mockito(我喜欢 BDD Mockito)非常有帮助。

在进行集成测试时,你通常需要引入 Spring。我倾向于发现我的集成测试通常会配置应用程序的“层”。例如,领域测试可能会启动 Hibernate + Spring Data + 内存数据库。对于 Spring MVC 测试,我倾向于不使用数据库,而是注入 mock 服务。上面讨论的分层架构在这里真正开始发挥作用,因为它很容易模拟或 stub 你的“web 层”所需的内容。

启动时间

一个典型的 Spring Boot REST 应用程序启动时间大约是 2.5 秒,一旦加入像 Hibernate 这样的技术,速度会变慢。Spring 的设计理念是“快速失败 & 尽早失败”,不幸的是,这确实会影响启动时间。从积极的一面看,一旦应用程序启动,它很可能处于正常运行状态。

如果你的启动时间有问题,可以考虑查看 Spring Boot Devtools,它应该能让重新启动更快。如果你有一些自己创建很慢的 bean,也应该考虑使用 @Lazy 初始化。

Spring Boot

Spring Boot 于 2013 年底推出,专门解决 Spring 用户面临的一些常见问题。不幸的是,我经常看到这句话被转述:“Spring 现在如此复杂,以至于它拥有了自己的框架。” Spring Boot 有意与 Spring Framework 区分开来,它有不同的目标、不同的依赖和不同的发布周期。我更喜欢将 Spring Framework 视为原材料,而将 Spring Boot 视为烘烤好的蛋糕。你可以自由地以任何你认为合适的方式混合这些原料,但谁不喜欢蛋糕呢?

Ingredients Cake

选择与开源

开源最伟大的优点之一就是它为你提供了巨大的自由。如果你想要依赖注入,但不太喜欢 Spring,可以看看 Guice。如果你想构建 REST 服务,但不喜欢 Spring MVC,可以试试 Ratpack。如果你想要一个集成良好、经过充分测试、文档齐全且有良好记录的框架,可以试试 SpringSpring Boot

如果你想改变你的开源项目的工作方式,尝试贡献吧!

获取 Spring 时事通讯

通过 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助你快速进步。

了解更多

获取支持

Tanzu Spring 通过一份简单的订阅,提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区所有即将到来的活动。

查看全部