你可以用本教程来理解 Spring 框架是什么,以及其核心特性(比如依赖注入或者面向切面编程)是如何工作的。此外,还有一个全面的常见问题解答。

简介

Spring 生态圈的复杂性

很多公司都在用 Spring,但是如果你访问 Spring 官网 spring.io,会看到 Spring 生态圈实际上是由 21 个不同的项目组成!

此外,如果你是最近几年才开始用 Spring 编程,那么你很有可能是直接就开始用 Spring Boot 或者 Spring Data。

不过,本教程只关注这些项目中最重要的一个项目:Spring 框架。为什么呢?

这是因为,Spring 框架是所有其它项目的基础。Spring Boot、Spring Data、Spring Batch 都是在 Spring 的基础上创建的。

这有两个含义:

  • 如果没有正确的 Spring 框架知识,你迟早会迷路的。不管你认为核心知识有多不重要,你是没法全靠直觉深入理解像 Spring Boot 这种技术的。
  • 花上大约 15 分钟阅读本指南,它涵盖了 Spring 框架最重要的 80%,在你的职业生涯中会得到一百万次的回报。

什么是 Spring 框架?

简短的回答

在其核心,Spring 框架实际上只是一个依赖注入容器,在其上添加了一些方便的层(比如:数据库访问、代理、面向切面编程、RPC、Web MVC 框架)。它可以帮助我们更快、更方便地创建 Java 应用程序。

现在,这种回答真的没用,对吧?

好在这里还有一个长的回答:那就是本文档的其余部分。

依赖注入基础

如果你已经知道依赖注入是什么,可以直接跳到《Spring 的依赖注入》一节。否则,请继续读下去。

什么是依赖?

想像一下,我们在写一个 Java 类去访问数据库中的一个 users 表。我们会把这种类称为 DAO 或者 Repository。所以,我们就是要写一个 UserDAO 类:

  1. public class UserDAO {
  2. public User findById(Integer id) {
  3. // 执行一个 sql 查询找到用户
  4. }
  5. }

这个 UserDAO 类只有一个方法 findById(),就是通过用户的 ID 找到数据库中的用户。

要执行 SQL 查询,UserDAO 就需要一个数据库连接。在 Java 中,我们通常从另一个称为 DataSource 的数据源类中获取数据库连接。于是,我们的代码就变成下面这样子了:

import javax.sql.DataSource;

public class UserDAO {

    public User findById(Integer id) throws SQLException {
        try (Connection connection = dataSource.getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // 使用 connection 等等
        }
    }

}
  • (1)现在的问题是,UserDAO 从哪里得到它的数据源依赖呢?DAO 很显然得依赖于一个有效的数据源去执行 SQL 查询。

new() 实例化依赖

最笨的解决方案是每次需要数据源时,就通过构造器创建一个新的 DataSource。那么,要连接到 MySQL 数据库,我们的 UserDAO 就是这样的:

import com.mysql.cj.jdbc.MysqlDataSource;

public class UserDAO {

    public User findById(Integer id) {
        MysqlDataSource dataSource = new MysqlDataSource(); // (1)
        dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
        dataSource.setUser("root");
        dataSource.setPassword("s3cr3t");

        try (Connection connection = dataSource.getConnection()) { // (2)
             PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
             // 执行该语句...将 jdbc 结果集转换成 user
             return user;
        }
    }
}
  • (1)我们想连接到 MySQL 数据库;因此,我们这里用 MysqlDataSource,以及硬编码的 url、username、password,以便于阅读。
  • (2)用新创建的数据源来查询。

这段代码是有效的,不过,我们来看看用另一个方法 findByFirstName() 扩展 UserDAO 类时会发生什么。

不幸的是,该方法也需要一个数据源才能工作。我们可以把这个新方法添加到 UserDAO,并通过引入一个 newDataSource() 方法,应用一些重构。

import com.mysql.cj.jdbc.MysqlDataSource;

public class UserDAO {

    public User findById(Integer id) {
        try (Connection connection = newDataSource().getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO 执行select,处理异常,返回用户
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = newDataSource().getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO 执行select,处理异常,返回用户
        }
    }

    public DataSource newDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource(); // (3)
        dataSource.setUser("root");
        dataSource.setPassword("s3cr3t");
        dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
        return dataSource;
    }
}
  • (1)findById() 已经重写为用新的 newDataSource() 方法。
  • (2)findByFirstName() 已经加进来了,而且也用新的 newDataSource() 方法。
  • (3)这是我们新抽取出来的方法,能创建新的数据源。

这种方式有效,但是有两个缺点:

  1. 如果想创建一个新的 ProductDAO 类,该类也要执行 SQL 语句,会出现什么情况呢?ProductDAO 也会有一个 DataSource 依赖,而这个依赖现在只在 UserDAO 类中可用。然后,我们会有另一个类似的方法,或者提取一个包含我们的 DataSource 的辅助类。
  2. 我们正在为每个 SQL 查询创建一个全新的 DataSource。考虑到一个 DataSource 会打开从 Java 程序到数据库的真实 Socket 连接。这需要时间,并且相当昂贵。如果我们只打开一个 DataSource,并重用它,而不是打开和关闭一堆 DataSource,那就更好了。一种方法是将 DataSource 存在 UserDAO 中的私有字段中,这样就可以在方法之间重用 - 但这对在多个 DAO 之间的重复没什么帮助。

全局 Application 类中的依赖

为解决这些问题,我们可以考虑编写一个如下所示的全局 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()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO 执行 select 等。
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = Application.INSTANCE.dataSource().getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO 执行 select 等。
        }
    }
}

这段代码在两个方面做了改进:

  • (1)UserDAO 不必再构造自己的 DataSource 依赖,而是要求 Application 类为其提供功能齐全的 DataSource。其他所有 DAO 都相同。
  • (2)Application 类是单例的(也就是说只能创建一个实例),并且该 Application 单例持有一个对 DataSource 单例的引用。

不过,这种解决方案依然有几个缺点:

  1. UserDAO 必须主动知道从哪里得到其依赖,它必须调用 Application 类 → Application.INSTANCE.dataSource()。
  2. 如果程序越来越大,并且有越来越多的依赖,那么我们就会有一个庞大的 Application.java 类用来处理所有依赖。此时,我们就会想尝试将其分解为更多的类/工厂等等。

控制反转

下面我们更进一步。

如果 UserDAO 完全不必操心查找依赖,不用主动调用 Application.INSTANCE.dataSource(),而是可以以某种方式呼喊它需要一个依赖,但不再控制依赖什么时候获取,如何获取,以及从哪里获取,那会不会很好?

这就是所谓控制反转(Inversion of Control,IoC)。

下面我们来看看带有全新构造器的 UserDAO 是什么样子:

import javax.sql.DataSource;

public class UserDAO {

    private DataSource dataSource;

    private UserDAO(DataSource dataSource) { // (1)
        this.dataSource = dataSource;
    }

    public User findById(Integer id) {
        try (Connection connection = dataSource.getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select etc.
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = dataSource.getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select etc.
        }
    }
}
  • (1)每当调用者通过 UserDAO 构造器创建一个新的 UserDAO 时,调用者还必须传递一个有效的 DataSource。
  • (2)然后,findByX() 方法直接用该 DataSource 就可以了。

从 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 的依赖注入

正如开始时已经提到的那样,Spring 框架的核心是一个依赖注入容器,该容器为我们管理我们所写的类以及它们的依赖。下面我们来搞清楚它是如何做到的。

什么是 ApplicationContext?你需要它做什么?

这个某人,控制所有我们的类,并可以适当管理它们(即:创建的时候带上所需依赖),在 Spring 领域中叫做 ApplicationContext

我们要实现的是以下代码:

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); // (1)

        UserDAO UserDAO = ctx.getBean(UserDAO.class); // (2)
        User user1 = UserDAO.findById(1);
        User user2 = UserDAO.findById(2);

        DataSource dataSource = ctx.getBean(DataSource.class); // (3)
        // etc ...
    }
}
  • (1)这里我们是在构建 Spring ApplicationContext。在接下来的段落中,我们将更详细地讨论其工作原理。
  • (2)ApplicationContext 可以为我们提供一个完全配置好的 UserDAO,即一个带有设置好 DataSource 依赖的 UserDAO。
  • (3)ApplicationContext 也可以直接给我们提供 DataSource,这个 DataSource 与在 UserDAO 内设置的 DataSource 是一样的。

这很酷,对吧?作为调用者,你不必再操作构建类,只需要请求 ApplicationContext 给你一个可以用的类就可以了。

但这是如何实现的呢?

什么是 ApplicationContextConfiguration?如何从配置构造 ApplicationContext?

在上面的代码中,我们将一个 someConfigClass 的变量放在 AnnotationConfigApplicationContext 构造器中。如下是一个快速提醒:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MyApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass); // (1)
        // ...
    }
}

你真正想传到 ApplicationContext 构造器中的,是对一个配置类的引用,这个配置类应该是下面这样的:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyApplicationContextConfiguration {  // (1)

    @Bean
    public DataSource dataSource() {  // (2)
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("s3cr3t");
        dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
        return dataSource;
    }

    @Bean
    public UserDAO UserDAO() { // (3)
        return new UserDAO(dataSource());
    }

}
  • (1) 我们有一个专用的 ApplicationContext 配置类,用 @Configuration 注解进行注解,看起来有点像《全局 Application 类中的依赖》一节中的 Application.java
  • (2) 有一个方法返回一个 DataSource,用 Spring 特定的 @Bean 注解进行注解。
  • (3) 还有一个方法,返回一个 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 类。这就是为什么我们需要创建一个 AnnotationConfigApplicationContext 的原因。

如果想从 XML 文件创建 ApplicationContext,就得创建一个 ClassPathXmlApplicationContext

还有很多其它方式,但是在现代 Spring 应用程序中,我们通常会从基于注解的应用程序上下文开始。

@Bean 注解是做什么的?什么是 Spring Bean?

我们将不得不把 ApplicationContext 配置类中的方法当作是工厂方法。目前,有一个方法知道如何构建 UserDAO 实例,有一个方法构建 DataSource 实例。

这些工厂方法创建的这些实例称为 bean。这里有一个很花哨的说法:我(Spring 容器)创建了它们,它们在我的控制之下。

不过这就引出了一个问题:Spring 应该为一个指定的 bean 创建多少个实例呢?

什么是 Spring Bean 的作用域?

Spring 应该创建多少个 DAO 的实例?要回答这个问题,我们需要了解 bean 的作用域

  • Spring 应该创建一个单例(所有 DAO 共享同一个 DataSource)吗?
  • Spring 应该创建一个原型(所有 DAO 得到其自己的 DataSource)吗?
  • 或者 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;
    }
}

@Scope 注解控制 Spring 会创建多少个实例。如上所述,这很简单:

  • Scope(“singleton”):bean 将是单例的,仅有一个实例。
  • Scope(“prototype”):每次有人需要一个 bean 的引用时,Spring 就会创建一个新的 bean。(不过,有几个注意事项,比如在单例中注入原型)。
  • Scope(“session”):为每个用户 HTTP 会话创建一个 bean。
  • 等等。

要点:大多数 Spring 应用程序几乎完全由单例 Bean 组成,偶尔还会添加其它 Bean 作用域(prototype、request、session、websocket 等)。

现在你已经了解了 ApplicationContext、Bean 和作用域,下面我们再来看看依赖,或者 UserDAO 获取 DataSource 的多种方式。

什么是 Spring 的 Java 配置?

到目前为止,我们都是在用 @Bean 注解过的 Java 方法的帮助下,在 ApplicationContext 配置中显式配置 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() { // (1)
        return new UserDAO(dataSource());
    }

}
  • (1) 一个问题:为什么必须显式调用 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  // (1)
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;
    }

    // (2)
    // 不再有 UserDAO @Bean 方法
}
  • (1) 添加 @ComponentScan 注解。
  • (2) 请注意,UserDAO 定义现在从上下文配置中消失了!

@ComponentScan 注解要做的是告诉 Spring:请看一下与上下文配置同一个包中的所有 Java 类,看看它们是否看起来像一个 Spring Bean!

这意味着,如果你的 MyApplicationContextConfiguration 位于包 com.foo 中,那么 Spring 会扫描以 com.foo 开头的每个包(包括子包)中潜在的 Spring Bean。

那么 Spring 是怎么知道哪个是 Spring Bean 呢?很简单:用 @Component 注解过的类。

@Component @Autowired 是干嘛用的?

下面我们给 UserDAO 添加上 @Component 注解。

import javax.sql.DataSource;
import org.springframework.stereotype.Component;

@Component
public class UserDAO {

    private DataSource dataSource;

    private UserDAO(DataSource dataSource) { // (1)
        this.dataSource = dataSource;
    }
}
  • (1) 跟以前写的 @Bean 方法类似,这就是告诉 Spring:喂,如果你通过 @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。(实际上,有一种方式,称为方法注入,我们在这里就不讨论了)。

但是很明显,它们是不同的,这意味着对于哪种注入方式是最好的,以及在项目中应该使用哪种注入方式,已经有了很多争论。

构造器注入与字段注入

关于到底是构造器注入好,还是字段注入好,网上有很多争论,甚至有一些强烈的声音声称字段注入是有害的

为了不给这些争论增添更多的噪音,本文的要点是:

  1. 在最近几年里,我在不同的项目中都用了这两种风格。仅凭个人经验,我并不真正偏爱其中某一种。
  2. 一致性为王:不要对 80% 的 bean 用构造器注入,对 10% 的 bean 使用字段注入,对剩余的 10% 使用方法注入。
  3. 官方文档中的 Spring 方式似乎是明智的:对强制依赖使用构造器注入,对可选依赖使用 setter 或者字段注入。警告:要与之保持一致。

小结:Spring 的 IoC 容器

到目前为止,我们应该了解了需要知道的有关 Spring 的依赖容器的所有知识。

当然还有很多,但是如果你对 ApplicationContext、Bean、依赖以及依赖注入的不同方式有很好的了解,那么你就已经上道了。

下面我们来看看除了纯粹的依赖注入外,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() { // (1)
        return new UserService();
    }
}
  • (1) 假设 UserService 是一个类,可让你从数据库表中查找用户,或将用户保存到该数据库表中。

这是 Spring 隐藏的杀手级功能出现的地方:Spring 会读入该上下文配置,其中包含我们所写的 @Bean 方法,因而知道如何创建和注入 UserService Bean。

但是 Spring 可以欺骗并创建比 UserService 类更多的东西。它是如何做到的?为什么要这样做呢?

Spring 可以创建代理

因为在背后,任何 Spring @Bean 方法都可以返回看起来和感觉像 UserService(在本例中)但实际上有不是 UserService 的东西。

它可以给你返回一个代理

代理会在某个时候委托给你写的 UserService,但它首先会执行它自己的功能

什么是 Spring 框架:Java 中的依赖注入 - 图1

更具体地说,Spring 默认情况下会创建动态的Cglib 代理,它们不需要接口即可工作(有点像 JDK 的内部代理机制):Cglib 可以通过动态子类化来代理类。(如果不确定各个代理模式,请阅读有关Wikipedia上代理模式的更多信息。)

Spring 为什么要创建代理?

因为它允许 Spring 在不修改代码的情况下为你的 bean 提供其他功能。总的来说,这就是面向切面(或AOP)编程的目的。

下面我们来看看最受欢迎的 AOP 示例:Spring 的 @Transactional 注解。

Spring 的 @Transactional

你上面的 UserService 实现可能看起来像这样:

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class UserService {

    @Transactional           // (2)
    public User activateUser(Integer id) {  // (1)
        // 执行一些 sql
        // 发送一个事件
        // 发送一个电子邮件
    }
}
  • (1) 我们编写了一个 activateUser() 方法,该方法在被调用时需要执行一些 SQL 以更新数据库中用户的状态,例如发送电子邮件。
  • (2) 该方法上的 @Transactional 通知 Spring 你需要一个打开的数据库连接/事务才能使该方法正常工作,并且该事务也应在最后提交。而且 Spring 需要做这些。

问题:尽管 Spring 可以通过 applicationContext 配置创建 UserService bean,但是它无法重写 UserService。它不能简单地在这里注入代码来打开数据库连接,并提交数据库事务。

不过,它可以做的是:围绕你的 UserService 创建一个代理,这个代理是事务的。这样,只有代理需要知道如何打开和关闭数据库连接,然后在中间委托给你的 UserService 就可以了。

下面我们再来看看那个爱管闲事的 ContextConfiguration。

@Configuration
@EnableTransactionManagement // (1)
public class MyApplicationContextConfiguration {

    @Bean
    public UserService userService() { // (2)
        return new UserService();
    }
}
  • (1)我们添加一个注解来通知 Spring:是的,我们需要 @Transactional 支持,它能自动在背后启用 Cglig 代理。
  • (2)有了上述注解设置后,Spring 就不会只是在这里创建和返回你的 UserService 了。它会创建一个你的 Bean 的 Cglib 代理,这个代理看起来会嗅到并委托给你的 UserService,但实际上包装了你的 UserService,并给它提供过了事务管理功能。

这最初似乎有点不直观,但是大多数 Spring 开发人员很快会在调试会话中遇到代理。由于代理的存在,Spring 栈跟踪可能变得很长而且不熟悉:当单步进入一个方法内部时,你很可能首先单步进入了代理,这会让人害怕。不过,这是完全正常和预期的行为。

Spring 必须用 Cglib 代理吗?用真正的字节码织入怎么样?

使用 Spring 实现 AOP 编程时,代理是默认的选择。不过,除了使用代理以外,还可以使用完整的 AspectJ 路由,如果需要的话,可以修改实际的字节码。不过,AspectJ 不在本指南的范围之内。

请参考后面《Spring AOP 和 AspectJ 之间的区别是什么》一节。

Spring 的 AOP 支持:小节

当然,关于面向切面编程还有很多要说的,但是本指南让您了解了最流行的Spring AOP 用处(比如 @Transactional 或 Spring Security 的 @Secured)如何工作。如果需要,你甚至可以编写自己的 AOP 注解。

如果想详细了解 Spring 的 @Transactional 管理的工作原理,请看看 @Transactional 指南。

Spring 的资源

我们一直在谈论依赖注入和代理。现在我们看看 Spring 框架中所谓的重要便捷实用程序。这些实用程序之一是 Spring 的资源支持。

考虑一下你如何尝试在 Java 中通过 HTTP 或者 FTP 访问一个文件。你可以使用 Java 的 URL 类,并编写一些样板代码。

同样,你将如何从应用程序的 classpath 中读取文件?或者从 Servlet 上下文,也就是从 Web 应用程序根目录(当然,在现代用 .jar 打包过的应用程序中这越来越罕见)。

同样,您需要编写相当数量的样板代码才能使其正常工作,不幸的是,每个用例的代码会有所不同(URL、类路径、Servlet上下文)。

但是有一个解决方案:Spring 的资源抽象。它很容易用代码解释。

import org.springframework.core.io.Resource;

public class MyApplication {

    public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass); // (1)

            Resource aClasspathTemplate = ctx.getResource("classpath:somePackage/application.properties"); // (2)

            Resource aFileTemplate = ctx.getResource("file:///someDirectory/application.properties"); // (3)

            Resource anHttpTemplate = ctx.getResource("https://marcobehler.com/application.properties"); // (4)

            Resource depends = ctx.getResource("myhost.com/resource/path/myTemplate.txt"); // (5)

            Resource s3Resources = ctx.getResource("s3://myBucket/myFile.txt"); // (6)
    }
}
  • (1) 与往常一样,需要一个 ApplicationContext 才能开始。
  • (2) 当在一个 ApplicationContext 上调用 getResource() 时,如果该方法的字符串参数是以 classpath: 开头的,那么 Spring 会在应用程序 classpath 上查找资源。
  • (3) 当调用 getResource() 时,如果该方法的字符串参数是以 file: 开头的,那么 Spring 会在硬盘上查找文件。
  • (4) 当调用 getResource() 时,如果该方法的字符串参数是以 https: 开头的,那么 Spring 会在 Web 上查找文件。
  • (5) 没有你没有指定一个前缀,那么它实际上是依赖于你配置的 ApplicationContext 的类型。更多信息请参考这里
  • (6) 这对于 Spring 框架来说并非开箱即用的,但是通过 Spring Cloud 等其他库,您甚至可以直接访问 s3:// 路径。

简而言之,Spring 让我们可以通过一个不错的小语法就能访问资源。资源接口有几个有趣的方法:

public interface Resource extends InputStreamSource {

    boolean exists();

    String getFilename();

    File getFile() throws IOException;

    InputStream getInputStream() throws IOException;

    // ... 其它方法注释掉了。
}

如你所见,它允许你对资源执行最常见的操作:

  • 资源是否存在?
  • 文件名是什么?
  • 获取对实际文件对象的引用。
  • 获取对原始数据(InputStream)的直接引用。

这样,不管资源是在 Web 上、classpath 上,还是硬盘上,我们可以用资源做任何我们想要的操作。

资源抽象看起来像是一个很小的功能,但是当它与 Spring 提供的下一个便捷功能:Properties 结合使用时,它确实会发光。

什么是 Spring 的环境?

任何应用程序的很大一部分都在读取属性,例如数据库用户名和密码、电子邮件服务器配置、付款明细配置等。

这些属性以最简单的形式存在于 .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(); // (1)
           String databaseUrl = env.getProperty("database.url"); // (2)
           boolean containsPassword = env.containsProperty("database.password");
           // etc
    }
}
  • (1) 通过 applicationContext,你总是可以访问当前程序的环境
  • (2) 另一方面,环境允许你访问属性。

那么,环境到底是什么呢?

什么是 Spring 的 @PropertySources?

简而言之,环境由一到多个属性来源组成。例如:

  • /mydir/application.properties
  • classpath:/application-default.properties

(注:环境也是由配置文件组成,即“dev”或“Production”配置文件,但我们在本指南修订版中不会详细介绍配置文件)。

默认情况下,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 {
    // 你的 bean
}

现在就说得通了,为什么我们之前谈到了Spring 的资源。因为这两个功能是齐头并进的。

@PropertySource 注解适用于任何有效的 Spring 配置类,并允许你在 Spring 的资源抽象的帮助下定义新的附加源:记住,它都是关于前缀的: http://file://classpath: 等。

通过 @PropertySources 定义属性很好,但是没有比通过环境访问它们更好的方法了吗? 是的,有。

Spring 的 @Value 注解和 Property 注入

你可以将属性注入到 bean 中,类似于使用 @Autowire 注解注入依赖。但是对于属性,需要用 @Value 注解。

import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;

@Component
public class PaymentService {

    @Value("${paypal.password}")  // (1)
    private String paypalPassword;

     public PaymentService(@Value("${paypal.url}") String paypalUrl) { // (2)
         this.paypalUrl = paypalUrl;
    }
}
  • (1) @Value 注解直接作用在字段上…
  • (2) 或者构造器参数上。

真的没什么大不了的。 每当你用 @Value 注解时,Spring 都会遍历你的(分层)环境,并查找合适的属性 — 如果这样的属性不存在,就抛出一条错误消息。

其它模块

Spring 框架还包含更多模块。下面我们来看一看。

Spring Web MVC

Spring Web MVC,也称为 Spring MVC,是 Spring 的 Web 框架。它允许你构建任何与 Web 相关的内容,从基于 HTML 的网站,到返回 JSON 或 XML 的 REST 风格的 Web 服务。它还支持 Spring Boot 等框架。

数据访问、测试、整合以及语言

Spring框架包含比您到目前为止看到的更方便的实用程序。让我们称它们为模块,不要将这些模块与spring.io上的其他20个Spring项目混淆,这些模块都是Spring框架项目的一部分。

那么,我们说的是什么样的便利呢?

您必须理解,基本上Spring在这些模块中提供的所有功能也都是纯Java版本的。要么由 JDK 提供,要么由第三方库提供。Spring 框架总是建立在这些现有功能的基础上。

这里有一个例子:用 Java 的 Mail API]发送电子邮件附件当然是可行的,但使用起来有点麻烦。参见此处获取代码示例。

Spring在Java的Mail API之上提供了一个很好的小包装器API,另外一个好处是它提供的所有东西都可以很好地与Spring的依赖注入容器混合在一起。 它是Spring的integration模块的一部分。

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; // (1)

    public void sendInvoice(User user, File pdf) throws Exception {
        MimeMessage mimeMessage = mailSender.createMimeMessage();

        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); // (2)

        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);
    }
}
  • (1)Everything related to configuring an email server (url, username, password) is abstracted away into the Spring specific MailSender class, that you can inject in any bean that wants to send emails.
  • (2) Spring offers convenience builders, like the MimeMessageHelper, to create multipart emails from, say, files as fast as possible.

So, to sum it up, Spring framework’s goal is to ‘springify’ available Java functionality, preparing it for dependency injection and therefore making the APIs easier to use in a Spring context.

模块概述

我想快速概述一下您在Spring框架项目中可能会遇到的最常用的工具,功能和模块。 但是请注意,在本指南的范围内不可能对所有这些工具进行详细介绍。请查看[官方文档](https://docs.spring.io/spring/docs/current/spring-framework-reference/index.html)以获取完整列表。

  • Spring 的 Data Access:不要把这个和 Spring Data(JPA/JDBC)库搞混了。它是 Spring @Transactional 支持以及纯 JDBC 和 ORM(比如 Hibernate) 集成的基础。
  • Spring 的 Integration 模块:让发送邮件、与 JMS 或者 AMQP、任务调度等更容易。
  • Spring 表达式语言 SpEL:尽管并不真正正确,但也可以将它看作是用于 Spring Bean 创建、配置、注入的 DSL 或者 Regex。本教程的后续版本会对此详细介绍。
  • Spring 的 Web Servlet 模块:让我们可以编写 Web 应用程序。包括 Spring MVC,但还支持 WebSocket、SockJS 和 STOMP 消息传递。
  • Spring 的 Web Reactive 模块:让我们可以编写响应式 Web 应用程序。
  • Spring 的测试框架:让我们可以(集成)测试 Spring 上下文,从而测试 Spring 应用程序,包括用于测试 REST 服务的辅助工具。

常见问题解答

Spring 框架的最新版本是多少?

当前最新版本是 Spring 5.2.7.RELEASE,在 Spring 官方博客上可以随时看到新版本公告。

我应该用哪个 Spring 版本?

选择哪个版本相对比较简单:

  • 如果是在创建新的 Spring Boot 项目,那么用到的 Spring 版本已经预定义好了。比如,如果用的是Spring Boot 2.2.x,那么就是用Spring 5.2.x(不过,理论上您可以覆盖它)。
  • 如果实在新建项目中使用普通 Spring,那么显然可以选择想要的任何版本,通常是最新的版本。
  • 如果是在旧项目中用 Spring,如果从业务的角度来看是有意义的,那么就可以考虑升级到更新的 Spring 版本。Spring 版本有很高的兼容性。

实际上,你会发现大多数公司正在使用 Spring 版本 4.x-5.x,不过也会冒出来罕见的旧 3.x(2009年发布)Spring 项目。

新版本的Spring多久发布一次? 他们支持多长时间?

下面是一个很好的小图,显示了 Spring 的版本历史:

什么是 Spring 框架:Java 中的依赖注入 - 图2

可以看到,最初的 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 项目,只需要添加如下依赖就可以了,而不是下载 .jar 文件,手动添加到项目。

<!-- Maven -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.7.RELEASE</version>
</dependency>
// Gradle
compile group: 'org.springframework', name: 'spring-context', version: '5.2.7.RELEASE'

对于额外的 Spirng 功能(比如 Spring 的 JDBC 或者 JMS 支持),会需要其它额外的库。

可以在 Spring 的官方文档中找到所有可用模块的一个列表,不过对 Maven 依赖而言,artifactId 确实遵循模块的名称。如下是一个示例:

<!-- Maven -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.7.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.7.RELEASE</version>
</dependency>

Spring 版本之间有什么不同?

与 JVM 类似,Spring 版本也是向后兼容的,也就是说,仍然可以用最新的 Spring 5.0 运行Spring 1.0 XML文件(不过我承认还没试过)。此外,例如从 3 升级到 5 也是有可能的,只需稍加努力(请参阅迁移指南)。

一般来说,较新的 Spring 版本构建在较旧的 Spring 版本之上,并且只有最小的破坏性变化。 因此,在 Spring 3 或 4 中学到的所有核心概念在 Spring 5 中仍然适用。

在这里可以详细了解过去 7 年中各个 Spring 版本的变化:

如下是一个摘要:

核心(依赖注入、事务管理等)总是保持不变或者有扩展。不过,Spring 与时俱进,提供对更新的 Java 语言版本、测试框架增强、WebSocket、响应式编程等的支持。

其他 20 个 Spring.io 项目现在在做什么?

在本指南的范围内,我不能详细介绍所有不同的项目,不过我们来看看最有可能遇到的项目。

  • Spring Boot:可能是最受欢迎的 Spring 项目。Spring Boot 是 Spring 框架的一个强制性的版本。看看“Spring 和 Spring Boot 之间的区别是什么”这一个问题的回答,就知道所谓强制性这个短语是什么意思。
  • Spring Batch:一个帮助我们编写好的旧批处理作业的库。
  • Spring Cloud:一组帮助将 Spring 项目更轻松地与云集成,或者编写微服务的库。
  • Spring Security:一个帮助实现安全的库,比如用 OAuth2 保护 Web应用程序。
  • 还有更多。。。。

摘要:所有这些库都扩展了 Spring 框架,并创建在其依赖注入核心原则之上。

Spring 和 Spring Boot 之间的区别是什么?

如果已经阅读了本指南,那么现在应该理解 Spring Boot 是在 Spring 的基础上创建的。虽然一个全面的 Spring Boot 指南即将问世,但这里有一个 Spring Boot 中的“强制性的默认设置”的例子。

Spring 为我们提供了从不同位置读取 .properties 文件的能力,比如,借助 @PropertySource 注解。在 Web MVC 框架的帮助下,它还提供了编写 JSON REST 控制器的能力。

问题是,我们必须自己编写和配置所有这些单独的部分。而 Spring Boot 则将这些单个组件捆绑在一起。比如:

  • 始终自动在各个地方查找 application.properties 文件,并将其读入。
  • 始终引导嵌入的 Tomcat,这样我们就可以立即看到我们编写的 @RestController 的结果。
  • 自动为我们配置好发送/接收 JSON 的所有内容,而无需担心特定的 Maven/Gradle 依赖。

这些都是通过运行一个 Java 类中的 main 方法实现的,这个类用 @SpringBootApplication 注解进行注解。更棒的是,Spring Boot 提供了 Maven/Gradle 插件,让我们把应用程序打包成一个 .jar 文件中,然后我们就可以按如下方式运行该文件:

java -jar mySpringBootApp.jar

因此,Spring Boot 就是利用现有的 Spring 框架部分,预先配置好,并把它们打包,让所需的开发工作尽可能少。

Spring AOP 和 AspectJ 之间的区别是什么?

正如在《Spring 必须用 Cglib 代理吗?用真正的字节码织入怎么样?》一节中所提到的:默认情况下,Spring 是用基于代理的 AOP。它把 Bean 封装在代理中,实现事务管理等功能。这有几个限制和注意事项,但是对于实现 Spring 开发人员面临的最常见的 AOP 问题来说,这是一种非常简单直接的方式。

而 AspectJ 允许我们通过加载时织入或者编译时织入,修改实际的字节码。这就给了我们更多的可能性,换取更多复杂性。

不过,我们可以配置 Spring 使用 AspectJ 的 AOP,而不是用默认的基于代理的 AOP。

关于这个主题的更多信息,这里有几个链接:

Spring 和 Spring Batch 之间的区别是什么?

Spring Batch是一个框架,可以简化编写批处理作业的过程,比如“在每晚凌晨 3 点读取这 95 个 CSV 文件,并对每个条目调用一个外部验证服务”。

同样,它建立在 Spring 框架的基础上,但它在很大程度上是自身一体。

不过请注意,在没充分理解 Spring 框架的常规事务管理及其与 Spring Batch 的关系的情况下,创建坚如磐石的批处理作业基本上是不可能的。

Spring 和 Spring Web MVC 之间的区别是什么?

如果已读过本指南,那么现在应该已经知道 Spring Web MVC 是 Spring 框架的一部分。

在较高层次上,它允许我们借助路由到 @Controller 类的 DispatcherServlet,将 Spring 应用程序转换为 Web 应用程序。

这些控制器可以是 RestController(将 XML 或 JSON 发送到客户端),也可以是旧的 HTML 控制器,我们可以在其中使用 Thymeleaf、Velocity 或 Freemarker 等框架生成 HTML。

Spring 和 Struts 之间的区别是什么?

这个问题实际上应该是:Spring Web MVC 和 Struts 有什么区别?

简短的历史答案是:Spring Web MVC 最初是 Struts 的竞争对手,据称 Struts 被 Spring 开发人员认为设计的不咋的。

现代的答案是,尽管有些旧项目中还在用Struts 2,但 Spring Web MVC 是与 Spring 领域与 Web 相关的一切内容(从 Spring Webflow 到 Spring Boot 的 RestController)的基础。

Spring XML 配置、注解配置和 Java 配置哪个更好?

Spring 最初只提供了 XML 配置。然后,慢慢地,越来越多的注解和 Java 配置功能出现了。

现在,你会发现 XML 配置主要用于较老的遗留项目中,新项目都会用基于 Java 或者基于注解的配置。

不过,请注意两件事情:

  1. 基本上没有什么可以阻止你在同一项目中组合 XML、注解和 Java Config,这通常会导致混乱。
  2. 争取做到 Spring 配置同质化,即不要随意用 XML生成一些配置,用 Java config 生成一些配置,用组件扫描生成一些配置。

构造器注入和字段注入哪个更好?

正如在依赖注入一节中所提到的,这是一个引起了很多争论的问题。最重要的是,你的选择应该是在整个项目中保持一致:不要一会儿用构造器注入,一会儿用字段注入。

最明智的方式是用在 Spring 官方文档中推荐的方式:对强制依赖使用构造器注入,对可选依赖使用 Setter/字段注入,然后在整个类中对这些可选依赖做空检查。

最重要的是,请记住:软件项目的总体成功不会取决于你最喜欢的依赖注入方式的选择。

Spring 的依赖注入容器有替代方案吗?

有的,Java 生态圈有两种流行的替代方案:

请注意,Dagger 只提供依赖注入,没有其他便利功能。Guice 提供依赖注入和其他功能,例如事务管理(在 Guice Persist的帮助下)。

结束

如果你已经读到这里,那么现在应该对 Spring 框架有一个相当全面的了解。

在后续指南中你会找到如何将其连接到其他 Spring 生态系统库(例如 Spring Boot 或 Spring Data),但是现在我希望你在尝试回答什么是Spring框架这个问题时牢记这个比方:

假设你要装修一个房子(〜=创建一个软件项目)。

Spring 框架就是你的 DIY 商店(~= 依赖注入容器),它为装修提供了大量工具,从煤气灯(~= 资源 / 属性),到大锤(~= Web MVC)。这些工具只是帮助你更快更方面地装修你的房子(~= 创建你的 Java 应用程序)。