2016 年如何避免讨厌 Spring

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

在感恩节周末,一篇 2014 年的文章名为 "Sam Atkinson 的《为什么我讨厌 Spring》" 在 Twitter 上开始流传。倾听批评意见,了解我们如何改进 Spring,这始终很有趣,大部分的 Spring Boot 就是源于倾听 人们谈论他们在框架中遇到的问题

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

向后兼容性的诅咒

除了人们写关于他们多么讨厌 Spring 之外,在 Spring 上工作的一个更具压力的方面是,功能往往具有非常长的尾部。Spring 非常重视向后兼容性,这意味着一旦添加了某个功能,就很难从根本上改变它。开发框架带来了应用程序开发人员通常不会遇到的独特挑战。诸如 YAGNI(你不需要它)和积极重构之类的敏捷技术更难应用。我们有 SMNI(有人可能需要它)和谨慎重构。在 Java 中存在 lambda、泛型甚至注释之前就很有意义的功能仍然得到支持。很容易争辩说,XML 如今不是连接 bean 的好方法,但争辩说它应该完全删除则要困难得多。我个人重视能够轻松升级旧的 Spring 应用程序并逐步迁移到新技术的事实。

还要记住,就像商业软件一样,开源也有有限的资源和时间压力。有时我们作为开发人员确实会犯错误,并且需要忍受一段时间。

太大而不能倒?

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

依赖项

一个典型的基于 Spring 的应用程序将依赖于任意数量的第三方库。从历史上看,很难确切地知道各种库的哪些版本可以很好地协同工作。如果您今天要启动一个新项目,我强烈建议使用 Spring Boot 的父 POMSpring 平台。两者都提供了一组经过精心挑选的依赖项,这些依赖项已知可以很好地协同工作。

我们甚至开发了一个 Gradle 插件,允许您在没有 Maven 父依赖项功能的情况下使用 POM 的已管理依赖项 部分。

如果您由于某种原因无法使用已管理的依赖项,您仍然可以参考 Spring Boot 文档以获取 已知可以很好地协同工作的兼容版本列表

注入

始终在您的 bean 中使用 基于构造函数的依赖项注入。始终对强制依赖项使用 断言。有关为什么基于字段的注入有害的更多背景信息,您可以阅读 Oliver Gierke 的这篇文章

与往常一样,规则有一个例外,当您 使用 SpringJUnit4ClassRunner 时,在测试中使用基于字段的注入是可以的。

泄漏的 Spring

我看到一些人争论说应用程序代码绝不应该依赖于任何框架。即,任何地方(配置代码除外)的 org.springframework 导入都是不好的。虽然您绝对可以这样做(对标准注释(例如 @Inject)有广泛的支持),但我倾向于不那么武断。我更喜欢在这里应用 80/20 规则。我认为依赖于框架类和注释是可以的,但我不想在离开 Spring 时重写超过 20% 的类。

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

配置

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

有些人认为为所有定义的 bean 提供显式配置更好,但在实践中,我发现这是一种阻碍。遵循这种方法的应用程序往往只有一个 @Configuration 类,最终会了解系统中的所有内容,并且包含一堆小的 @Bean 方法。我还发现注释的自文档特性非常有用。我可以轻松搜索 @Service 并找到我所有的服务 bean。

Sam 的文章指出“...注释仍然是魔术。在运行应用程序之前,您不知道它是否已正确连接”,但我没有真正看到这与拥有一个 手动连接所有内容的类 有什么区别。您仍然需要在某个时候运行整个应用程序才能确保它正常工作。

分层

任何规模较大的系统都需要考虑架构分层。几乎我所有的 Spring 应用程序最终都会在底部有一个“领域(domain)”层,在顶部有一个“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 进行单元测试。在测试领域关于模拟有很多争论,但我通常发现谨慎使用Mockito(我喜欢BDD Mockito)非常有帮助。

在进行集成测试时,您通常需要涉及 Spring。我倾向于发现我的集成测试通常会配置应用程序的“一层”。例如,领域测试可能会启动 Hibernate + Spring Data + 内存数据库。对于 Spring MVC 测试,我倾向于不使用数据库,并注入模拟服务。上面讨论的分层架构在这里真正开始发挥作用,因为它可以轻松模拟或存根“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 社区中所有即将举行的活动。

查看全部