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>配置。












网友评论