IoC(Inversion of Control),即控制反转,在 Spring 中实现控制反转的是 IoC 容器,Spring 实现 IoC 容器的方式主要是 DI(Dependency Inject),即依赖注入。 怎么理解呢?举个例子,要使用一个对象时我们可以通过new
的方式自己创建,如果按照 Spring IoC 的思想,则我们不需要自己创建对象,只需要告诉 IoC 容器你要给我创建哪些对象,按照什么规则创建,在我需要使用对象的时候把创建好的对象给我,这样对象将由 Spring IoC 来管理,可以减少代码中类似new
方式创建对象的硬编码,降低类之间的耦合。
所以如何将我们自己开发的类的对象交给 Spring IoC 容器来管理,即如何把对象装配到 IoC 容器中,这是我们重点要关注的问题。在 Spring 通常可以选择如下几类配置方式:
- 基于 XML 的配置(<bean>)
- 基于 XML + 注解的配置(XML + <context:component-scan> + @Component)
- 基于 Java 配置类 + 注解的配置(@Configuration + @Component + @ComponentScan)
- 基于纯 Java 配置类的配置(@Configuration + @Bean)
- 混合配置
前边说过 Spring 实现 IoC 容器的方式主要是依赖注入,Spring 中主要支持以下两种依赖注入方式:
- setter 注入
- 构造器注入
这样 Spring 就知道如何创建对象、组织对象的的依赖关系了,其中 setter 注入更加灵活常用。
接下来就是具体的学了,测试代码是银行转账的场景的例子,其中AccountDao
代表持久层,模拟转账的数据库操作;AccountService
代表业务层,模拟具体的转账业务。表现层用单元测试模拟。
需要使用的Maven依赖:
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--IoC必须的-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
一、基于 XML 的配置
AccountServiceImpl
中的transfer
用来模拟转账流程,要使用对象AccountDao
,即依赖关系,接下来就是如何把AccountServiceImpl
对象创建交给 IoC 容器来管理了。
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
// setter方式注入accountDao
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
// 模拟转账流程
public void transfer(int accountId1, int accountId2, float money) {
// 1.查询转出账户
Account account1 = accountDao.findAccountById(accountId1);
// 2.查询转入账户
Account account2 = accountDao.findAccountById(accountId2);
// 3.转出账户减少钱
account1.setMoney(account1.getMoney() - money);
// 4.转入账户增加钱
account2.setMoney(account2.getMoney() + money);
// 5.更新转出账户
accountDao.updateAccount(account1);
// 6.更新转入账户
accountDao.updateAccount(account2);
}
}
定义bean.xml
文件,其中<bean>
标签的作用是描述要将那个类的对象交给 Spring IoC 容器来管理,其中id
属性是对象在 IoC 容器的唯一标识,class
属性就是具体的类路径。
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="accountService" class="com.shh.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
<bean id="accountDao" class="com.shh.dao.impl.AccountDaoImpl"/>
</beans>
这样AccountServiceImpl
、AccountDaoImpl
对象的创建将由 IoC 容器管理。之前在AccountServiceImpl
中需要依赖AccountDao
,并提供了setter方法,这里通过<property>
标签为依赖对象赋值,即通过 setter 方式完成依赖注入。
通过构造器注入也很好实现,修改AccountServiceImpl
类:
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
// 构造器方式注入accountDao
public AccountServiceImpl(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
修改bean.xml
:
<bean id="accountService" class="com.shh.service.impl.AccountServiceImpl">
<constructor-arg name="accountDao" ref="accountDao"/>
</bean>
所以大致的流程如下:
- 在类中定义好对象的依赖关系,确定注入方式(setter或构造器),推荐 setter 方式。
- 在 XML 文件中配置要管理的对象,及其依赖关系。
已经将对象交给 IoC 容器来管理,那么就可以根据id
从 IoC 容器中获得对象。单元测试类如下:
public class AccountServiceTest {
@Test
public void transfer() {
// 加载配置文件来创建容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
// 根据id取出对象
AccountService accountService = (AccountService) context.getBean("accountService");
accountService.transfer(1, 2, 100);
}
}
ClassPathXmlApplicationContext
是ApplicationContext
接口的实现类,可以通过加载 XML 配置文件来创建 IoC 容器,并创建<bean>
配置的对象保存到容器中。
其实在AccountServiceImpl
中我们已经明确知道要依赖AccountDao
,但在 XML 中还需通过<constructor>
或<property>
来描述依赖关系,未免有些重复,可以通过给<beans>
配置default-autowire
属性来完成自动装配,这属于全局配置,容器中所有对象都会采用自动装配。或者给<bean>
配置autowire
属性实现自动装配,这样就不用配置<constructor>
或<property>
了,例如:
<bean id="accountService" class="com.shh.service.impl.AccountServiceImpl" autowire="byName"/>
<bean id="accountDao" class="com.shh.dao.impl.AccountDaoImpl"/>
可选的自动装配方式有如下几种:
- byName
- byType
- constructor
- no
此外还有几点需要注意下:
- IoC 容器默认在创建时就会创建其管理的对象,可通过
<beans>
的default-lazy-init
属性修改 - IoC 容器创建的对象是默认是单例的,可以通过
<bean>
的scope
属性修改
二、基于 XML + 注解的配置
其实用 XML 的方式配置要管理的对象,及其依赖关系,还是有些繁琐的,每个需要 IoC 容器管理的对象都要配置<bean>
标签,所以更简单的注解配置来了,可以在类上使用@Component
注解,作用相当于<bean>
标签,都是告诉 IoC 容器创建这个类的对象:
@Component("accountService")
public class AccountServiceImpl implements AccountService {
public void transfer(int accountId1, int accountId2, float money) {
}
}
注解的value
属性值accountService
表示这个类的对象在 IoC 容器中的id,如果不配置则会将类名首字母小写作为在容器中的id,即accountServiceImpl
,同样给AccountDaoImpl
类也加上注解,由于没有配置value
属性,则该对象在容器中的id为accountDaoImpl
:
@Component
public class AccountDaoImpl implements AccountDao {
public Account findAccountById(int id) {
}
public void updateAccount(Account account) {
}
}
按照之前的业务需求,AccountServiceImpl
需要依赖AccountDao
实现类的对象,在基于 XML 的配置中,我们可以配置依赖注入或自动装配的方式完成对象的初始化,此时可以使用@Autowired
注解实现自动装配,可选的装配方式和 XML 配置中提到的类似:
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(int accountId1, int accountId2, float money) {
}
}
@Autowired
注解默认按照对象的类型完成装配,Sping 会在 IoC 容器查找AccountDao
的实现类的对象,找到则装配成功,那么问题来了,如果在容器中AccountDao
还有一个实现类AccountDaoImpl2
的对象,则会产生冲突,Spring 不知道使用哪个对象来装配而产生异常。
@Component
public class AccountDaoImpl2 implements AccountDao {
public Account findAccountById(int id) {
}
public void updateAccount(Account account) {
}
}
要解决这个问题,可以在AccountServiceImpl
指定要依赖的AccountDao
对象名为容器中已存在的对象id,例如accountDaoImpl
:
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDaoImpl;
}
但这样显然有些生硬,我就想在AccountServiceImpl
中使用accountDao
作为名称,这里有两种解决方案:
- 使用
@Primary
,告诉 Spring 优先使用哪个类的对象,例如产生冲突时优先使用AccountDaoImpl2
的对象:
@Primary
@Component
public class AccountDaoImpl2 implements AccountDao {
}
- 使用
@Qualifier
,有冲突时,直接指定使用容器中哪个对象即可
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
@Qualifier("accountDaoImpl")
private AccountDao accountDao;
}
除了@Component
,还可以使用@Service
、@Repository
,作用都是一致的,在开发中我们习惯在业务层的类上使用@Service
,在持久层的类上使用@Repository
。
到这里已经用注解的方式替换掉了<bean>
标签配置,则可以删掉 XML 中的<bean>
标签:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
</beans>
<beans>
标签空空如也,那么执行单元测试时:
public class AccountServiceTest {
@Test
public void transfer() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
AccountService accountService = (AccountService) context.getBean("accountService");
accountService.transfer(1, 2, 100);
}
}
相当于加载了一个空的配置文件,没有了<bean>
标签自然不会创建对象并保存到容器,无法从容器得到id为accountService
的对象。所以现在需要做的就是告诉 Spring 去哪里找配置了@Component
注解的类,来创建对象并保存到容器,这就相当于在 XML 中配置了<bean>
,很简单一行配置搞定:
<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
https://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="com.shh"/>
</beans>
作用就是让 Spring 具备解析注解的功能,并去com.shh
包下扫描、解析使用了@Component
注解的类。
使用<context:component-scan/>
之所以能让 Spring 具备解析注解的功能,因为它隐式的启用了<context:annotation-config>
配置。
到这里我们已经成功的将 XML 配置文件中的<bean>
拿掉了,但是又多了一行<context:component-scan base-package="com.shh"/>
配置,能否把它也拿掉,用纯注解的方式实现功能呢?
三、基于 Java 配置类 + 注解的配置
要拿掉<context:component-scan base-package="com.shh"/>
,就要找到一个注解能够实现扫描、解析使用了@Component
注解的类,在 Spring 中可以使用@ComponentScan
注解实现这个功能,这样就从 XML 配置文件移除了所有的配置项。
但是有问题了,由于我们此时的目标是采用纯注解配置,自然不能使用bean.xml
了,所以需要定义一个配置类来充当bean.xml
的角色,这个配置类需要使用@Configuration
注解标注,配置类的定义如下:
@Configuration
@ComponentScan(basePackages = "com.shh")
public class BeanConfig {
}
我们之前使用的ClassPathXmlApplicationContext
初始化时需要的参数就是 XML 配置文件,现在没有了 XML 配置文件,自然不能使用这个类了,其实ApplicationContext
接口还有另一个实现类AnnotationConfigApplicationContext
,可应用于纯注解得 IoC 开发,来加载配置类:
public void transfer2() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
AccountService accountService = (AccountService) context.getBean("accountService");
accountService.transfer(1, 2, 100);
}
这样顺利的从XML配置、XML+注解的配置过渡到了没有 XML 的配置,解决的核心问题就是如何用注解替换掉 XML
四、基于纯 Java 配置类的配置
其实当我们使用@Configuration
注解时还有新的玩法,看代码:
@Configuration
public class BeanConfig2 {
@Bean
public AccountService accountService(AccountDao accountDao){
return new AccountServiceImpl();
}
@Bean("accountDao")
public AccountDao getAccountDao(){
return new AccountDaoImpl();
}
}
相比BeanConfig
类,去掉了@ComponentScan
,这样就不用了扫描、解析指定包下使用了@Component
的类,可以无需添加@Component
注解。
同时有多了两个用@Bean
注解标注的方法,这样当配置类被加载时会将方法的返回对象保存到 IoC 容器中,对象在容器中的id默认为方法名,也可以通过@Bean
的属性修改。
五、混合配置
Spring IoC 的各种配置方式相互兼容,不会冲突,这次我们尝试将上边所有的配置方式融合到一起,即 XML + 注解 + Java 配置类。
首先看bean.xml
配置文件的部分,它的职责就是创建accountDao
对象:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="accountDao" class="com.shh.dao.impl.AccountDaoImpl"/>
</beans>
上边通过 XML 配置的AccountDaoImpl
又要依赖Account
,但是我们并没有在 XML 中配置依赖关系,后边会通过@Bean
注解提供依赖对象:
public class AccountDaoImpl implements AccountDao {
private Account account;
public Account getAccount() {
return account;
}
public Account findAccountById(int id) {
return account;
}
public void updateAccount(Account account) {
}
}
接下来是使用@Component
类,后边我们通过@ComponentScan
来扫描它,这个类又通过自动装配的方式依赖AccountDao
:
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(int accountId1, int accountId2, float money) {
.......
}
}
最后就是 Java 配置类了,这里用到了一个新的注解@ImportResourc
来引入bean.xml
,这样当加载BeanConfig3
时 XML 配置文件也会加载:
@Configuration
@ImportResource("bean.xml")
@ComponentScan(basePackages = "com.shh")
public class BeanConfig3 {
@Bean("account")
public Account getAccount() {
return new Account();
}
}
经测试所有需要的对象都能被正常创建,所以不管我们有多复杂的需求场景,Spring IoC 都能应对,但一般我们也不会这样做,毕竟太复杂,何必自己为难自己呢。
四、小结
以上就是 Spring IoC 常用的配置方式,一般都会根据项目实际情况组合使用,例如 XML + 注解、Java 配置类 + 注解,混合配置的场景并不多见。我们自己开发的类,优先考虑使用@Component
+ @Autowired
实现自动化装配。第三方开发的类,我们无法修改时,优先考虑在 XML 中通过<bean>
配置。
网友评论