Spring IoC

作者: SheHuan | 来源:发表于2019-10-17 22:21 被阅读0次

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>

这样AccountServiceImplAccountDaoImpl对象的创建将由 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);
    }
}

ClassPathXmlApplicationContextApplicationContext接口的实现类,可以通过加载 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作为名称,这里有两种解决方案:

  1. 使用@Primary,告诉 Spring 优先使用哪个类的对象,例如产生冲突时优先使用AccountDaoImpl2的对象:
@Primary
@Component
public class AccountDaoImpl2 implements AccountDao {
}
  1. 使用@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>配置。

相关文章

网友评论

    本文标题:Spring IoC

    本文链接:https://www.haomeiwen.com/subject/iiiipctx.html