Spring IoC

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);
    }

    // 通过对象的方法创建对象
    public Object getObject {
        return new Object();
    }
    
    // 通过类的静态方法创建对象
    public static Object getObject2 {
        return new Object();
    }
}

定义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"/>
    <!-- 通过对象的方法创建对象 -->
    <bean id="object1" factory-bean="accountService" factory-method="getObject"/>
    <!-- 通过类的静态方法创建对象 -->
    <bean id="object2" class="com.shh.service.impl.AccountServiceImpl" factory-method="getObject2"/>
</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属性修改:singleton(单例)、prototype(多例)、request(web应用的请求范围)、session(web应用的会话范围)、global-session(集群环境的会话范围,非集群时就是session)

二、基于 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;
}

还有一个@Resource注解,功能和@Autowired类似,但是@Resource默认按照对象名完成对象的注入的,当一个接口在容器有多个对象时,使用@Resource会方便些,不用搭配其他注解。

除了@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>配置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容