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