抢占先机
VMware 提供培训和认证,助你加速前进。
了解更多编辑:下面有一个很棒的评论提到,原标题《构建 Spring Boot 应用程序的结构》有点误导人,因为这篇博客无意讨论如何组织类型和包,而是思考 Spring 如何与你代码中的对象交互。下一段试图澄清这一点,但显然不够到位。总之,我已经修改了标题,这不幸会导致之前指向它的链接失效。对于糟糕的标题和现在两天内的断链,我感到抱歉。我希望它对每个人都有用,即使我在标题方面显然可以做得更好......
思考应用程序的结构是很困难的。在高层有很多东西需要考虑——它是批处理作业、Web 应用程序、消息应用程序等等。相应的框架——Spring Batch、Spring Webflux、Spring Integration——将指导这些决策。此外还有许多其他框架,它们经过量身定制,旨在帮助你在特定的业务领域构建生产级应用。我们不会在这篇文章中探讨这些。相反,我想回答一个问题:我们如何组织我们的配置?
我无法给你一个固定的答案并期望在一篇文章中说完,但我们可以讨论 2021 年 Spring Boot 应用程序配置的技术维度。更容易讨论的是如何组织你的 Java 对象,使其在控制反转(IoC)容器中良好运行。记住,归根结底,Spring 就是一大堆对象。它需要知道你希望如何安排你的对象——它们如何连接以及如何相互关联——以便为它们提供服务。例如,当一个方法开始和结束时,它可以开始和提交事务。当请求到达时,它可以创建 HTTP 端点来调用你的 Spring 控制器处理方法。它可以响应来自 Apache Kafka broker、AWS SQS、RabbitMQ 或其他任何地方的新消息来调用你的消息监听器对象。Spring 可以做的事情很多很多,但这都假设你首先已经向 Spring 注册了这些对象。
Spring 拥有你对象的元模型——这有点像 Java 的反射 API。它知道哪些类有注解。它知道哪些对象有构造函数。它知道给定的对象依赖于哪些依赖项、哪些 bean 以及哪种类型。你的任务是帮助它构建这个元模型来为你管理所有对象。例如,如果它能够控制你的对象的创建,那么它也可以在你的对象被创建之前改变它们的创建方式。
Spring 只有知道对象是如何连接在一起的,才能为你提供所有这些服务。因此,其思想是你给 Spring 提供普通的 Java 对象(POJO),它会检测它们上面的注解,并使用这些注解来连接你的服务的行为。但是,当然,除非它控制了你的 Java 对象的创建,否则它无法做到这一点。
在幕后,它通过创建 Java InvocationHandler(一个 JDK 代理)或者,更常见的是,使用类似 CGLIB 的东西创建一个扩展你的 Java 类的新类来做到这一点。这个类是你的类的一个子类。所以,想象一下你有这样一个类
class CustomerService {
private final JdbcTemplate template;
CustomerService (JdbcTemplate jt) {
this.JdbcTemplate = jt;
}
@Transactional
public void updateCustomer ( long customerId, String name){
// .. .
}
}
你希望 Spring 在每次调用该方法时自动启动和停止一个事务。为了实现这一点,Spring 需要在你方法调用之前和之后插入自身。在幕后,它做了类似这样的事情
class SpringEnhancedCustomerService extends CustomerService {
// Spring provides a reference from the applicationContext of type JdbcTemplate
SpringEnhancedCustomerService (JdbcTemplate jt) {
super(JdbcTemplate ) ;
}
@Override
public void updateCustomer (long customerId, String name) {
// call Java code to start a JDBC transaction
super.updateCustomer(customerId, name);
// call Java code to stop a JDBC transaction
}
}
在你的代码中,你可以注入对 CustomerService
的引用。你仍然会得到一个,但不是你创建的那个。相反,你会得到那个子类。正是这个魔术——你想要一顶帽子,结果却得到一顶里面有兔子的帽子——让 Spring 如此强大。
因此,Spring 必须知道你的对象。有很多方法可以做到这一点。
一种是你非常明确地指定。在 Spring Boot 之前,你有两种标准选项:XML 和 Java 配置。然而,那是 2013 年或更早的事情了。现在,我们不鼓励使用 XML,所以只剩下 Java 配置。这里有一个例子
@Configuration
class ServiceConfiguration {
@Bean DataSource h2DataSource (){
return ... ;
}
@Bean JdbcTemplate JdbcTemplate (DataSource ds) {
return new JdbcTemplate(ds);
}
@Bean CustomerService customerService (JdbcTemplate jdbcTemplate) {
return new CustomerService (jdbcTemplate);
}
}
在这里,你创建了三个对象并明确地将它们连接在一起。当 Spring 启动时,它会找到 @Configuration
类,调用所有用 @Bean
注解的方法,将所有返回值存储在应用程序上下文中,并使其可用于注入。如果看起来方法带有参数,它会查找返回该类型值的任何其他方法并先调用它。然后将此值作为参数注入到方法中。如果它已经因为其他注入调用了该方法,它只会重用已经创建的实例。
这种方法的好处在于它是明确的——所有关于对象如何连接的信息都在一个地方——配置类中。但是,对于你自己创建的类,信息却分散在两个不同的地方:类本身和配置类。
因此,还有另一种更隐式的方法你可以使用:组件扫描。在这种方法中,Spring 会在类路径中查找带有stereotype 注解的类,例如 @Component
或 @Controller
。所有的 stereotype 注解最终都用 @Component
注解。@Component
是最低层、最不区分的注解。如果你查看 @Controller
,它用 @Component
注解。如果你查看 @RestController
,它用 @Controller
注解。虽然有三层间接性,但用 @RestController
注解的类至少仍被视为用 @Component
注解的类。专门的注解增加了专门的处理方式,但它们仍然是 @Component
的特化,而不是替代品。
因此,我们可能会觉得在配置类中定义 CustomerService
并进行配置很烦人。毕竟,如果 Spring 只知道这个类,它肯定可以自己弄清楚其余的关系吧?它可以查看构造函数,看到要构造一个 CustomerService
实例,它需要一个 JdbcTemplate
的引用,而 JdbcTemplate
已经在其他地方定义了。
这就是组件扫描的作用。你可以将 @Service
(另一个用 @Component
注解的 stereotype 注解)添加到类中,然后删除配置类中的 @Bean
方法。Spring 会自动创建该服务,并提供所需的依赖项。它还会对该类进行子类化以提供这些服务。
我们正在取得进展,去除了越来越多的样板代码。但是 DataSource
和 JdbcTemplate
呢?你需要它们,但肯定不必每次都重新创建吧?这就是 Spring Boot 的精髓所在。它使用 @Condition
注解来修饰用 @Component
或 @Configuration
注解的类,以便在创建类或调用 @Bean
方法之前进行测试评估。这些测试可以在环境中寻找线索。例如,假设你的类路径中存在 H2——一个嵌入式 SQL 数据库。并且你的类路径中包含 JdbcTemplate
类的 spring-jdbc
库。它可以利用一个测试来检查类路径中这些类的存在,并推断你想要一个嵌入式 SQL DataSource
,并且希望一个 JdbcTemplate
实例与新创建的 DataSource
连接起来。它有自己的配置为你提供这些 bean。现在,你可以完全移除 @Configuration
类了!Spring Boot 提供了其中两个 bean,并根据 stereotype 注解推断出了另一个。
我们已经探讨了 Spring IoC 容器的基本动机,也了解了 IoC 容器如何工作以帮助实现框架提出的承诺。
确实,我们可以进一步深入,探讨面向切面编程(AOP)、自动配置等等,但这篇博文旨在提供一个心智模型,帮助你理解何时应用哪种配置,以便你可以专注于将可用软件安全快速地推向生产环境这一重要工作。