控制反转和依赖注入

学Spring的时候,接触到两个比较重要的概念,一个是IoC(Inversion of Control),控制反转;一个是DI(Dependency Injection),依赖注入。

工厂和单例解耦

在一个简易的后端项目中,处理三层架构最简单的方式自然就是视图层中new业务层的对象,业务层中又new持久化层的对象。但这样下来,耦合度很高,因为这样导致了编译时需要处理各类之间的依赖关系。一个最直接的解耦方式,就是利用读取配置文件,再进行反射,在运行时创建实例对象来取代new,这样能够剥离各层之间的依赖关系。

首先将需要反射的全类名存储在一个简易的properties文件中:

1
2
service=org.example.service.impl.ServiceImpl
dao=org.example.dao.impl.DaoImpl

配置文件写好了,现在我们需要去读取这个配置文件,然后通过反射来进行实例创建。此过程最好放置在一个工厂类中,通过工厂模式来进一步解耦。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyBeanFactory {
private static Properties props;

static {
props = new Properties();
try {
InputStream in = MyBeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
} catch (Exception e) {
throw new ExceptionInInitializerError("Fail to load properties files.");
}
}
}

上述代码中,在调用Properties对象的load方法之前,并没有通过指定一个路径来创建InputStream对象,而是通过MyBeanFactory.class.getClassLoader().getResourceAsStream()来获取。因为对于properties文件,我们一般是放在resources目录下,但若是直接指定路径,在项目部署后,是无法通过指定resources路径来获取到文件的。

之后,我们在MyBeanFactory类中添加一个创建实例的方法来完成我们的工厂类:

1
2
3
4
5
6
7
8
9
10
public static Object getBean(String beanName) {
String path = props.getProperty(beanName);
Object bean = null;
try {
bean = Class.forName(path).getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}

现在,我们就可以用MyBeanFactory.getBean()来代替各层中的new了。

但还有一个问题,假如我们在视图层内创建了多次某个业务层的Bean,这样创建出来的这些Bean是多例状态,有时我们希望这些创建出来的Bean是同一个Bean对象,这时我们就需要引入单例模式。

思路很简单,在MyBeanFactory中添加一个哈希表,来存储各个properties文件中的value值所对应的实例对象,之后的getBean()函数,不再进行实例创建,而是直接从这个哈希表中进行查找。下面是更改后的MyBeanFactory,为了方便,简单写了一个饿汉式单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyBeanFactory {
private static Properties props;
private static HashMap<String, Object> beans;

static {
props = new Properties();
try {
InputStream in = MyBeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);

// Singleton
beans = new HashMap<>();
Enumeration keys = props.keys();
while(keys.hasMoreElements()) {
String key = keys.nextElement().toString();
String path = props.getProperty(key);
beans.put(key, Class.forName(path).getDeclaredConstructor().newInstance());
}
} catch (Exception e) {
throw new ExceptionInInitializerError("Fail to load properties files.");
}
}

public static Object getBean(String beanName) {
return beans.getOrDefault(beanName, null);
}
}

控制反转

现在来说正题,IoC。IoC(Inversion of Control),控制反转,是Spring框架的核心之一。在上述例子中,我们将new替换成了利用MyBeanFactory来创建实例,以业务层中创建持久化层的对象的具体代码为例,我们可以将

1
Dao dao = new DaoImpl();

替换成

1
Dao dao = (DaoImpl)MyBeanFactory.getBean("dao");

在第一种写法中,我们创建实例对象的控制权仍处于我们的应用中,这使得各模块之间的依赖变强,大大增加了耦合度。

1
2
3
4
5
6
7
8
9
                  +----------+
+---> | Resource |
| +----------+
+-----+ | +----------+
| App | --------> | Resource |
+-----+ | +----------+
| +----------+
+---> | Resource |
+----------+

而在第二种写法中,我们将控制权交给了工厂,工厂会自动创建对应的实例并返回给应用。

1
2
3
4
5
6
7
8
9
                                        +----------+
+---> | Resource |
| +----------+
+-----+ +---------+ | +----------+
| App | <-------> | Factory | --------> | Resource |
+-----+ +---------+ | +----------+
| +----------+
+---> | Resource |
+----------+

下面是维基百科中对于控制反转的定义:

In software engineering, inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework.

控制反转的核心在于将创建对象的控制权交给框架而非具体组件。很明显,IoC是一种DIP原则的实现思路。

依赖注入

依赖注入,Dependency Injection(DI),是一种IoC的具体实现方式。

IoC是一种基于DIP原则的思路,将实例的控制权全部交给了框架,应用组件本身不会去创建管理实例。而DI,为IoC提供了一种具体的方法:现在各组件之间的依赖全权由框架负责,框架通过注入的方式,将各组件所需的资源提供给该组件。依赖注入大大增强了各模块可重复使用性,使得整个应用更加灵活,因为各组件之间不再存在强依赖,依赖是框架注入给组件的,组件本身不需要管理依赖。而且若需要更改依赖的注入,只需对配置文件稍作更改即可,无需任何代码。

关于DI和IoC,两者总是有些含糊不清,容易混淆在一起。简单来讲,两者是描述的同一个思想的不同角度。就Spring来讲,Spring框架会提供IoC容器,类似于第一部分我们手撕出来的工厂,他负责的是Bean的生命周期控制。这就是IoC,描述了一种将Bean的生命周期管理全权交给IoC容器的思想。但是,IoC究竟要怎么去创建一个Bean呢,这就需要DI了。DI描述的是框架究竟该通过何种方式去创建这个实例。

Spring中基于XML的IoC容器

基本xml配置

第一部分中我们手撕了一个MyBeanFactory来实现控制的反转,但是在Spring框架中,已经集成了相关功能。Spring框架中的配置文件使用的不是properties而是xml,我们需要在resources文件夹下创建一个xml文件来作为我们的配置文件,具体格式如下,也可以参照Spring的官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions go here -->

</beans>

例如我需要根据我的各个dao对象来对xml文件进行配置,只需创建一个dao.xml,然后在其中进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountDao" class="org.example.dao.impl.AccountDaoImpl">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<bean id="itemDao" class="org.example.dao.impl.ItemDaoImpl">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions for data access objects go here -->

</beans>

在使用的时候,我们需要首先获取IoC容器对象:

1
ApplicationContext ac = new ClassPathXmlApplicationContext("dao.xml");

这个容器对象就相当于第一部分我们手撕的工厂类,同时,此容器对象中同样提供了getBean()方法。

Bean创建方法

上面这种<bean id="itemDao" class="org.example.dao.impl.ItemDao"></bean>标签只能根据默认构造函数进行实例创建。(注意,JavaBean规范中要求必须提供一个无参数的默认构造函数)若此时我们有一个外部jar包,假设其中有一个工厂类,我们需要根据工厂类中的特定方法来获取实例对象,可以用下面的标签:

1
2
<bean id="instanceFactory" class="org.example.factory.InstanceFactory"></bean>
<bean id="accountDao" factory-bean="instanceFactory" factory-method="getAccountDao"></bean>

这样,我们就可以通过InstanceFactory这个类中的getAccountDao方法来获取到我们需要的AccountDao这个对象了。

若此时的外部工厂类中的获取特定实例对象的方法是一个静态方法,我们可以用下面的标签来进行代替:

1
<bean id="accountDao" class="org.example.factory.StaticFactory" factory-method="getAccountDao"></bean>

scope属性

<bean>标签还有一个scope属性,此属性用于描述Bean的作用范围,取值有5个:

  • singleton:单例,缺省值
  • prototype:多例
  • request:web应用的请求范围
  • session:web应用的会话范围
  • global-session:若存在负载均衡,作用于整个集群的会话范围。若无集群,等同于session。

Bean生命周期

ApplicationContext创建的Bean无非两种,单例和多例。

对于单例对象,随着容器创建而创建,随着容器销毁而销毁。生命周期同容器的生命周期。

对于多例对象,总是延时创建的,也就是说,容器创建的时候对象并不会被创建,只有需要使用该对象的时候才会被创建。并且也不会随着容器销毁而销毁,只要被使用,就会一直存在。当不再有引用时,被gc回收。

Spring中基于XML的DI

注入的方式大体有两种:

  • 构造函数注入
  • set方法注入

能够进行注入的对象大体有以下几种:

  • 基本类型的包装类和String
  • 配置过的Bean
  • 集合类

构造函数注入

假设我们现在有一个类,(随便写的一个类,没有什么具体含义),他有一个三个参数的构造函数。

1
2
3
4
5
6
7
8
9
10
11
public class Clazz {
private Integer num;
private String str;
private Date date;

public Clazz (Integer num, String str, Date date) {
this.num = num;
this.str = str;
this.date = date;
}
}

现在我们要通过注入来根据这个构造函数创建实例。在Spring的xml配置文件中,提供了一个<constructor-arg>标签来进行构造函数注入的相关配置。此标签需用在<bean>标签下:

1
2
3
<bean id="exampleClass" class="org.example.Clazz">
<constructor-arg></constructor-arg>
</bean>

之后,我们需要通过该标签的相关属性,来进行参数的传递。<constructor-arg>有如下几个属性:

  • type:指定注入数据的类型,与构造函数中的参数类型对应。
  • index:指定注入数据在构造函数的参数列表中的位置,从0开始。
  • name:指定注入数据在构造函数的参数列表中的名称。
  • value:注入数据的具体值。均为字符串,Spring会根据构造函数来进行具体的类型转换。
  • ref:用于指定其他Bean类型。

可见,type,index和name是用来指定注入哪个参数的。但type是无法唯一确定需要注入的参数的,因为一个构造函数中可能存在多个同类型的参数。而name是可以唯一指定的,因为参数名称是无法重复的。所以一般来讲,只用name属性就足够进行参数指定了。

1
2
3
4
5
<bean id="exampleClass" class="org.example.Clazz">
<constructor-arg name="num" value="6"></constructor-arg>
<constructor-arg name="str" value="hello"></constructor-arg>
<constructor-arg name="date" value="2020-01-01"></constructor-arg>
</bean>

假如我们进行上述的注入配置,在运行之后,会生成报错:

1
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'exampleClass' defined in class path resource [dao.xml]: Unsatisfied dependency expressed through constructor parameter 2: Could not convert argument value of type [java.lang.String] to required type [java.util.Date]: Failed to convert value of type 'java.lang.String' to required type 'java.util.Date';

可见,Spring成功将字符串"6"转换成了整型6,但是无法将日期字符串转换成日期对象。因为日期对象并非基本类型的包装对象,Spring自然无法自行转换。这时我们需要额外配置一个Date类型的Bean来进行指代,这时候我们的ref属性就派上用场了:

1
2
3
4
5
6
<bean id="exampleClass" class="org.example.Clazz">
<constructor-arg name="num" value="6"></constructor-arg>
<constructor-arg name="str" value="hello"></constructor-arg>
<constructor-arg name="date" ref="exampleDate"></constructor-arg>
</bean>
<bean id="exampleDate" class="java.util.Date"></bean>

如果需要创建一个特定日期的Date对象的话,可以使用上面Bean的创建方法中提到的利用指定方法来创建一个Bean对象,此处我们可以用SimpleDateFormat中的parse方法。这时会比较麻烦,需要在<constructor-arg>内再套一个<bean>标签,并在这个<bean>标签内使用<constructor-arg>来进行给parse方法传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="exampleClass" class="org.example.Clazz">
<constructor-arg name="num" value="6"></constructor-arg>
<constructor-arg name="str" value="hello"></constructor-arg>
<constructor-arg name="date">
<bean factory-bean="exampleDate" factory-method="parse">
<constructor-arg value="2020-01-01"></constructor-arg>
</bean>
</constructor-arg>
</bean>

<bean id="exampleDate" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd"></constructor-arg>
</bean>

setter注入

顾名思义,setter注入无非就是给构造函数的参数分别各写一个set方法,然后通过set方法注入。对于上述的Clazz类,我们写如下几个setter:

1
2
3
4
5
6
7
8
9
10
11
public void setNum(Integer num) {
this.num = num;
}

public void setStr(String str) {
this.str = str;
}

public void setDate(Date date) {
this.date = date;
}

这时,在<bean>标签内,需要额外添加<property>标签,每一个<property>标签对应一个setter。<property>标签的属性有如下三个:

  • name:注入时调用的setter名称。具体的属性值是setter名称去掉’set’,且后面第一个大写字母均变为小写。例如,setUserName()对应的属性值为’userName’。
  • value:注入数据的具体值。均为字符串,Spring会根据构造函数来进行具体的类型转换。
  • ref:用于指定其他Bean类型。

除了name用法不同,value和ref用法同构造函数注入。

1
2
3
4
5
6
<bean id="exampleClass" class="org.example.Clazz">
<property name="num" value="6"></property>
<property name="str" value="hello"></property>
<property name="date" ref="exampleDate"></property>
</bean>
<bean id="exampleDate" class="java.util.Date"></bean>

很明显,相比构造函数注入,setter注入要灵活的多,它不需要一次性注入全部的变量就可以创建实例。但是它也有弊端,setter注入是无法保证获取到的对象中每个属性都有值。

注入集合类

对于一些常用的集合类,例如ArrayListMapSetProperties。他们的注入需要额外的标签,而且只能用构造器注入和setter注入。

现在我们在Clazz类中加入这些类型的属性变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Clazz {
private String[] strArray;
private List<String> strList;
private Map<String, String> strMap;
private Set<String> strSet;
private Properties props;

public String[] getStrArray() {
return strArray;
}

public List<String> getStrList() {
return strList;
}

public Map<String, String> getStrMap() {
return strMap;
}

public Set<String> getStrSet() {
return strSet;
}

public Properties getProps() {
return props;
}
}

由于构造函数注入和setter注入大同小异,下面只以setter注入为例。

对于这些复杂的集合类的注入,我们需要使用<property><constructor-arg>下的一些子标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<bean id="exampleClass" class="org.example.Clazz">
<!-- 数组注入 -->
<property name="strArray">
<array>
<value>A</value>
<value>B</value>
</array>
</property>
<!-- List注入 -->
<property name="strList">
<list>
<value>C</value>
<value>D</value>
</list>
</property>
<!-- Set注入 -->
<property name="strSet">
<set>
<value>E</value>
<value>F</value>
</set>
</property>
<!-- Map注入,两种方式 -->
<property name="strMap">
<map>
<entry key="key1" value="G"></entry>
<entry key="key2">
<value>H</value>
</entry>
</map>
</property>
<!-- Propert注入,与Map不同,只有一种方式 -->
<property name="props">
<props>
<prop key="key1">I</prop>
<prop key="key2">J</prop>
</props>
</property>
</bean>

简单地打印一下结果:

1
2
3
4
5
Array: [A, B]
List: [C, D]
Set: [E, F]
Map: {key1=G, key2=H}
Properties: {key1=I, key2=J}

我们对xml做一下手脚,将数组、List和Set的标签顺次替换,将Map和Properties的标签交换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<bean id="exampleClass" class="org.example.Clazz">
<property name="strArray">
<set>
<value>E</value>
<value>F</value>
</set>
</property>
<property name="strList">
<array>
<value>A</value>
<value>B</value>
</array>
</property>
<property name="strSet">
<list>
<value>C</value>
<value>D</value>
</list>
</property>
<property name="strMap">
<props>
<prop key="key1">I</prop>
<prop key="key2">J</prop>
</props>
</property>
<property name="props">
<map>
<entry key="key1" value="G"></entry>
<entry key="key2">
<value>H</value>
</entry>
</map>
</property>
</bean>

再打印一下结果:

1
2
3
4
5
Array: [E, F]
List: [A, B]
Set: [C, D]
Map: {key1=I, key2=J}
Properties: {key1=G, key2=H}

非常的amazing啊,注入成功了。这说明了一个问题,对于这些集合类,数组、List和Map是相近的,Map和Properties是相近的,这些相近的集合类之间的标签,是可以等价使用的。

如果集合内部存储的是Bean对象的话,我们可以用<ref>标签来作为代替:

1
2
3
4
5
6
7
8
<bean id="exampleClass" class="org.example.Clazz">
<property name="dateArray">
<set>
<ref bean="exampleDate"></ref>
</set>
</property>
</bean>
<bean id="exampleDate" class="java.util.Date"></bean>

Spring中基于注解的IoC容器

@Component

@Component注解效果等同于xml中的<bean>标签,作用是将被标注的类放进IoC容器中。

1
2
3
@Component(value = "accountService")
public class AccountServiceImpl implements AccountService {
}

经过上述的标注,IoC容器中会创建一个AccountServiceImpl对象,key为accountService。如果不指定value的值,key会默认设为该类的类名,且首字母变为小写,例如上述代码,默认key值为accountServiceImpl。若注解中只有一个属性,且该属性为value,那么可以省去value=

标注完注解后,需要在xml文件中进行一下配置,告知Spring需要扫描@Component标签。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="org.example"></context:component-scan>

</beans>

@Controller, @Service和@Repository

@Controller, @Service@Repository这三个注解基本与@Component效果相同。Spring提供这三个标签是为了区分视图层、业务层和持久化层的Bean。其中,@Controller一般用于视图层,@Service一般用于业务层,@Repository一般用于持久化层。

@Scope

用于指定Bean的作用范围,效果等同于xml中的<bean>标签的scope属性。@Scope注解拥有一个属性value,取值同<bean>标签的scope属性,常用有’singleton’和’prototype’,用来指定单例和多例。

@PreDestory和@PostConstruct

@PreDestory用于指定销毁方法,@PostConstruct用于指定初始化方法。效果分别等同于xml中的<bean>标签的destory-method属性和init-method属性。

注意,上述两个注解在Java 9中已经弃用,若需使用,需要在Maven工程的POM中进行相关的依赖导入。参考来源找不到@PostConstruct和@PreDestroy(原因和解决方法)

1
2
3
4
5
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>

Spring中基于注解的DI

注意,注解DI只能注入基本类型、String和自定义Bean类型,集合类型只能通过XML。

@Autowired

@Autowired注解可以标注变量,也可以标注方法。当某个标量被标注上@Autowired时,Spring会根据该变量的变量类型,自动在IoC中寻找对应的Bean来进行注入。注意,因为是自动的,所以需要对应Bean的类型在IoC容器中是唯一的,不能有多个相同类型的Bean。

1
2
3
4
5
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao = null;
}

上述代码中,accountDao变量被标注为@Autowired,Spring会在IoC容器中自动查找类型为AccountDao的Bean来进行注入。如果IoC容器中没有此类型的Bean,直接报错;如果有多个此类型的Bean,Spring会根据被注解的变量名称,来寻找IoC容器中是否有对应的key,如果有,就将此key对应的Bean注入。例如上述代码,假设此时IoC容器中有多个类型为AccountDao的Bean,那么Spring会寻找key为accountDao的Bean,若存在,则直接注入;若不存在,则报错。

同基于xml的DI,@Autowired也可注解构造函数和setter。规则同上。

@Qualifier

@Qualifier需要与@Autowired配合使用。@Qualifier拥有value属性,可以指定Bean的名称,从而指定注入IoC容器中的哪一个Bean,需要建立在@Autowired注解匹配类型之后,不能独立使用。

1
2
3
4
5
6
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
@Qualifier("accountDao")
private AccountDao accountDao = null;
}

@Resource

@Autowired@Qualifier非常笨拙,而且需要配合使用。这时我们可以使用@Resource注解来代替。@Resource拥有一个属性name,用来指定要注入的Bean的id。

1
2
3
4
5
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao = null;
}

注:jdk版本过高会导致找不到@Resource标签,需要在Maven工程的POM中加一个额外的依赖。具体方法同“@PreDestory和@PostConstruct”部分。

@Value

上述三个注解只能标注Bean类型,对于基本类型和String,需要用到@Value@Value拥有一个value属性,且支持EL表达式。

完全脱离XML的Spring IoC与DI

在上面两个部分,即使我们用了注解,但仍需要在xml文件中配置<context:component-scan>标签。现在我们想用注解去彻底代替掉xml,这时我们需要先创建一个类,名为SpringConfiguration,将它放在config包下。

@Configuration

此注解的作用是标识当前被标注的类是一个配置类:

1
2
3
@Configuration
public class SpringConfiguration {
}

@ComponentScan

很明显,此注解是用来代替<context:component-scan>标签的,用来告知Spring需扫描哪个包下的@Component注解。他有两个属性valuebasePackages,但这两个属性的作用是一样的,选一个即可,用来表示创建容器时需被扫描的包。

1
2
3
4
@Configuration
@ComponentScan(basePackages = {"org.example"})
public class SpringConfiguration {
}

在注解中,如果属性的值为数组,但数组中有且只有一个值,{}其实是可以省略的。

@Bean

@Bean注解用来标注在方法上,被@Bean标注的方法需要创建一个特定的Bean对象并返回,这样就可以告知Spring容器,我们需要把这个方法返回值作为Bean对象放进IoC容器中来方便后续的注入。显然,@Bean注解能够用来代替使用factory-beanfactory-method<bean>标签。

一个很简单的场景,假如我引用了第三方的jar包,比如数据库连接的dbutils,我现在需要里面的QueryRunner对象来进行数据库访问,那么显然,我是无法给QueryRunner这个对象标注上@Component的,因为它存在于第三方的jar包中。此时就需要用到@Bean注解。

我们在SpringConfiguration类中创建一个方法叫做createQueryRunner,这个方法会new一个QueryRunner对象并返回。这时在这个方法上标注上@Bean,就可以告知Spring容器,我们需要把返回的Bean放进IoC容器中。此注解有一个属性name,用来指定这个Bean在IoC容器中的key值。若没有指定name,key的缺省值为当前方法的名称,例如对于下述代码中创建的QueryRunner对象,key为createQueryRunner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
@Scope("prototype")
public QueryRunner createQueryRunner(DataSource dataSource) {
return new QueryRunner(dataSource);
}

@Bean(name = "dataSource")
public DataSource createDataSource() {
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/ioc_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
ds.setUser("root");
ds.setPassword("password");
} catch (Exception e) {
throw new RuntimeException(e);
}
return ds;
}

如果@Bean标注的方法需要传入参数,Spring框架会在IoC容器中寻找是否有合适的对象,具体规则同@Autowired注释。

AnnotationConfigApplicationContext

我们发现,在创建IoC容器的时候,使用的是ClassPathXmlApplicationContext对象,但是这个对象需要去读取xml文件,我们现在又想完全摒弃xml文件。这时就需要用AnnotationConfigApplicationContext。用法如下,需要传入配置类的Class对象:

1
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

@Import

加入我现在有多个配置类,例如我将所有数据库连接相关的全部放入一个名为JdbcConfiguration的类中,这个时候我需要IoC能读取SpringConfigurationJdbcConfiguration两个配置类的话,有以下几种选择:

  • SpringConfigurationJdbcConfiguration全部标注上@Configuration,并且在SpringConfiguration中的@ComponentScan的属性中加上config包:

    1
    2
    3
    4
    @Configuration
    @ComponentScan({"org.example", "config"})
    public class SpringConfiguration {
    }
  • 在创建IoC容器时额外加入JdbcConfiguration的Class对象。此时SpringConfiguration中的@ComponentScan的属性就不再需要加上config包了,并且SpringConfigurationJdbcConfiguration@Configuration注解也可以省去:

    1
    ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class, JdbcConfiguration.class);
  • 使用@Import注解。在SpringConfiguration中加入@Import@Import注解拥有一个value属性,属性值为数组,其中元素为需要引入的其他配置类的Class对象。此时,IoC容器不再需要加入JdbcConfiguration的Class对象,并且SpringConfiguration中的@ComponentScan的属性也不需要加上config包:

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan("org.example")
    @Import(JdbcConfiguration.class)
    public class SpringConfiguration {
    }

@PropertySource

在数据库连接中,我们有一些写死在代码中的信息,例如驱动的选择、数据库url、用户和密码等。我们希望可以随时修改,这就需要额外添加一个配置文件。

现在,在resource文件夹下创建一个properties文件:

1
2
3
4
jdbc-driver=com.mysql.jdbc.Driver
jdbc-url=jdbc:mysql://localhost:3306/ioc_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
jdbc-username=root
jdbc-password=password

之后,我们需要给配置类标注上@PropertySource来指定配置文件。@PropertySource拥有一个属性value,表示配置文件的路径。由于配置文件在项目工程的resource目录下,编译后会生成在类路径下,所以需要classpath来指定类路径:

1
2
3
4
5
6
@Configuration
@ComponentScan("org.example")
@Import(JdbcConfiguration.class)
@PropertySource("classpath:jdbc-config.properties")
public class SpringConfiguration {
}

若需要指定多个配置文件,可以使用@PropertySources注解。

标注@PropertySource后,我们就可以创建一些需要从配置文件中读取的成员变量,并用上文提到的@Value注解来进行注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
public class JdbcConfiguration {
@Value("${jdbc-driver}")
private String driver;
@Value("${jdbc-url}")
private String url;
@Value("${jdbc-username}")
private String username;
@Value("${jdbc-password}")
private String password;

@Bean
@Scope("prototype")
public QueryRunner queryRunner(DataSource ds) {
return new QueryRunner(ds);
}

@Bean(name = "dataSource")
public DataSource dataSource() {
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
} catch (Exception e) {
throw new RuntimeException(e);
}
return ds;
}
}

控制反转和依赖注入

http://xnsi.github.io/2020/07/20/ioc-di/

Author

s.x.

Posted on

2020-07-20

Updated on

2021-11-19

Licensed under

Comments