控制反转和依赖注入
学Spring的时候,接触到两个比较重要的概念,一个是IoC(Inversion of Control),控制反转;一个是DI(Dependency Injection),依赖注入。
工厂和单例解耦
在一个简易的后端项目中,处理三层架构最简单的方式自然就是视图层中new业务层的对象,业务层中又new持久化层的对象。但这样下来,耦合度很高,因为这样导致了编译时需要处理各类之间的依赖关系。一个最直接的解耦方式,就是利用读取配置文件,再进行反射,在运行时创建实例对象来取代new,这样能够剥离各层之间的依赖关系。
首先将需要反射的全类名存储在一个简易的properties
文件中:
1 | service=org.example.service.impl.ServiceImpl |
配置文件写好了,现在我们需要去读取这个配置文件,然后通过反射来进行实例创建。此过程最好放置在一个工厂类中,通过工厂模式来进一步解耦。
1 | public class MyBeanFactory { |
上述代码中,在调用Properties对象的load
方法之前,并没有通过指定一个路径来创建InputStream
对象,而是通过MyBeanFactory.class.getClassLoader().getResourceAsStream()
来获取。因为对于properties文件,我们一般是放在resources目录下,但若是直接指定路径,在项目部署后,是无法通过指定resources路径来获取到文件的。
之后,我们在MyBeanFactory
类中添加一个创建实例的方法来完成我们的工厂类:
1 | public static Object getBean(String beanName) { |
现在,我们就可以用MyBeanFactory.getBean()
来代替各层中的new了。
但还有一个问题,假如我们在视图层内创建了多次某个业务层的Bean,这样创建出来的这些Bean是多例状态,有时我们希望这些创建出来的Bean是同一个Bean对象,这时我们就需要引入单例模式。
思路很简单,在MyBeanFactory
中添加一个哈希表,来存储各个properties文件中的value值所对应的实例对象,之后的getBean()
函数,不再进行实例创建,而是直接从这个哈希表中进行查找。下面是更改后的MyBeanFactory
,为了方便,简单写了一个饿汉式单例:
1 | public class MyBeanFactory { |
控制反转
现在来说正题,IoC。IoC(Inversion of Control),控制反转,是Spring框架的核心之一。在上述例子中,我们将new替换成了利用MyBeanFactory
来创建实例,以业务层中创建持久化层的对象的具体代码为例,我们可以将
1 | Dao dao = new DaoImpl(); |
替换成
1 | Dao dao = (DaoImpl)MyBeanFactory.getBean("dao"); |
在第一种写法中,我们创建实例对象的控制权仍处于我们的应用中,这使得各模块之间的依赖变强,大大增加了耦合度。
1 | +----------+ |
而在第二种写法中,我们将控制权交给了工厂,工厂会自动创建对应的实例并返回给应用。
1 | +----------+ |
下面是维基百科中对于控制反转的定义:
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 |
|
例如我需要根据我的各个dao对象来对xml文件进行配置,只需创建一个dao.xml
,然后在其中进行如下配置:
1 |
|
在使用的时候,我们需要首先获取IoC容器对象:
1 | ApplicationContext ac = new ClassPathXmlApplicationContext("dao.xml"); |
这个容器对象就相当于第一部分我们手撕的工厂类,同时,此容器对象中同样提供了getBean()
方法。
Bean创建方法
上面这种<bean id="itemDao" class="org.example.dao.impl.ItemDao"></bean>
标签只能根据默认构造函数进行实例创建。(注意,JavaBean规范中要求必须提供一个无参数的默认构造函数)若此时我们有一个外部jar包,假设其中有一个工厂类,我们需要根据工厂类中的特定方法来获取实例对象,可以用下面的标签:
1 | <bean id="instanceFactory" class="org.example.factory.InstanceFactory"></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 | public class Clazz { |
现在我们要通过注入来根据这个构造函数创建实例。在Spring的xml配置文件中,提供了一个<constructor-arg>
标签来进行构造函数注入的相关配置。此标签需用在<bean>
标签下:
1 | <bean id="exampleClass" class="org.example.Clazz"> |
之后,我们需要通过该标签的相关属性,来进行参数的传递。<constructor-arg>
有如下几个属性:
- type:指定注入数据的类型,与构造函数中的参数类型对应。
- index:指定注入数据在构造函数的参数列表中的位置,从0开始。
- name:指定注入数据在构造函数的参数列表中的名称。
- value:注入数据的具体值。均为字符串,Spring会根据构造函数来进行具体的类型转换。
- ref:用于指定其他Bean类型。
可见,type,index和name是用来指定注入哪个参数的。但type是无法唯一确定需要注入的参数的,因为一个构造函数中可能存在多个同类型的参数。而name是可以唯一指定的,因为参数名称是无法重复的。所以一般来讲,只用name属性就足够进行参数指定了。
1 | <bean id="exampleClass" class="org.example.Clazz"> |
假如我们进行上述的注入配置,在运行之后,会生成报错:
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 | <bean id="exampleClass" class="org.example.Clazz"> |
如果需要创建一个特定日期的Date对象的话,可以使用上面Bean的创建方法中提到的利用指定方法来创建一个Bean对象,此处我们可以用SimpleDateFormat
中的parse
方法。这时会比较麻烦,需要在<constructor-arg>
内再套一个<bean>
标签,并在这个<bean>
标签内使用<constructor-arg>
来进行给parse
方法传参:
1 | <bean id="exampleClass" class="org.example.Clazz"> |
setter注入
顾名思义,setter注入无非就是给构造函数的参数分别各写一个set方法,然后通过set方法注入。对于上述的Clazz类,我们写如下几个setter:
1 | public void setNum(Integer num) { |
这时,在<bean>
标签内,需要额外添加<property>
标签,每一个<property>
标签对应一个setter。<property>
标签的属性有如下三个:
- name:注入时调用的setter名称。具体的属性值是setter名称去掉’set’,且后面第一个大写字母均变为小写。例如,
setUserName()
对应的属性值为’userName’。 - value:注入数据的具体值。均为字符串,Spring会根据构造函数来进行具体的类型转换。
- ref:用于指定其他Bean类型。
除了name用法不同,value和ref用法同构造函数注入。
1 | <bean id="exampleClass" class="org.example.Clazz"> |
很明显,相比构造函数注入,setter注入要灵活的多,它不需要一次性注入全部的变量就可以创建实例。但是它也有弊端,setter注入是无法保证获取到的对象中每个属性都有值。
注入集合类
对于一些常用的集合类,例如Array
,List
,Map
,Set
和Properties
。他们的注入需要额外的标签,而且只能用构造器注入和setter注入。
现在我们在Clazz类中加入这些类型的属性变量:
1 | public class Clazz { |
由于构造函数注入和setter注入大同小异,下面只以setter注入为例。
对于这些复杂的集合类的注入,我们需要使用<property>
或<constructor-arg>
下的一些子标签:
1 | <bean id="exampleClass" class="org.example.Clazz"> |
简单地打印一下结果:
1 | Array: [A, B] |
我们对xml做一下手脚,将数组、List和Set的标签顺次替换,将Map和Properties的标签交换:
1 | <bean id="exampleClass" class="org.example.Clazz"> |
再打印一下结果:
1 | Array: [E, F] |
非常的amazing啊,注入成功了。这说明了一个问题,对于这些集合类,数组、List和Map是相近的,Map和Properties是相近的,这些相近的集合类之间的标签,是可以等价使用的。
如果集合内部存储的是Bean对象的话,我们可以用<ref>
标签来作为代替:
1 | <bean id="exampleClass" class="org.example.Clazz"> |
Spring中基于注解的IoC容器
@Component
@Component
注解效果等同于xml中的<bean>
标签,作用是将被标注的类放进IoC容器中。
1 |
|
经过上述的标注,IoC容器中会创建一个AccountServiceImpl
对象,key为accountService
。如果不指定value的值,key会默认设为该类的类名,且首字母变为小写,例如上述代码,默认key值为accountServiceImpl
。若注解中只有一个属性,且该属性为value
,那么可以省去value=
。
标注完注解后,需要在xml文件中进行一下配置,告知Spring需要扫描@Component
标签。
1 |
|
@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 | <dependency> |
Spring中基于注解的DI
注意,注解DI只能注入基本类型、String和自定义Bean类型,集合类型只能通过XML。
@Autowired
@Autowired
注解可以标注变量,也可以标注方法。当某个标量被标注上@Autowired
时,Spring会根据该变量的变量类型,自动在IoC中寻找对应的Bean来进行注入。注意,因为是自动的,所以需要对应Bean的类型在IoC容器中是唯一的,不能有多个相同类型的Bean。
1 |
|
上述代码中,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 |
|
@Resource
@Autowired
和@Qualifier
非常笨拙,而且需要配合使用。这时我们可以使用@Resource
注解来代替。@Resource
拥有一个属性name
,用来指定要注入的Bean的id。
1 |
|
注: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 |
|
@ComponentScan
很明显,此注解是用来代替<context:component-scan>
标签的,用来告知Spring需扫描哪个包下的@Component
注解。他有两个属性value
和basePackages
,但这两个属性的作用是一样的,选一个即可,用来表示创建容器时需被扫描的包。
1 |
|
在注解中,如果属性的值为数组,但数组中有且只有一个值,{}其实是可以省略的。
@Bean
@Bean
注解用来标注在方法上,被@Bean
标注的方法需要创建一个特定的Bean对象并返回,这样就可以告知Spring容器,我们需要把这个方法返回值作为Bean对象放进IoC容器中来方便后续的注入。显然,@Bean
注解能够用来代替使用factory-bean
和factory-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 |
|
如果@Bean
标注的方法需要传入参数,Spring框架会在IoC容器中寻找是否有合适的对象,具体规则同@Autowired
注释。
AnnotationConfigApplicationContext
我们发现,在创建IoC容器的时候,使用的是ClassPathXmlApplicationContext
对象,但是这个对象需要去读取xml文件,我们现在又想完全摒弃xml文件。这时就需要用AnnotationConfigApplicationContext
。用法如下,需要传入配置类的Class对象:
1 | ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); |
@Import
加入我现在有多个配置类,例如我将所有数据库连接相关的全部放入一个名为JdbcConfiguration
的类中,这个时候我需要IoC能读取SpringConfiguration
和JdbcConfiguration
两个配置类的话,有以下几种选择:
将
SpringConfiguration
和JdbcConfiguration
全部标注上@Configuration
,并且在SpringConfiguration
中的@ComponentScan
的属性中加上config包:1
2
3
4
public class SpringConfiguration {
}在创建IoC容器时额外加入
JdbcConfiguration
的Class对象。此时SpringConfiguration
中的@ComponentScan
的属性就不再需要加上config包了,并且SpringConfiguration
和JdbcConfiguration
的@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
public class SpringConfiguration {
}
@PropertySource
在数据库连接中,我们有一些写死在代码中的信息,例如驱动的选择、数据库url、用户和密码等。我们希望可以随时修改,这就需要额外添加一个配置文件。
现在,在resource文件夹下创建一个properties文件:
1 | com.mysql.jdbc.Driver = |
之后,我们需要给配置类标注上@PropertySource
来指定配置文件。@PropertySource
拥有一个属性value,表示配置文件的路径。由于配置文件在项目工程的resource目录下,编译后会生成在类路径下,所以需要classpath
来指定类路径:
1 |
|
若需要指定多个配置文件,可以使用@PropertySources
注解。
标注@PropertySource
后,我们就可以创建一些需要从配置文件中读取的成员变量,并用上文提到的@Value
注解来进行注入。
1 |
|