# Spring IoC 基础

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version> <!-- 5.1.17.RELEASE -->
</dependency>

# 1. 代码配置和 xml 配置

  • Spring 从 2.x 开始,提供注解,用以简化 XML 配置文件配置

  • Spring 从 3.x 开始,基于注解功能,『提供』了全新的配置方式:Java 代码配置方式。

  • 4.x 时代,Spring 官方『推荐』使用 Java 代码配置,以完全替代 XML 配置,实现零配置文件。

  • 到了 Spring Boot 时代,Spring 官方甚至直接『要求』使用 Spring 的代码配置方式进行配置。

实际上无论是从实际使用的灵活性、方便性,还是从官方的态度,都应该优先使用 Java 代码配置方式。

# 2. 实例化 Spring IoC 容器

Spring 核心容器的理论很简单:Spring 核心容器就是一个超级大工厂,所有的单例对象都由它创建并管理

你必须创建、实例化 Spring IoC 容器,读取其配置文件来创建 Bean 实例。然后你可以从 Spring IoC 容器中得到可用的 Bean 实例。

BeanFactory  “老祖宗”接口
└── ApplicationContext  最常用接口
    └── AbstractApplicationContext  接口的抽象实现类
        ├── ClassPathXmlApplicationContext  具体实现类之一
        └── AnnotationConfigApplicationContext  具体实现类之二

Spring IoC 容器主要是基于 BeanFactoryApplicationContext 两个接口:

  • BeanFactory 是 Spring IoC 容器的顶层接口,它是整个 Spring IoC 容器体系的“老祖宗”。

    “老祖宗”接口中定义了我们未来最常用、最关注的方法:getBean 方法。

  • ApplicationContext 是最常用接口。

  • ClassPathXmlApplicationContextApplicationContext 的实现类之一。顾名思义,它从 classpath 中加载一个或多个 .xml 配置文件,构建一个应用程序上下文。

    ApplicationContext context = new ClassPathXmlApplicationContext("aaa.xml");
    ApplicationContext context = new ClassPathXmlApplicationContext("bbb.xml", "ccc.xml");
    
  • AnnotationConfigApplicationContext 也是 ApplicationContext 的实现类之二。不过它需要的是一个配置类或多个配置类,而非配置文件。

    ApplicationContext context = new AnnotationConfigApplicationContext(Xxx.class);
    ApplicationContext context = new AnnotationConfigApplicationContext(Yyy.class, Zzz.class);
    

在获得『应用程序上下文(也就是 IoC 容器)后,你只需要调用 getBean 方法并传入唯一的 Bean ID 和 Bean 的 Class 对象,就可以获得容器中的 Bean 。

// 大多数情况下 id 非必须
Human tom = context.getBean("tom", Human.class);
// 或者
Human tom = context.getBean(Human.class);

使用 Java 代码进行 Java Bean 的配置远比使用 XML 配置文件进行要简单很多,因为进行配置的『思维模式』发生了变化:

  • 使用 XML 进行配置,你要面面俱到地『告知』Spring 你要如何如何地去创建、初始化一个 Bean 。

  • 使用 Java 代码进行配置,你只需要提供一个方法,你自己全权(程序员)负责方法的具体实现,并在方法的最后返回一个 Java Bean, Spring 不关心你的方法的实现内容和细节,它只保证未来调用你所写的方法,且只调用一次

在这种思路下,XML 配置的『很多情况』在 Java 代码中就被统一成了『一种情况』,因此变得更简洁。

# 3. Spring 通过 Java 代码配置 Java Bean

通过 Java 代码配置 Java Bean 的整体流程如下:

  1. 准备好一个配置类(XxxConfig)。其中写一个或多个方法,每个方法负责返回你的项目中的(逻辑上的)单例对象。

    至于你的项目中是有多少个单例对象,那就需要你自己去分析、去设计。

    例如:

    public class XxxConfig {
      
        @Bean
        public DataSource dataSource() throws Exception {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/scott?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false");
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUsername("root");
            dataSource.setPassword("123456");
    
            return dataSource;
        }
    }
    
  2. 创建 AnnotationConfigApplicationContext 对象(它就是我们口头所说的 Spring IoC 容器),并将上述的配置类作为参数传递给它。

    在背后发生了这样的一件事情:Spring IoC 容器会去调用,你上述的配置类中的标注了 @Bean 的方法。它只调用一次,并将这些方法的返回值(各个对象的引用)保存起来。

    毫无疑问,这个对象必定就是单例的。

    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(YyyConfig.class);
    
  3. 根据我们自己的需要,你可以向 Spring IoC 容器要( context.getBean(XXX.class) )上述的配置类中的这么些个单例对象。

    在获得这些单例对象之后,你要干什么,就是你自己的事情了。

    DataSource ds = context.getBean(DataSource.class);
    Connection connection = ds.getConnection();
    ...
    

# 创建对象的 3 种方式

再次强调,以何种方式创建对象(包括如何赋值,赋什么值)这是程序员自己考虑的事情,Spring IoC 并不关心。它只关心、关注你所提供的方法的返回值(对象)

创建 Bean ,常见的方式常见 3 种:

# 方式
1 类自身的构造方法
2 工厂类提供的工厂方法
3 工厂对象提供的工厂方法

在 Spring 的代码配置中,你自己决定使用何种方式创建对象并返回:

  • 通过类自身的构造方法

    @Bean
    public DataSource dataSource() throws Exception {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/scott?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
    
        return dataSource;
    }
    
  • 工厂类提供的工厂方法

    @Bean
    public DataSource dataSource() throws Exception {
        Properties properties = new Properties();
        properties.setProperty("driver", "com.mysql.cj.jdbc.Driver");
        properties.setProperty("url", "jdbc:mysql://127.0.0.1:3306/scott?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false");
        properties.setProperty("username", "root");
        properties.setProperty("password", "123456");
    
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
    
        return dataSource;
    }
    
  • 工厂对象提供的工厂方法

    略。

# Java Bean 的属性的赋值

在 Java 代码配置中,和『如何创建对象是程序员的“家务事”,Spring 并不关心』一样,『以何种方式(有参构造 or Setter)为对象的属性赋值,以及赋何值也是程序员的“家务事”』,Spring 也不关心。

上述 @Bean 方法所返回的 Java Bean,对 Spring 而言,其属性有值,就有值,没有值,就没有值;是这个值,就是这个值,是那个值就是那个值。

在 XML 方式的配置中,为 Java Bean 赋初值的配置要啰嗦的多得多。

# 引用类型的属性的赋值

大多数情况下,在 Java 代码的配置中,为对象的属性赋值都比较直接。但是在 Spring 的容器中,Java Bean 可能会存在引用。

即,一个 Spring 容器中的 Java Bean 的某个属性的值是容器中的另一个 Java Bean 的引用。

在 Java 代码配置中,有多(3)种方式来配置 Java Bean 的引用关系,这里推荐使『通过参数表示引用关系』:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>${hikaricp.version}</version> <!-- 3.2.0 -->
</dependency>
@Bean
public HikariConfig hikariConfig() {
    HikariConfig config = new HikariConfig();
    config.setDriverClassName("com.mysql.cj.jdbc.Driver");
    config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/scott?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false");
    config.setUsername("root");
    config.setPassword("123456");

    return config;
}

@Bean
public DataSource dataSource(HikariConfig config) {
    DataSource dataSource = new HikariDataSource(config);
    return dataSource;
}

# 循环引用

如果 Spring 容器中的两个对象,相互引用,那么会遇到循环引用问题:

  • 类定义:

    public class Husband {
      private Wife wife;
      ... 
    }
    
    public class Wife {
        private Husband husband;
        ...
    }
    
  • 常规配置:

    @Bean
    public Husband husband(Wife wife) { ... }
    
    @Bean
    public Wife wife(Husband husband) { ... }
    

解决这个问题的办法是对两个 Bean 中的其中一个使用 @Lazy 注解:

@Bean
public Husband husband(@Lazy Wife wife) { ... }

@Bean
public Wife wife(Husband husband) { ... }

通过 @Lazy 注解,Spring 生成并返回了一个 Wife 的代理对象,因此给 Husband 注入的 Wife 并非真实对象,而是其代理,从而顺利完成了 Husband 实例的构造(而不是报错);而 Wife 依赖的 Husband 是直注入完整的 Husband 对象本身。因此,这里通过 @Lazy 巧妙地避开了循环依赖的发生。

# 4. 简化配置

如果我们的项目中有几十个 Java Bean 要配置,那么就需要我们去编写几十个 @Bean 方法。很显然,这是很麻烦的事情。

为此,Spring 提供了几个注解来简化我们的配置。

# @Component 注解

@Component 注解用于标注于 Bean 的类上。凡是被标注了该注解的类(只要在扫描路径下)都会被 Spring 创建。

@Component 注解有唯一的属性 value 属性。它用来为 Bean 命名。

@Component 注解有三个语义化的子注解:

语义化子注解 用处
@Repository 用于持久层
@Service 用于业务层
@Controller 用于 Web 层

@Component 注解要结合 @ComponentScan 注解使用:

@ComponentScan(basePackages = "com.example")
public class ApplicationConfig {
}

# @Configuration 注解

@Configuration 专用于标注于我们的配置类(XxxConfig)上。

@Configuration
public class YyyConfig {
    ...
}

它有 2 个作用:

  • 逻辑上,它可以用来标识『这个类是个配置类』。

  • 它会导致 Spring IoC 容器将这个配置类的对象,打入到 Spring IoC 容器的管理范畴内。

    简单来说,这样一来 Spring IoC 容器中会『多』出来一个单例对象:YyyConfig 对象。

# @Value 注解和 @PropertySource 注解

@Value 注解用于标注于『简单类型』属性上。凡是被标注了该注解的属性都会被 Spring 注入值(赋值)

@Value 注解有唯一的属性 value 属性。它用来为简单属性指定值。


@PropertySource 可以配合 @Value 来简化对简单类型的属性的赋值。

@PropertySource 除了可以直接用在 @Component 上,也可以用在配置类上。

  • jdbc.properties

    xxx.yyy.zzz.driver-class-name=com.mysql.cj.jdbc.Driver
    xxx.yyy.zzz.url=jdbc:mysql://127.0.0.1:3306/scott\
      ?useUnicode=true\
      &characterEncoding=utf-8\
      &useSSL=false\
      &serverTimezone=UTC
    xxx.yyy.zzz.username=root
    xxx.yyy.zzz.password=123456
    

    注意,这里有个和本知识点无关的小细节:需要有前缀,否则会因为命名冲突导致问题。因为, driver-class-nameurlusernamepassword 这些单词太常见了。

  • Java Bean

    @PropertySource("classpath:jdbc.properties")   // 看这里,看这里,看这里
    public class ZzzConfig{
    
        @Value("${xxx.yyy.zzz.driver-class-name}")
        private String driver;
    
        @Value("${xxx.yyy.zzz.url}")
        private String url;
    
        @Value("${xxx.yyy.zzz.username}")
        private String username;
    
        @Value("${xxx.yyy.zzz.password}")
        private String password;
    
        ...
    }
    

# @Autowired 注解

@Autowired 注解用于标注于『引用类型』属性上。凡是被标注了该注解的属性都会被 Spring 以『类型』为依据注入另一个 Bean 的引用。

@Autowired 注解有唯一的属性 required 属性(默认值为 true。它用来指示该对该属性的注入是否为必须(默认为 必须,即,在 Spring IoC 容器中没有发现符合类型的其它 Bean 时,会抛出异常。

# @Qualifier 注解

@Qualifier 注解需要结合 @Autowired 注解使用。它用于标注于引用类型属性上。凡是被标注了该注解的属性都会被 Spring 以『名字』为依据注入另一个 Bean 的引用。

@Qualifier 注解有唯一的属性 value 属性。它用于指示需要注入的另一个 Bean 的名字。

一个小细节:包扫描的 Bean 会早于配置的 bean 先创建。

# 5. @Component 的无参构造和有参构造

如果你使用了 @Component 的 Java Bean 中有无参的构造器,或包括无参构造器在内的多个构造器,那么:

Spring 是使用你的『无参构造器』来创建对象,(此时对象的各个属性还没有值),然后再通过『反射』对各个属性赋值。

如果你的类的构造器『只有有参构造器』,而没有无参的构造器,那么,Spring 会调用你有参的构造器去创建这个对象,并同时完成对其属性的赋值。此后,Spring 不再另外对你的属性赋值。

Spring 官方推荐使用有参构造器创建并初始化对象。如果遇到循环依赖问题,使用前面所说的 @Lazy 解决。

# 6. JSR-250 的 @Resource

Spring 不但支持自己定义的 @Autowired 注解,还支持几个由 JSR-250 规范定义的注解,它们分别是 @Resource、@PostConstruct 以及 @PreDestroy 。

简单来说, @Resource 一个人就能实现 @Autowired@Autowired + @Qualifier 两种功能。

@Resource 有两个重要属性:nametype

  • name 属性解析为 JavaBean 的名字

  • type 属性则解析为 JavaBean 的类型

因此, name 属性和 type 属性两者同时出现,或同时不出现,亦或者出现一个,就意味着不同的『注入规则』,也就分成了 4 种不同情况:

  1. 如果同时指定了 nametype ,则从 IoC 容器中查找同时匹配这两个条件的 Bean 进行装配,找不到则抛出异常。

    注意,type 和 name 两个条件是『』的关系。

    // Spring 在 IoC 容器中查找类型是 DaoDao,且名字是 catDao 的 JavaBean 
    // 来为 animalDao 属性赋值。
    @Resource(type = DogDao.class, name = "catDao") 
    private AnimalDao animalDao;
    
  2. 如果只指定了 name ,则从 IoC 容器中查找 name 匹配的 Bean 进行装配,找不到则抛出异常。

    // Spring 在 IoC 容器中查找名字是 catDao 的 JavaBean 
    // 来为 animalDao 属性赋值。
    @Resource(name = "catDao") 
    private AnimalDao animalDao;
    
  3. 如果只指定了 type ,则从 IoC 容器中查找 type 匹配的 Bean 进行装配,找不到或者找到多个,都会抛出异常。

    // Spring 在 IoC 容器中查找类型是 DogDao 的 JavaBean 来为 animalDao 属性赋值。
    @Resource(type = DogDao.class)
    private AnimalDao animalDao;
    
  4. 如果既没有指定 name ,又没有指定 type ,则先以 name 为依据在 IoC 容器中查找,如果没有找到,再以 type 为依据在 IoC 容器中查找。

    这种情况下,类型和名字不是『且』的关系,而是『』的关系。

    // Spring IoC 先在容器中查找名字为 animalDao 的 JavaBean 来为 animalDao 属性赋值。
    // 如果没有找到,Spring IoC 再在容器中查找类型为 AnimalDao 的 JavaBean 来为 animal 属性赋值。
    @Resource
    private AnimalDao animalDao;
    

# 7. 三种注入方式的使用技巧

『基于字段的依赖注入』方式有很多缺点,我们应当避免使用基于字段的依赖注入。

推荐的方法是使用『基于构造函数的依赖注入』方式和『基于 setter 方法的依赖注入』方式。

  • 对于『必需的』依赖项,建议使用基于构造函数的注入,以使它们成为不可变的,并防止它们为 null 。

  • 对于『可选的』依赖项,建议使用基于 Setter 方法的注入。