Spring 框架是什么?不仅仅是依赖注入
(编者注:本文约 7800 字,您可能不想在移动设备上阅读。请将其添加为书签,稍后再回来阅读。即使在桌面上,也要一口一口地阅读这篇文章。)
介绍
Spring 生态系统的复杂性
很多公司都在使用 Spring,但如果你去spring.io看看,就会发现 Spring 世界实际上由 21 个不同的活跃项目组成。哎哟!
此外,如果您在过去几年内开始使用 Spring 进行编程,那么很有可能您直接进入了Spring Boot或 Spring Data。
但是,本指南仅介绍其中最重要的一个项目:Spring Framework。为什么?
因为我们必须理解 Spring 框架是所有其他项目的基础。Spring Boot、Spring Data、Spring Batch 都建立在 Spring 之上。
这有两层含义:
-
如果没有扎实的 Spring 框架知识,你迟早会迷失方向。无论你认为核心知识多么不重要,你都无法完全理解 Spring Boot 之类的东西。
-
花费约 15 分钟阅读本指南,其中涵盖了 Spring 框架最重要的 80%,将为您的职业生涯带来百万倍的回报。
什么是 Spring 框架?
简短的回答:
Spring 框架的核心其实就是一个依赖注入容器,在其上附加了一些便利层(例如:数据库访问、代理、面向方面编程、RPC、Web MVC 框架)。它能帮助你更快、更便捷地构建 Java 应用程序。
那么,这真的没有什么帮助,不是吗?
幸运的是,还有一个很长的答案:
本文档的其余部分。
依赖注入基础知识
如果你已经知道依赖注入是什么,可以直接跳到section_title。否则,请继续阅读。
什么是依赖?
假设您正在编写一个 Java 类,用于访问数据库中的用户表。您可能将这些类称为 DAO(数据访问对象)或 Repositories。因此,您需要编写一个 UserDAO 类。
public class UserDao {
public User findById(Integer id) {
// execute a sql query to find the user
}
}
您的 UserDAO 只有一种方法,可让您通过各自的 ID 在数据库表中查找用户。
要执行正确的 SQL 查询,你的 UserDAO 需要一个数据库连接。在 Java 世界中,你(通常)从另一个名为 DataSource 的类获取该数据库连接。所以,你的代码现在看起来应该像这样:
import javax.sql.DataSource;
public class UserDao {
public User findById(Integer id) throws SQLException {
try (Connection connection = dataSource.getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where id = ?");
// use the connection etc.
}
}
}
- 现在的问题是,你的 UserDao 从哪里获取它的 dataSource依赖?DAO 显然依赖于一个有效的 DataSource 来触发那些 SQL 查询。
使用 new() 实例化依赖项
最简单的解决方案是每次需要时都通过构造函数创建一个新的 DataSource。因此,要连接到 MySQL 数据库,你的 UserDAO 可以像这样:
import com.mysql.cj.jdbc.MysqlDataSource;
public class UserDao {
public User findById(Integer id) {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
try (Connection connection = dataSource.getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where id = ?");
// execute the statement..convert the raw jdbc resultset to a user
return user;
}
}
}
-
我们想要连接到 MySQL 数据库;因此我们使用 MysqlDataSource 并在此处硬编码 url/用户名/密码以便于阅读。
-
我们使用新创建的数据源进行查询。
这是可行的,但是让我们看看当我们用另一种方法 findByFirstName 扩展我们的 UserDao 类时会发生什么。
不幸的是,该方法还需要一个 DataSource 才能工作。我们可以将这个新方法添加到 UserDAO 中,并通过引入一个newDataSource方法进行一些重构。
import com.mysql.cj.jdbc.MysqlDataSource;
public class UserDao {
public User findById(Integer id) {
try (Connection connection = newDataSource().getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where id = ?");
// TODO execute the select , handle exceptions, return the user
}
}
public User findByFirstName(String firstName) {
try (Connection connection = newDataSource().getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name = ?");
// TODO execute the select , handle exceptions, return the user
}
}
public DataSource newDataSource() {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
return dataSource;
}
}
-
findById 已被重写以使用新的 newDataSource() 方法。
-
findByFirstName 已被添加并且还使用新的 newDataSource() 方法。
-
这是我们新提取的方法,能够创建新的数据源。
这种方法有效,但有两个缺点:
-
如果我们想创建一个新的 ProductDAO 类,它也执行 SQL 语句,会发生什么?这时,ProductDAO 类会有一个 DataSource 依赖,而这个依赖现在只能在 UserDAO 类中使用。这时,你需要另一个类似的方法,或者提取一个包含 DataSource 的辅助类。
-
我们为每个 SQL 查询创建一个全新的 DataSource。考虑到 DataSource 会打开一个从 Java 程序到数据库的真实套接字连接。这需要时间,而且成本相当高。如果我们只打开一个DataSource 并重复使用,而不是打开和关闭大量的 DataSource,那就更好了。一种方法是将 DataSource 保存在 UserDao 的私有字段中,这样就可以在方法之间重用它——但这不利于多个 DAO 之间的重复调用。
全局应用程序类中的依赖项
为了解决这些问题,您可以考虑编写一个全局的 Application 类,如下所示:
import com.mysql.cj.jdbc.MysqlDataSource;
public enum Application {
INSTANCE;
private DataSource dataSource;
public DataSource dataSource() {
if (dataSource == null) {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
this.dataSource = dataSource;
}
return dataSource;
}
}
您的 UserDAO 现在可能看起来像这样:
import com.yourpackage.Application;
public class UserDao {
public User findById(Integer id) {
try (Connection connection = Application.INSTANCE.dataSource().getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where id = ?");
// TODO execute the select etc.
}
}
public User findByFirstName(String firstName) {
try (Connection connection = Application.INSTANCE.dataSource().getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name = ?");
// TODO execute the select etc.
}
}
}
这是两个方面的改进:
-
您的 UserDAO 不再需要构建自己的 DataSource 依赖项,而是可以请求 Application 类为其提供一个功能齐全的依赖项。所有其他 DAO 也一样。
-
您的应用程序类是一个单例(意味着只会创建一个实例),并且该应用程序单例保存对 DataSource 单例的引用。
然而,该解决方案仍然存在一些缺点:
-
UserDAO必须主动知道从哪里获取其依赖项,它必须调用应用程序类→Application.INSTANCE.dataSource()。
-
如果你的程序越来越大,依赖项也越来越多,你就会有一个庞大的 Application.java 类来处理所有依赖项。这时,你就需要尝试将依赖项拆分成更多的类/工厂等等。
控制反转(IoC)
让我们更进一步。
如果您和 UserDAO完全不必担心查找依赖项,那不是很好吗?您的 UserDAO 无需主动调用 Application.INSTANCE.dataSource(),只需(以某种方式)告知需要依赖项,但无法控制何时、如何、从哪里获取依赖项?
这就是所谓的控制反转。
让我们看一下,有了全新的构造函数之后,我们的 UserDAO 会是什么样子。
import javax.sql.DataSource;
public class UserDao {
private DataSource dataSource;
private UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public User findById(Integer id) {
try (Connection connection = dataSource.getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where id = ?");
// TODO execute the select etc.
}
}
public User findByFirstName(String firstName) {
try (Connection connection = dataSource.getConnection()) {
PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name = ?");
// TODO execute the select etc.
}
}
}
-
每当调用者通过构造函数创建一个新的 UserDao 时,调用者还必须传入一个有效的 DataSource。
-
findByX 方法将简单地使用该数据源。
从 UserDao 的角度来看,这读起来好多了。它不再了解应用程序类,也不知道如何自行构造 DataSources。它只向外界宣告:“如果你想构造(即使用)我,你需要给我一个数据源”。
但是假设你现在想要运行你的应用程序。之前你可以调用“new UserService()”,但现在你必须确保调用的是 new UserDao(dataSource)。
public class MyApplication {
public static void main(String[] args) {
UserDao userDao = new UserDao(Application.INSTANCE.dataSource());
User user1 = userDao.findById(1);
User user2 = userDao.findById(2);
// etc ...
}
}
依赖注入容器
因此,问题在于,作为程序员,您仍然在通过构造函数主动构建 UserDAO,从而手动设置 DataSource 依赖项。
如果有人知道你的 UserDAO 依赖 DataSource 构造函数,并且知道如何构造它,那岂不是很棒?然后还能神奇地为你构造出两个对象:一个可以工作的 DataSource 和一个可以工作的 UserDao?
那个人是一个依赖注入容器,这正是 Spring 框架所关注的。
Spring 的 IOC/依赖注入容器
正如一开始提到的,Spring 框架的核心是一个依赖注入容器,它负责管理你编写的类及其依赖关系(参见上一节)。让我们来了解一下它是如何做到的。
什么是 ApplicationContext?它有什么用处?
在 Spring 世界中,可以控制所有类并能适当管理它们(即使用必要的依赖项创建它们)的人被称为ApplicationContext 。
我们想要实现的是下面的代码(我在上一节中描述了UserDao和DataSource,如果你直接跳到这里就去浏览一下):
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import javax.sql.DataSource;
public class MyApplication {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass);
UserDao userDao = ctx.getBean(UserDao.class);
User user1 = userDao.findById(1);
User user2 = userDao.findById(2);
DataSource dataSource = ctx.getBean(DataSource.class);
// etc ...
}
}
-
这里我们正在构建 Spring ApplicationContext。在接下来的段落中,我们将更详细地介绍它的工作原理。
-
ApplicationContext 可以为我们提供一个完全配置的 UserDao,即设置了 DataSource 依赖项的 UserDao。
-
ApplicationContext 也可以直接给我们提供 DataSource,它和 UserDao 里面设置的 DataSource是一样的。
这很酷,不是吗?作为调用者,你不用再操心构造类了,直接让 ApplicationContext 给你提供可用的类就行了!
但这是如何实现的呢?
什么是 ApplicationContextConfiguration?如何从配置构建 ApplicationContext。
在上面的代码中,我们在 AnnotationConfigApplicationContext 构造函数中放置了一个名为“someConfigClass”的变量。这里简单回顾一下:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MyApplication {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass);
// ...
}
}
您真正想要传递到 ApplicationContext 构造函数中的是配置类的引用,它应该如下所示:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyApplicationContextConfiguration {
@Bean
public DataSource dataSource() {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
return dataSource;
}
@Bean
public UserDao userDao() {
return new UserDao(dataSource());
}
}
-
您有一个专用的 ApplicationContext 配置类,用 @Configuration 注释注释,它看起来有点像section_title中的 Application.java 类。
-
您有一个返回 DataSource 的方法,并用 Spring 特定的@bean注释进行注释。
-
您有另一种方法,它返回一个 UserDao 并通过调用 dataSource bean 方法构造所述 UserDao。
这个配置类已经足以运行您的第一个 Spring 应用程序。
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MyApplication {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyApplicationContextConfiguration.class);
UserDao userDao = ctx.getBean(UserDao.class);
// User user1 = userDao.findById(1);
// User user2 = userDao.findById(1);
DataSource dataSource = ctx.getBean(DataSource.class);
}
}
现在,让我们了解一下 Spring 和 AnnotationConfigApplicationContext 对您编写的配置类究竟做了什么。
为什么要构造 AnnotationConfigApplicationContext?还有其他 ApplicationContext 类吗?
构建 Spring ApplicationContext 的方法有很多种,例如通过 XML 文件、带注解的 Java 配置类,甚至可以通过编程方式。对外而言,这通过单个ApplicationContext接口来表示。
从上面看 MyApplicationContextConfiguration 类。它是一个包含 Spring 特定注解的 Java 类。因此,你需要创建一个注解ConfigApplicationContext。
相反,如果您想从 XML 文件创建 ApplicationContext,则可以创建ClassPathXmlApplicationContext。
还有许多其他的,但在现代 Spring 应用程序中,您通常会从基于注释的应用程序上下文开始。
@bean注解的作用是什么?什么是 Spring Bean?
你必须将 ApplicationContext 配置类中的方法视为工厂方法。目前,有一个方法知道如何构造 UserDao 实例,还有一个方法知道如何构造 DataSource 实例。
这些工厂方法创建的实例称为Bean。这是一个更贴切的说法:我(Spring 容器)创建了它们,并且它们在我的控制之下。
但这引出了一个问题:Spring 应该创建多少个特定 bean 的实例?
Spring bean 范围是什么?
Spring 应该创建多少个DAO实例?要回答这个问题,你需要了解Bean 的作用域。
-
Spring 是否应该创建一个单例:所有 DAO 共享相同的数据源?
-
Spring 是否应该创建一个原型:所有 DAO 都有自己的数据源?
-
或者你的 bean 应该有更复杂的作用域,比如说:每个 HttpRequest 对应一个新的 DataSource?每个 HttpSession 对应一个新的 DataSource?或者每个 WebSocket 对应一个新的 DataSource?
您可以在此处阅读可用 bean 范围的完整列表,但现在只需知道您可以使用另一个注释来影响范围。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyApplicationContextConfiguration {
@Bean
@Scope("singleton")
// @Scope("prototype") etc.
public DataSource dataSource() {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
return dataSource;
}
}
作用域注解控制 Spring 将创建多少个实例。如上所述,这相当简单:
-
Scope("singleton") → 您的 bean 将是单例,只会有一个实例。
-
Scope("prototype") → 每当有人需要引用你的 bean 时,Spring 都会创建一个新的。(不过这里有几点需要注意,比如在单例中注入原型)。
-
Scope(“session”) → 每个用户 HTTP 会话都会创建一个 bean。
-
ETC。
要点:大多数 Spring 应用程序几乎完全由单例 bean 组成,偶尔会涉及其他 bean 范围(原型、请求、会话、websocket 等)。
现在您已经了解了 ApplicationContexts、Beans 和 Scopes,让我们再看看依赖关系,或者我们的 UserDAO 可以获取 DataSource 的多种方式。
Spring 的 Java 配置是什么?
到目前为止,您已经在 ApplicationContext 配置中借助@bean注释的 Java 方法明确配置了 Bean。
这就是所谓的 Spring Java 配置,而不是像 Spring 历史上那样,将所有配置都写在 XML 中。我们来简单回顾一下它的样子:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyApplicationContextConfiguration {
@Bean
public DataSource dataSource() {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
return dataSource;
}
@Bean
public UserDao userDao() {
return new UserDao(dataSource());
}
}
- 一个问题:为什么必须显式调用 new UserDao() ,并手动调用 dataSource() ?Spring 自己不能解决所有问题吗?
这就是另一个名为 @ComponentScan 的注释的用武之地。
@ComponentScan 起什么作用?
您需要对上下文配置应用的第一个更改是使用附加的 @ComponentScan 注释对其进行注释。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class MyApplicationContextConfiguration {
@Bean
public DataSource dataSource() {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("s3cr3t");
dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
return dataSource;
}
// no more UserDao @Bean method!
}
-
我们添加了@ComponentScan注释。
-
请注意,上下文配置中现在缺少 UserDAO 定义!
这个 @ComponentScan 注释的作用是告诉 Spring:查看与上下文配置位于同一包中的所有Java 类是否看起来像 Spring Bean!
这意味着如果您的 MyApplicationContextConfiguration 位于包 com.marcobehler 中,Spring 将扫描以 com.marcobehler 开头的每个包(包括子包)以查找潜在的 Spring bean。
Spring 如何知道某个对象是否是 Spring bean?很简单:你的类需要用一个叫做 @Component 的标记注解来标注。
@Component 和 @Autowired 起什么作用?
让我们将@Component 注释添加到您的 UserDAO。
import javax.sql.DataSource;
import org.springframework.stereotype.Component;
@Component
public class UserDao {
private DataSource dataSource;
private UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
}
- 这告诉 Spring,类似于你之前写的@bean方法:嘿,如果你通过你的 @ComponentScan 发现我用 @Component 注释,那么我想成为一个 Spring bean,由你(依赖注入容器)管理!
(当您稍后查看@Controller、@Service 或@Repository 等注释的源代码时,您会发现它们都由多个进一步的注释组成,并且始终包括@Component!)。
只缺少一小段信息。Spring 如何知道它应该采用你指定为@bean方法的 DataSource,然后使用该特定的 DataSource 创建新的 UserDAO?
很简单,使用另一个标记注解:@Autowired。因此,你的最终代码将如下所示。
import javax.sql.DataSource;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class UserDao {
private DataSource dataSource;
private UserDao(@Autowired DataSource dataSource) {
this.dataSource = dataSource;
}
}
现在,Spring 拥有创建 UserDAO bean 所需的所有信息:
-
UserDAO 用 @Component 注释 → Spring 将创建它
-
UserDAO 有一个 @Autowired 构造函数参数 → Spring 将自动注入通过@bean方法配置的 DataSource
-
如果在任何 Spring 配置中没有配置 DataSource,您将在运行时收到 NoSuchBeanDefinition 异常。
构造函数注入与自动装配重温
我在上一节中稍微骗了你一下。在早期的 Spring 版本(4.2 之前)中,你需要指定 @Autowired 才能使构造函数注入起作用。
在较新的 Spring 版本中,Spring 实际上已经足够智能,无需在构造函数中显式使用 @Autowired 注解即可注入这些依赖项。所以这也能正常工作。
@Component
public class UserDao {
private DataSource dataSource;
private UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
}
那我为什么要提到 @Autowired 呢?因为它不会造成任何损害,也就是说,它使事情更加明确,而且除了构造函数之外,你还可以在许多其他不同的地方使用 @Autowired。
让我们看一下依赖注入的不同方式——构造函数注入只是其中一种。
什么是字段注入?什么是 Setter 注入?
简单地说,Spring 不必通过构造函数来注入依赖项。
它也可以直接注入字段。
import javax.sql.DataSource;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class UserDao {
@Autowired
private DataSource dataSource;
}
或者,Spring 也可以注入 setter。
import javax.sql.DataSource;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class UserDao {
private DataSource dataSource;
@Autowired
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
这两种注入方式(字段、setter)与构造函数注入的结果相同:您将获得一个可以正常工作的 Spring Bean。(实际上,还有另一种注入方式,称为方法注入,我们这里就不介绍了。)
但显然,它们彼此不同,这意味着关于哪种注入方式最好以及应该在项目中使用哪种注入方式存在很多争论。
构造函数注入 vs. 字段注入
网络上关于构造函数注入和字段注入哪个更好的争论很多,甚至有强烈的声音声称setter 注入是有害的。
为了避免这些争论进一步加剧,本文的要点如下:
-
近年来,我在多个项目中尝试过构造函数注入和字段注入这两种注入方式。仅就个人经验而言,我并不真正偏爱哪一种。
-
一致性为王:不要对 80% 的 bean 使用构造函数注入,对 10% 使用字段注入,对剩余的 10% 使用方法注入。
-
Spring官方文档中的方法似乎比较合理:对强制依赖项使用构造函数注入,对可选依赖项使用 setter/字段注入。需要注意的是:请务必遵循此方法。
摘要:Spring的IoC容器
到目前为止,您应该了解有关 Spring 依赖容器的所有内容。
当然还有更多内容,但是如果您对 ApplicationContexts、Beans、依赖项和不同的依赖注入方法有很好的掌握,那么您已经走在了正确的道路上。
让我们看看除了纯粹的依赖注入之外,Spring 还能提供什么。
Spring AOP(面向方面编程)和代理
依赖注入或许能让程序结构更合理,但 Spring 生态系统的精髓并非如此。我们再来看一个简单的 ApplicationContextConfiguration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyApplicationContextConfiguration {
@Bean
public UserService userService() {
return new UserService();
}
}
- 我们假设 UserService 是一个类,它允许您从数据库表中查找用户 - 或者将用户保存到该数据库表中。
这就是 Spring 隐藏的杀手级功能:
Spring 读取该上下文配置,其中包含您编写的@bean方法,因此 Spring 知道如何创建和注入 UserService bean。
但是 Spring 可以作弊,创建UserService 类以外的其他类。怎么做到的?为什么?
Spring 可以创建代理
因为在底层,任何 Spring @bean方法都可以返回看起来和感觉像(在您的情况下) UserService 的东西,但实际上不是。
它可以返回给你一个代理。
代理将在某个时候委托给您编写的 UserService,但首先将执行其自己的功能。
更具体地说,Spring 默认会创建动态Cglib 代理,这些代理不需要接口即可工作(类似于 JDK 的内部代理机制):相反,Cglib 可以通过动态子类化来代理类。(如果您不确定各个代理模式,请阅读Wikipedia 上有关代理的更多信息。)
Spring 为什么要创建代理?
因为它允许 Spring 给你额外的功能,而无需修改你的代码。简而言之,这就是面向方面(或:AOP)编程的精髓所在。
让我们看一下最流行的AOP 示例,Spring 的 @Transactional 注释。
Spring 的 @Transactional
上面的 UserService 实现可能看起来有点像这样:
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class UserService {
@Transactional
public User activateUser(Integer id) {
// execute some sql
// send an event
// send an email
}
}
-
我们编写了一个 activateUser 方法,当调用该方法时,需要执行一些 SQL 来更新数据库中用户的状态,也许发送电子邮件或消息事件。
-
该方法上的 @Transactional 标记向 Spring 发出信号,告知该方法需要打开数据库连接/事务才能工作,并且该事务也应在方法结束时提交。Spring需要执行此操作。
问题:虽然 Spring 可以通过 applicationContext 配置创建 UserService Bean,但它无法重写 UserService。它无法简单地在其中注入打开数据库连接并提交数据库事务的代码。
但它能做的是,为你的 UserService创建一个支持事务的代理。这样,代理只需要知道如何打开和关闭数据库连接,然后就可以简单地将操作委托给你的 UserService 了。
让我们再次看一下那个无辜的 ContextConfiguration。
@Configuration
@EnableTransactionManagement
public class MyApplicationContextConfiguration {
@Bean
public UserService userService() {
return new UserService();
}
}
-
我们添加了一个注释信号 Spring:是的,我们需要 @Transactional 支持,它会自动在后台启用 Cglib 代理。
-
通过上述注解设置,Spring不仅仅是在这里创建并返回你的 UserService。它还会为你的 bean 创建一个 Cglib 代理,该代理负责查看、嗅探和委托你的 UserService,但实际上它包装了你的 UserService 并提供其事务管理功能。
这乍一看可能有点不直观,但大多数 Spring 开发人员在调试会话中很快就会遇到代理。由于代理的存在,Spring 的堆栈跟踪可能会变得相当长且不熟悉:当你进入一个方法时,你很可能会先进入代理内部——这让人望而却步。然而,这完全是正常且在意料之中的行为。
Spring 必须使用 Cglib 代理吗?真正的字节码编织怎么办?
使用 Spring 进行 AOP 编程时,代理是默认选择。然而,您并不限于使用代理,也可以完全使用 AspectJ,如果需要,这会修改实际的字节码。不过,涵盖 AspectJ 的内容超出了本指南的范围。
另请参阅:section_title。
Spring 的 AOP 支持:摘要
关于面向方面编程当然还有很多内容可以讲,但本指南将帮助您了解最流行的 Spring AOP 用例(例如 @Transactional 或 Spring Security 的 @Secured)的工作原理。如果需要,您甚至可以编写自己的 AOP 注解。
作为突然结束的安慰,如果您想详细了解 Spring 的 @Transactional 管理如何工作,请查看我的 @Transactional 指南。
Spring 的资源管理
我们已经讨论了依赖注入和代理一段时间了。现在,我们先来看看 Spring 框架中我认为重要的便捷工具。其中一个就是 Spring 的资源支持。
想象一下,如何通过 HTTP 或 FTP 访问 Java 中的文件。您可以使用Java 的 URL 类,并编写一些管道代码。
同样,如何从应用程序的类路径读取文件?或者从 servlet 上下文(也就是 Web 应用程序的根目录)读取文件(诚然,在现代的 packaged.jar 应用程序中,这种情况越来越少见)。
同样,您需要编写大量样板代码才能使其工作,并且不幸的是,每个用例(URL、类路径、servlet 上下文)的代码都会有所不同。
但有一个解决方案:Spring 的资源抽象。这在代码中很容易解释。
import org.springframework.core.io.Resource;
public class MyApplication {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass);
Resource aClasspathTemplate = ctx.getResource("classpath:somePackage/application.properties");
Resource aFileTemplate = ctx.getResource("file:///someDirectory/application.properties");
Resource anHttpTemplate = ctx.getResource("https://marcobehler.com/application.properties");
Resource depends = ctx.getResource("myhost.com/resource/path/myTemplate.txt");
Resource s3Resources = ctx.getResource("s3://myBucket/myFile.txt");
}
}
-
与往常一样,您需要一个 ApplicationContext 来开始。
-
当您使用以classpath:开头的字符串在 applicationContext 上调用 getResource() 时,Spring 将在您的..应用程序类路径上查找资源。
-
当您使用以file:开头的字符串调用 getResource() 时,Spring 将在您的硬盘上查找文件。
-
当您使用以https:(或 http)开头的字符串调用 getResource() 时,Spring 将在网络上查找文件。
-
如果您未指定前缀,则实际上取决于您配置的 applicationContext 类型。更多信息请见此处。
-
这对于 Spring Framework 来说并不是开箱即用的,但借助 Spring Cloud 等附加库,您甚至可以直接访问 s3:// 路径。
简而言之,Spring 让你能够通过简洁的语法访问资源。资源接口有几个有趣的方法:
public interface Resource extends InputStreamSource {
boolean exists();
String getFilename();
File getFile() throws IOException;
InputStream getInputStream() throws IOException;
// ... other methods commented out
}
如您所见,它允许您对资源执行最常见的操作:
-
它存在吗?
-
文件名是什么?
-
获取对实际 File 对象的引用。
-
获取原始数据(InputStream)的直接引用。
这样,您就可以使用资源做任何您想做的事情,而不管它是在网络上、类路径上还是硬盘上。
资源抽象看起来像是一个很小的功能,但是当与 Spring 提供的下一个便利功能“属性”相结合时,它确实会大放异彩。
Spring 的环境是什么?
任何应用程序的很大一部分都是读取属性,例如数据库用户名和密码、电子邮件服务器配置、Stripe 支付详细信息配置等。
最简单的形式是,这些属性位于 .properties 文件中,并且可能有很多:
-
其中一些位于您的类路径上,因此您可以访问一些与开发相关的密码。
-
文件系统或网络驱动器中的其他内容,因此生产服务器可以拥有自己的安全属性。
-
有些甚至可以以操作系统环境变量的形式出现。
Spring 尝试通过其环境抽象让您轻松地注册并自动在所有这些不同来源中查找属性。
import org.springframework.core.env.Environment;
public class MyApplication {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass);
Environment env = ctx.getEnvironment();
String databaseUrl = env.getProperty("database.url");
boolean containsPassword = env.containsProperty("database.password");
// etc
}
}
-
通过 applicationContext,您可以随时访问当前程序的环境。
-
另一方面,环境让您可以访问属性等。
那么,环境到底是什么?
Spring 的 @PropertySources 是什么?
简而言之,一个环境由一到多个属性源组成。例如:
-
/mydir/application.properties
-
类路径:/application-default.properties
(注意:环境还由配置文件组成,即“开发”或“生产”配置文件,但我们不会在本指南的此修订版中详细介绍配置文件)。
默认情况下,Spring MVC Web 应用程序环境由 ServletConfig/Context 参数、JNDI 和 JVM 系统属性源组成。它们也是分层的,这意味着它们具有重要性顺序并且相互覆盖。
但是,自己定义新的@PropertySources相当容易:
import org.springframework.context.annotation.PropertySources;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySources(
{@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties"),
@PropertySource("file://myFolder/app-production.properties")})
public class MyApplicationContextConfiguration {
// your beans
}
现在我们就更容易理解为什么我们之前会提到section_title了。因为这两个功能是相辅相成的。
@PropertySource 注释适用于任何有效的 Spring 配置类,并允许您在 Spring 资源抽象的帮助下定义新的、附加的源:请记住,这都是关于前缀的:http://、file://、classpath: 等。
通过 @PropertySources 定义属性固然很好,但是除了通过环境变量来访问属性之外,还有什么更好的方法吗?有的。
Spring 的 @Value 注释和属性注入
你可以将属性注入到 Bean 中,就像使用 @Autowired 注解注入依赖项一样。但对于属性,你需要使用 @Value 注解。
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
@Component
public class PaymentService {
@Value("${paypal.password}")
private String paypalPassword;
public PaymentService(@Value("${paypal.url}") String paypalUrl) {
this.paypalUrl = paypalUrl;
}
}
-
@Value 注释直接作用于字段……
-
或者构造函数参数。
其实就没什么特别的了。每当你使用 @Value 注解时,Spring 都会遍历你的(层级)环境并查找合适的属性 - 如果不存在这样的属性,则会抛出错误消息。
附加模块
Spring 框架包含的模块还有很多,现在我们来看一下。
Spring Web MVC
Spring Web MVC,也称为 Spring MVC,是 Spring 的 Web 框架。它允许您构建任何与 Web 相关的内容,从基于 HTML 的网站到返回 JSON 或 XML 的 RESTful Web 服务。它还支持 Spring Boot 等框架。
注意 - 2020 年 4 月:您可以在我的新指南中找到有关 Spring MVC 的详细描述:Spring MVC:深入指南。
数据访问、测试、集成和语言
Spring 框架包含的便捷实用程序比您目前看到的还要多。我们将它们称为模块,不要将这些模块与 spring.io 上的其他 20 个 Spring 项目混淆。相反,它们都是 Spring 框架项目的一部分。
那么,我们谈论的是什么样的便利?
您必须了解,Spring 在这些模块中提供的所有内容,基本上都可以在纯 Java 中使用。无论是由 JDK 还是第三方库提供。Spring 框架始终构建在这些现有功能之上。
举个例子:使用Java 的 Mail API发送电子邮件附件当然可行,但使用起来有点麻烦。请参阅此处的代码示例。
Spring 在 Java 的 Mail API之上提供了一个简洁的小型包装器 API ,其额外的好处是,它提供的所有功能都能与 Spring 的依赖注入容器完美融合。它是 Springintegration
模块的一部分。
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
public class SpringMailSender {
@Autowired
private JavaMailSender mailSender;
public void sendInvoice(User user, File pdf) throws Exception {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setTo("john@rambo.com");
helper.setText("Check out your new invoice!");
FileSystemResource file = new FileSystemResource(pdf);
helper.addAttachment("invoice.pdf", file);
mailSender.send(mimeMessage);
}
}
-
与配置电子邮件服务器(url、用户名、密码)相关的所有内容都被抽象到 Spring 特定的 MailSender 类中,您可以将其注入任何想要发送电子邮件的 bean。
-
Spring 提供了便捷的构建器,例如 MimeMessageHelper,可以尽快从文件创建多部分电子邮件。
因此,总而言之,Spring 框架的目标是“springify”可用的 Java 功能,为依赖注入做好准备,从而使 API 更易于在 Spring 上下文中使用。
与配置电子邮件服务器(url、用户名、密码)相关的所有内容都被抽象到 Spring 特定的 MailSender 类中,您可以将其注入任何想要发送电子邮件的 bean。
- Spring 提供了便捷的构建器,例如 MimeMessageHelper,可以尽快从文件创建多部分电子邮件。
因此,总而言之,Spring 框架的目标是“springify”可用的 Java 功能,为依赖注入做好准备,从而使 API 更易于在 Spring 上下文中使用。
模块概述
我想简要概述一下您在 Spring 框架项目中可能遇到的最常用实用程序、功能和模块。但请注意,本指南无法详细介绍所有这些工具。您可以查看官方文档以获取完整列表。
-
Spring 的数据访问:不要与 Spring Data (JPA/JDBC) 库混淆。它是 Springs @Transactional 支持以及纯 JDBC 和 ORM(如 Hibernate)集成的基础。
-
Spring 的集成模块:使您更轻松地发送电子邮件、与 JMS 或 AMQP 集成、安排任务等。
-
Spring 表达式语言 (SpEL):虽然这并非完全正确,但可以将其视为 Spring Bean 创建/配置/注入的 DSL 或正则表达式。本指南的后续版本将对此进行更详细的介绍。
-
Spring 的 Web Servlet 模块:允许您编写 Web 应用程序。包含 Spring MVC,还支持 WebSockets、SockJS 和 STOMP 消息传递。
-
Spring 的 Web Reactive 模块:允许您编写反应式 Web 应用程序。
-
Spring 的测试框架:允许您(集成)测试 Spring 上下文以及 Spring 应用程序,包括用于测试 REST 服务的辅助实用程序。
Spring 框架:常见问题解答
我应该使用哪个 Spring 版本?
选择 Spring 版本相对简单:
-
如果您正在构建新的 Spring Boot 项目,则所使用的 Spring 版本已经为您预定义。例如,如果您使用的是 Spring Boot 2.2.x,那么您将使用 Spring 5.2.x(尽管理论上您可以覆盖此版本)。
-
如果您在新建项目中使用普通的 Spring,那么显然可以选择任何版本。当前最新版本是Spring 5.2.3.RELEASE,您可以随时在Spring 博客上找到新版本公告。
-
如果您在遗留项目中使用 Spring,那么您可以随时考虑升级到较新的 Spring 版本(如果从业务角度来看这有意义的话)(或者如果您想尊重EOL 公告) - Spring 版本具有高度的兼容性(请参阅下一段)。
实际上,您会发现公司使用的 Spring 版本是 4.x-5.x,但也会出现罕见的旧版 3.x(初始版本:2009 年)Spring 项目。
Spring 的新版本多久发布一次?支持期限是多久?
这是一个漂亮的小图表,向您展示了 Spring 的版本历史:
您可以看到,Spring 的初始版本大约在 17 年前发布,其主要框架版本每 3-4 年发布一次。但这还不包括维护分支。
-
例如,Spring 4.3 已于 2016 年 6 月发布,并将支持到 2020 年底。
-
甚至对 Spring 5.0 和 5.1 的支持也将于 2020 年底停止,转而支持 2019 年 9 月发布的 Spring 5.2。
注意:除 5.2 之外的所有 Spring 版本的当前 EOL()(不再更新和支持)目前设置为2020 年 12 月 31 日。
开始使用 Spring 需要哪些库?
事实上,当你要搭建一个 Spring 项目时,只需要一个依赖项,那就是spring-context。它是 Spring 依赖注入容器正常工作的最低要求。
如果您正在开发 Maven 或 Gradle 项目,您可以简单地向其中添加以下依赖项(另请参阅:section_title) - 而不是下载所述 .jar 文件并手动将其添加到您的项目中。
<!-- Maven -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
// Gradle
compile group: 'org.springframework', name: 'spring-context', version: '5.2.3.RELEASE'
要获得其他 Spring 功能(如 Spring 的 JDBC 或 JMS 支持),您将需要其他附加库。
您可以在Spring 官方文档中找到所有可用模块的列表,但就 Maven 依赖项而言,artifactIds 确实会跟随模块名称。以下是一个例子:
<!-- Maven -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
Spring 各个版本之间有何区别?
与 JVM 类似,Spring 版本具有极强的向后兼容性,这意味着你(基本上)仍然可以在最新的 Spring 5.0 中运行 Spring 1.0 的 XML 文件(虽然我承认我还没有尝试过)。此外,只需稍加努力,也可以从 Spring 3 升级到 Spring 5.0(请参阅迁移指南)。
因此,一般来说,较新的 Spring 版本构建于较旧的 Spring 版本之上,并且重大变更较少(例如,与 Python 2 和 3 相比)。因此,您在 Spring 3 或 4 版本中学习到的所有核心概念仍然适用于 Spring 5 版本。
您可以在这里全面了解过去 7 年中各个 Spring 版本的变化:
给你一个执行摘要:
核心(依赖注入、事务管理等)始终保持不变或不断扩展。然而,Spring 会与时俱进,提供对较新 Java 语言版本、测试框架增强、Websockets、响应式编程等的支持。
其他 20 个 Spring.io 项目现在做什么?
在本指南的范围内,我无法详细介绍所有不同的项目,但让我们看一下您最有可能遇到的项目。
-
Spring Boot:可能是最受欢迎的 Spring 项目。Spring Boot 是 Spring 框架的一个偏执版本。查看section_title来了解这个看似毫无意义的短语的真正含义。
-
Spring Batch:一个帮助您编写好的旧批处理作业的库。
-
Spring Cloud:一组库,可帮助您的 Spring 项目更轻松地与“云”(想想:AWS)集成或编写微服务。
-
Spring Security:一个帮助您保护的库,例如使用 OAuth2 或 Basic Auth 的 Web 应用程序。
-
等等……
要点:所有这些库都扩展了 Spring 框架并建立在其依赖注入核心原则之上。
Spring 和 Spring Boot 有什么区别?
如果你已经阅读了本指南,那么你现在应该明白 Spring Boot 是建立在Spring 构建的。虽然完整的 Spring Boot 指南即将发布,但这里还是举个例子来解释 Spring Boot 中“自定义默认值”的含义。
Spring 允许您从各种位置读取 .properties 文件,例如借助 @PropertySource 注解。它还允许您借助其 Web MVC 框架编写 JSON REST 控制器。
问题在于,你必须自己编写和配置所有这些单独的部分。而 Spring Boot 则将这些单独的部分打包在一起。例如:
-
始终自动在各个地方查找 application.properties 文件并读取它们。
-
始终启动嵌入式 Tomcat,以便您可以立即看到编写 @RestControllers 的结果。
-
自动配置您发送/接收 JSON 的所有内容,而无需担心特定的 Maven/Gradle 依赖项。
所有这些都通过在 Java 类中运行 main 方法来实现,该方法使用了 @SpringBootApplication 注解。更棒的是,Spring Boot 提供了 Maven/Gradle 插件,可让你将应用程序打包成 .jar 文件,你可以像这样运行它:
java -jar mySpringBootApp.jar
因此,Spring Boot 就是采用现有的 Spring 框架部分,对其进行预配置和打包 - 尽可能减少开发工作。
Spring AOP 和 AspectJ 之间有什么区别?
正如section_title部分所述,Spring 默认使用基于代理的 AOP。它将 bean 包装在代理中以实现事务管理等功能。这种方法虽然有一些限制和注意事项,但对于解决 Spring 开发人员面临的最常见 AOP 问题来说,是一种非常简单直接的方法。
另一方面,AspectJ 允许你通过加载时织入或编译时织入来更改实际的字节码。这为你提供了更多可能性,但同时也带来了更大的复杂性。
但是,您可以将 Spring 配置为使用 AspectJ 的AOP,而不是其默认的基于代理的 AOP。
如果您想获得有关此主题的更多信息,请访问以下几个链接:
Spring 和 Spring Batch 之间有什么区别?
Spring Batch 是一个框架,可以更轻松地编写批处理作业,即“每天凌晨 3 点读取这 95 个 CSV 文件,并为每个条目调用外部验证服务”。
再次,它建立在 Spring 框架之上,但它本身就是一个项目。
但请注意,如果不充分了解 Spring Framework 的常规事务管理以及它与 Spring Batch 的关系,就不可能构建坚如磐石的批处理作业。
Spring 和 Spring Web MVC 有什么区别?
如果您已经阅读过本指南,您现在应该了解 Spring Web MVC是Spring 框架的一部分。
从高层次上讲,它允许您在路由到@Controller 类的 DispatcherServlet 的帮助下将 Spring 应用程序转变为 Web 应用程序。
这些可以是 RestControllers(您将 XML 或 JSON 发送到客户端)或旧的 HTML 控制器,您可以使用 Thymeleaf、Velocity 或 Freemarker 等框架生成 HTML。
Spring 和 Struts 有什么区别?
问题应该是:Spring Web MVC 和 Struts 之间有什么区别?
简短的历史答案是:Spring Web MVC 最初是作为 Struts 的竞争对手,据称 Spring 开发人员认为它的设计很差(参见维基百科)。
现代的答案是,虽然Struts 2肯定仍在一些遗留项目中使用,但 Spring Web MVC 才是Spring 宇宙中所有与 Web 相关的框架的基础。从 Spring Webflow 到 Spring Boot 的 RestControllers。
哪个更好?Spring XML、注解还是 Java 配置?
Spring 最初仅支持 XML 配置。后来,慢慢地,越来越多的注解/Java 配置特性出现了。
今天,您会发现 XML 配置主要用于较旧的遗留项目 - 而较新的项目都采用基于 Java / 注释的配置。
但请注意两件事:
-
基本上没有什么可以阻止您在同一个项目中组合 XML / 注释 / Java 配置,这通常会导致混乱。
-
您希望在 Spring 配置中努力实现同质性,即不要随机生成一些带有 XML 的配置、一些带有 Java 配置的配置和一些带有组件扫描的配置。
构造函数注入还是字段注入?哪个更好?
正如依赖注入部分所述,这个问题会引发许多不同的意见。最重要的是,你的选择应该在整个项目中保持一致:不要对 83% 的 bean 使用构造函数注入,而对剩下的 17% 使用字段注入。
明智的做法是使用Spring 文档中推荐的方法:对强制依赖项使用构造函数注入,对可选依赖项使用 setter/字段注入,然后在整个类中对这些可选依赖项进行空检查。
最重要的是,请记住:您的软件项目的整体成功不会取决于您最喜欢的依赖注入方法的选择(双关语)。
Spring 的依赖注入容器有替代品吗?
是的,Java 生态系统中流行的两个是:
-
Google 的 Dagger,以前是 Square 的。
请注意,Dagger仅提供依赖注入,没有其他便捷功能。Guice 提供依赖注入和其他功能,例如事务管理(借助 Guice Persist)。
鳍
如果您已经读到这里,那么您现在应该对 Spring 框架有一个相当透彻的了解。
您将在后续指南中了解它如何与其他 Spring 生态系统库(如 Spring Boot 或 Spring Data)连接,但现在我希望您在尝试回答“什么是 Spring 框架?”这个问题时记住这个比喻。
想象一下您想要翻新一所房子(~= 建立一个软件项目)。
Spring 框架是您的 DIY 商店(~=依赖注入容器),它提供了大量不同的工具,从本生灯(~=资源/属性)到大锤(~= Web MVC),供您进行装修。这些工具可以帮助您更快、更便捷地翻新房屋(=~构建您的 Java 应用程序)。
(注意:不要问我如何得出这些比较的结论;))
今天就到这里。如果您有任何问题或建议,请发送电子邮件至marco@marcobehler.com或在下方留言。如果您想进行实践,请查看Spring 框架练习课程。
感谢您的阅读。 Auf Wiedersehen。
致谢
非常感谢:
-
Patricio Moschcovich,出色地校对了这篇文章并指出了大量的小错误。
-
Maciej Walkowiak 指出 @RestController 一直是 Spring MVC 的一部分,而不是 Spring Boot。
原始来源
本文最初发表于https://www.marcobehler.com/guides/spring-framework。
文章来源:https://dev.to/marcobehler/what-is-spring-framework-from-dependency-injection-to-web-mvc-2dhd