# 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 容器主要是基于 BeanFactory 和 ApplicationContext 两个接口:
BeanFactory 是 Spring IoC 容器的顶层接口,它是整个 Spring IoC 容器体系的“老祖宗”。
“老祖宗”接口中定义了我们未来最常用、最关注的方法:getBean 方法。
ApplicationContext 是最常用接口。
ClassPathXmlApplicationContext 是 ApplicationContext 的实现类之一。顾名思义,它从 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 的整体流程如下:
准备好一个配置类(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; } }
创建 AnnotationConfigApplicationContext 对象(它就是我们口头所说的 Spring IoC 容器),并将上述的配置类作为参数传递给它。
在背后发生了这样的一件事情:Spring IoC 容器会去调用,你上述的配置类中的标注了 @Bean 的方法。它只调用一次,并将这些方法的返回值(各个对象的引用)保存起来。
毫无疑问,这个对象必定就是单例的。
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(YyyConfig.class);
根据我们自己的需要,你可以向 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-name 、url 、username 、password 这些单词太常见了。
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 有两个重要属性:name 和 type:
name 属性解析为 JavaBean 的名字
type 属性则解析为 JavaBean 的类型
因此, name 属性和 type 属性两者同时出现,或同时不出现,亦或者出现一个,就意味着不同的『注入规则』,也就分成了 4 种不同情况:
如果同时指定了
name
和type
,则从 IoC 容器中查找同时匹配这两个条件的 Bean 进行装配,找不到则抛出异常。注意,type 和 name 两个条件是『且』的关系。
// Spring 在 IoC 容器中查找类型是 DaoDao,且名字是 catDao 的 JavaBean // 来为 animalDao 属性赋值。 @Resource(type = DogDao.class, name = "catDao") private AnimalDao animalDao;
如果只指定了
name
,则从 IoC 容器中查找 name 匹配的 Bean 进行装配,找不到则抛出异常。// Spring 在 IoC 容器中查找名字是 catDao 的 JavaBean // 来为 animalDao 属性赋值。 @Resource(name = "catDao") private AnimalDao animalDao;
如果只指定了
type
,则从 IoC 容器中查找 type 匹配的 Bean 进行装配,找不到或者找到多个,都会抛出异常。// Spring 在 IoC 容器中查找类型是 DogDao 的 JavaBean 来为 animalDao 属性赋值。 @Resource(type = DogDao.class) private AnimalDao animalDao;
如果既没有指定 name ,又没有指定 type ,则先以 name 为依据在 IoC 容器中查找,如果没有找到,再以 type 为依据在 IoC 容器中查找。
这种情况下,类型和名字不是『且』的关系,而是『或』的关系。
// Spring IoC 先在容器中查找名字为 animalDao 的 JavaBean 来为 animalDao 属性赋值。 // 如果没有找到,Spring IoC 再在容器中查找类型为 AnimalDao 的 JavaBean 来为 animal 属性赋值。 @Resource private AnimalDao animalDao;
# 7. 三种注入方式的使用技巧
『基于字段的依赖注入』方式有很多缺点,我们应当避免使用基于字段的依赖注入。
推荐的方法是使用『基于构造函数的依赖注入』方式和『基于 setter 方法的依赖注入』方式。
对于『必需的』依赖项,建议使用基于构造函数的注入,以使它们成为不可变的,并防止它们为 null 。
对于『可选的』依赖项,建议使用基于 Setter 方法的注入。