走进Spring框架系列—IOC应用

一、简介

在上一篇文章《走进Spring框架系列—初识IOC》中,我们介绍了什么是IOC,为什么要用IOC以及IOC的好处,本篇文章承接上一章,讲解IOC的应用,这些都是讲解Spring框架源码的铺垫,万事不可一蹴而就,当然,若自己对IOC应用已经有相当自信又想学习源码的读者可跳过本章节。

小伙伴们可能有一些疑惑,IOC的应用需要单独开一个章节吗,不就是@autowired或者xml里的一个bean标签吗,如果你是这么认为的,那么一定要看完本章节,Spring IOC的应用可远远不止这些,把应用玩转,才是学习源码的奠基石。

二、Spring IOC的编码风格

还是老样子,直接上官网,Spring IOC有几种编码风格不是笔者瞎总结的,而是官网告诉我们的:

Spring官网中介绍的3种IOC编码风格

在传统Spring框架开发中,我们一般使用xml的方式定义元数据,自Spring2.5开始支持注解配置元数据,从Spring3.0,支持JavaConfig技术,因此我们有3种IOC的编码风格,它们可以互相组合使用,甚至3个一起使用。

在上面官网截图中间绿色提示部分,官方还暗示我们现在使用JavaConfig的开发者越来越多,xml越来越不受欢迎(毕竟项目越大,xml里要维护的东西就越多),笔者也不喜欢xml方式,另外springboot也是基于JavaConfig的,大家都喜欢零配置,所以这也是springboot诞生的价值体现,如果你或者你的公司还在使用xml配置Spring IOC,对JavaConfig不是很熟悉的话,那么它会开启你对spring编程的新领域。

2.1 XML

虽然xml现在被各种嫌弃,但毕竟是曾经Spring IOC唯一的配置方式,还是要从它讲起的。

在上一章中已经简单介绍过Spring IOC的用法,但由于第一章的目的主要是为了讲清楚IOC的好处,让大家能感受到IOC的魅力,所以只是里面涉及到的很多应用都是一笔带过。这里仍然沿用上一章的例子,并做更详细的讲解。

首先,仍然是这个有Car属性的People类:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
public class People {
    private Car car;

    public People(){ }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }

    public void setCar(Car car) {
        this.car = car;
    }
}

然后是Car接口和两个具体实现类:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:27
 * @Description: Car接口
 */
public interface Car {
    String getCarName();
}
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:27
 * @Description: 奔驰车
 */
public class BenzCar implements Car{
    @Override
    public String getCarName() {
        return "奔驰";
    }
}
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 宝马车
 */
public class BMWCar implements Car{
    @Override
    public String getCarName() {
        return "宝马";
    }
}

现在我们要使用Spring IOC的xml方式来管理这些对象,要使用Spring IOC仅需要导入以下这个包:

<dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context</artifactId>
       <version>5.2.10.RELEASE</version>
</dependency>

在资源文件路径下新建一个spring.xml(名字随你取,但作为程序员,还是标准命名比较好),并使用bean标签把上面三个对象交给IOC容器管理:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="benzCar" class="com.richard.car.BenzCar"/>
    <bean id="bmwCar" class="com.richard.car.BMWCar"/>
    <bean id="people" class="com.richard.user.People"/>
</beans>

这样,在IOC容器在初始化时就会自动实例化这三个对象并放在一个集合当中,编写主类测试:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        BeanFactory beanFactory=new ClassPathXmlApplicationContext("classpath:spring.xml");
        People people = (People) beanFactory.getBean("people");
        people.drive();
    }
}

我们使用xml方式,就必须使用BeanFactory的ClassPathXmlApplicationContext实现类来初始化IOC容器,并传入xml文件路径作为参数。

其中beanFactory.getBean("people")中的参数就是我们在bean标签中配置的id,如果你不设置id,IOC容器也会自动生成:


IOC容器自动为bean生成的id

因此,为了方便获取bean和管理bean之间的依赖,我们还是为每个bean设置一个id,还可以使用name属性为这个bean设置一个别名:

<bean id="people" name="superman" class="com.richard.user.People"/>

然后在主类中通过参数"superman"也可得到people对象,注意,id是每个bean唯一的标识,而name不是,一个bean可以有多个name,多个bean也可以有相同的name,name一般情况下用得比较少,我们就用id即可。

bean也配置完成了,但此时如果直接运行会报空指针异常:

Exception in thread "main" java.lang.NullPointerException
    at com.richard.user.People.drive(People.java:39)
    at com.richard.Test.main(Test.java:18)

为什么呢?因为People中有一个Car属性,我们把这种关系称为依赖,即People依赖了Car,此时的Car还没有被赋值,调用drive()方法就会抛出空指针异常,因此,我们需要显式配置People需要依赖哪一个Car:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="benzCar" class="com.richard.car.BenzCar"/>
    <bean id="bmwCar" class="com.richard.car.BMWCar"/>
    <bean id="people" class="com.richard.user.People">
        <!--ref指向上面定义的bean的id-->
        <property name="car" ref="bmwCar"/><!--有多个属性可配置多个这样的property标签-->
    </bean>
</beans>

此时运行主类结果:

今天开宝马 

可以发现,通过修改配置文件,我们可以随时切换所依赖的类,而不需要修改任何业务逻辑代码,IOC会根据我们的配置把所需要的依赖注入给变量,这种方式就称为依赖注入,它就是Spring IOC实现的具体手段,依赖注入又有两种方式:构造方法注入和set方法注入,我们上面用的便是set方法注入,也就是说如果将People类中的set方法去掉,运行便会报以下异常:

警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'people' defined in class path resource [spring.xml]: Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property 'car' of bean class [com.richard.user.People]: Bean property 'car' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'people' defined in class path resource [spring.xml]: Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property 'car' of bean class [com.richard.user.People]: Bean property 'car' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1734)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)
    at com.richard.Test.main(Test.java:17)
Caused by: org.springframework.beans.NotWritablePropertyException: Invalid property 'car' of bean class [com.richard.user.People]: Bean property 'car' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
    at org.springframework.beans.BeanWrapperImpl.createNotWritablePropertyException(BeanWrapperImpl.java:243)
    at org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:426)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266)
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:97)
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:77)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1730)
    ... 13 more

这个异常也说的很清楚,car属性是不可写的,我们没有提供一个set方法。

这里有个重点,xml的set注入,是根据set方法的名字去掉"set"字符串再把驼峰命名的大写字母改为小写来匹配注入的,也就是说上面xml文件配置property中name属性的"car"指向的是setCar方法名去掉"set"再把"C"小写后的名字"car",如果你把People中的Car属性变量名"Car car"改成"Car xxx"不会影响正常注入,但如果你把set方法名改成"setXXX()"而不同步更改xml文件的话,就会报上面这个不提供set方法一样的错误,读者有兴趣的话可自行尝试。

接着再来看看构造方法注入,首先修改People类:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
public class People {
    private Car car;

    public People(Car car){
        this.car=car;
    }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }

    public void setCar(Car car) {
        this.car = car;
    }
}

然后修改spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="benzCar" class="com.richard.car.BenzCar"/>
    <bean id="bmwCar" class="com.richard.car.BMWCar"/>
    <bean class="com.richard.user.People">
        <constructor-arg ref="bmwCar"/><!--有多个参数需要配置,可使用多个这样的constructor-arg标签-->
        <!--<constructor-arg ref="example" name="example" index="0"/>-->
        <!--有多个参数时,可使用name属性指定配置哪个参数,也可使用index属性指定所配置的参数的下标-->
    </bean>
</beans>

运行结果与上面一样。值得一提的是,属性的注入并不只是能注入对象,基本类型甚至集合都能注入,给People添加几个属性:

public class People {
    private Car car;
    //身份证号
    private int id;
    //aihao
    private Map<String,String> hobbies;
    //性格
    private List<String> characters;

    public People(Car car){
        this.car=car;
    }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }

    public void setCar(Car car) {
        this.car = car;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setHobbies(Map<String, String> hobbies) {
        this.hobbies = hobbies;
    }

    public void setCharacters(List<String> characters) {
        this.characters = characters;
    }
    
    //增加自我介绍方法,方便看注入结果
    public void selfIntroduction(){
        StringBuilder sb = new StringBuilder();
        Iterator<Map.Entry<String, String>> iter = hobbies.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, String> entry = iter.next();
            sb.append(entry.getKey());
            sb.append('=').append('"');
            sb.append(entry.getValue());
            sb.append('"');
            if (iter.hasNext()) {
                sb.append(',').append(' ');
            }
        }
        System.out.println("我的id是:"+id+",我的性格是:"+characters.toString()
        +",我的爱好是:"+sb.toString());
    }
}

修改spring.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="benzCar" class="com.richard.car.BenzCar"/>
    <bean id="bmwCar" class="com.richard.car.BMWCar"/>
    <bean id="people" name="superman" class="com.richard.user.People">
        <constructor-arg ref="bmwCar"/>
        <property name="id" value="123456789"/>
        <property name="characters" >
            <list>
                <value>大方</value>
                <value>开朗</value>
            </list>
        </property>
        <property name="hobbies">
            <map>
                <entry key="食物" value="甜食"/>
                <entry key="运动" value="跑步"/>
            </map>
        </property>
    </bean>
</beans>

修改主类并运行:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        BeanFactory beanFactory=new ClassPathXmlApplicationContext("classpath:spring.xml");
        People people = (People) beanFactory.getBean("superman");
        people.drive();
        people.selfIntroduction();
    }
}
运行结果:
今天开宝马
我的id是:123456789,我的性格是:[大方, 开朗],我的爱好是:食物="甜食", 运动="跑步"

类似这样的基本类型或者集合注入很少会用到,了解就好。

2.2自动装配

不知读者有没有发现上面xml配置有一个很大的弊端,就是假如我的People类不止一个属性时,为了维护好依赖关系就要配置很多property标签,当项目庞大有非常多类时,我们每一个类都要去配大量的property,而且我们本身在类里面已经定义好了我们需要哪些依赖,我还要去配置文件里显式配置这些依赖,作为一个强大的依赖管理容器,你不该自己去帮我找到我想要的那些依赖吗?没错,基于这样的思想,于是就有了自动装配技术。

打开自动装配功能非常简单,修改spring.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd"
        default-autowire="byName">
    <bean id="benzCar" class="com.richard.car.BenzCar"/>
    <bean id="bmwCar" class="com.richard.car.BMWCar"/>
    <bean id="people" class="com.richard.user.People"/>
</beans>

在bean的父标签beans里添加属性"default-autowire="即表示你要为所有的bean开启自动装配。你也可以在单个bean标签里设置这个属性,即表示被设置的bean才使用自动装配,没设置的如果有依赖仍然需要显式配置。

它的值有5种,即自动装配有5种方式:

  • byType:通过类型自动注入,即IOC容器在实例化我的People对象时,发现里面有一个Car类型的属性,就会遍历所有被IOC管理的bean,如果找到满足instanceof Car这个条件的bean,就会把这个bean注入给People的Car属性。但是,如果有两个或以上的bean都满足这个条件,就会抛出UnsatisfiedDependencyException异常:
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'people' defined in class path resource [spring.xml]: 
Unsatisfied dependency expressed through bean property 'car'; 
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type 'com.richard.car.Car' available: 
expected single matching bean but found 2: benzCar,bmwCar//想得到单个类型匹配的bean,但找到了两个

我的项目中使用byType时,有两个常用办法解决上述问题:

  1. 最简单粗暴的,将xml中BenzCar或者BMWCar的bean标签删除掉一个,那么IOC容器中就只剩下另一个也是唯一一个类型为Car的bean,当然就能完成通过类型的自动注入。
  2. 在bean标签中加上primary属性:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd"
        default-autowire="byType">
    <bean id="benzCar" class="com.richard.car.BenzCar"/>
    <bean id="bmwCar" class="com.richard.car.BMWCar" primary="true"/>
    <bean id="people" class="com.richard.user.People" />
</beans>

这样就告诉了IOC容器,虽然有多个Car类型的bean,但BMWCar才是我想要的。
运行结果:

今天开宝马
  • byName:通过名称自动注入,注意,byType和byName都是通过set方法自动注入,如果没有提供set方法就会报我上面介绍setter方式注入时提到过的错误,而byName"by"的也是set方法的name,与我上面重点介绍的setter方式匹配规则一样,即set方法如果叫setBmwCar,就会去掉"set",把"B"小写,然后在容器中找一个id为"bmwCar"的bean,最后完成自动注入:
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
public class People {
    private Car car;

    public void setBmwCar(Car car) {
        this.car = car;
    }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }
}
主类运行结果:今天开宝马
  • constructor:通过构造方法来注入,仍然有byType和byName两种方式,只不过是隐式规则:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd"
        default-autowire="constructor">
    <bean  id="benzCar" class="com.richard.car.BenzCar"/>
    <bean  id="bmwCar" class="com.richard.car.BMWCar"/>
    <bean  id="people" class="com.richard.user.People" />
</beans>
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
public class People {
    private Car car;

    public People(Car benzCar){
        this.car=benzCar;
    }

    public void setCar(Car car) {
        this.car = car;
    }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }
}
主类运行结果:今天开奔驰

首先,根据构造方法参数类型Car,byType找到BenzCar和BMWCar两个满足条件的bean,然后根据参数变量名benzCar,byName找到BenzCar的id满足条件,完成此次自动注入。

如果byType只找到一个满足条件的bean就直接注入,如果找到多个,然后通过byName又没有找到符合条件的bean会发生什么,以及该怎么解决,相信读者已经有答案了,忘了的话再回头看上面的byType介绍。

  • no:不使用自动装配。
  • default:默认为no。

2.3 Annotation

自动装配让我们在xml文件中省下了大篇幅的依赖显式配置,但是在项目很大时,仍然要写很多bean标签,而基于注解来使用IOC便可以让我们更专注于写代码本身,而不用花很多精力去维护一个xml文件。

在xml中我们显示定义了bean,并在bean标签的class属性中配置了这个类的全限定名,那么IOC容器就可以通过反射得到Class类并实例化对象,而基于注解的方式没有这样的配置,因此,IOC容器必须自己去扫描我们给定的路径下的所有类文件,并根据类是否加了注解来决定该类是否要load到IOC容器当中。

因此,首先我们要修改xml文件,删掉bean配置,通知IOC容器开启扫描功能:

<?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
       http://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.richard"/><!--开启扫描功能 并告诉IOC我要扫描com.richard包下所有的类-->
</beans>

然后,为我们的类加上注解@Component注解:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:27
 * @Description: 奔驰车
 */
/*
    Spring在扫描包时发现该类加了此注解,便会把这个类放进容器进行管理
 */
@Component
public class BenzCar implements Car{
    @Override
    public String getCarName() {
        return "奔驰";
    }
}
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 宝马车
 */
@Component
public class BMWCar implements Car{
    @Override
    public String getCarName() {
        return "宝马";
    }
}
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
public class People {
    @Autowired
    private Car benzCar;
    
    public void drive(){
        System.out.println("今天开"+benzCar.getCarName());
    }
}

主类没有任何修改,运行结果:

今天开奔驰

这里,要注意两个点:

  1. 如何把对象交给IOC容器管理?
    很明显,在类名上加上@Component注解,使用过Spring框架的小伙伴应该知道还有其他注解也有这个作用,它们是@Service、@Repository、@Controller,那么它们有什么区别呢?标准答案是:没区别。这个看源码就可以知道:
package org.springframework.stereotype;

@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Documented
@org.springframework.stereotype.Component //可以看到,@Service没有额外功能,实质上还是一个@Component
public @interface Service {
    @org.springframework.core.annotation.AliasFor(annotation = org.springframework.stereotype.Component.class)
    java.lang.String value() default "";
}

@Repository和@Controller也是一样的,感兴趣的小伙伴可以自己打开源码验证。那么我们为什么在使用MVC时要在Dao层加@Repository,Service层加@Service,Controller层加@Controller呢?事实上你全部用@Component代替,丝毫不影响功能,标准答案是:为了规范。官网也给出了解释:

官网对这几个注解的解释

最重点的一句话我已经在图中勾画出来,它翻译过来意思就是:"@Service、@Repository、@Controller这三个注解在未来发布的Spring框架中可能会被赋予额外的语义"。因此,我们在编程过程中就更有理由规范编码了。

另外,@Component注解有一个参数value,可以为bean起一个自定义的名字,就像xml中的id和name属性,但基于注解的IOC本身默认给bean的名字就是根据驼峰命名规则的类名首字母小写,不像xml默认是全限定名,所以,如非特殊情况,我们很少给bean另起一个名字:


基于注解的IOC容器自动为bean生成的id

除了我自己定义的类,这时容器中还有5个类,它们是初始化IOC容器时Spring内部的类,这个要等我们讲到源码时会讲解到,此时忽略它们就好。

  1. 如何管理对象依赖?
  • @Autowired:可以看到,我在我的People类中的Car属性上就加上了这个注解,它的注入方式与之前讲到的XML自动装配里的constructor方式相似,也就是先byType,通过类型遍历,如果只找到一个符合条件的bean,直接注入,如果找到多个,就通过属性名byName注入,我这里有两个Car的实现类,属性名叫benzCar,因此最终会通过byName找到BenzCar这个bean完成注入。如果找不到无法完成注入呢?当然是抛出异常了。

这里还可以看出基于注解的IOC与基于XML的另一个区别,那就是基于注解的是不需要提供set方法的,它直接给属性赋值,当然,除非你这样写:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
public class People {
    private Car car;

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }

    @Autowired
    public void setCar(Car benzCar) {
        this.car = benzCar;
    }
}

@Autowired也可以作用在方法上,构造方法注入的写法与上述set注入写法类似,但这样的写法也比较少见。

  • @Resource:它与@Autowired在注入上的功能其实是相似的,不同的是它默认先byName去查找,找到即注入,如果找不到才会byType去查找。另外,它提供了一个参数name,可以指定我们要注入的bean的id,这样就可以避免通过byName查找时要修改类中该属性的所有字段名:
@Component
public class People {
    @Resource(name = "BMWCar")
    private Car car;

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }
}
主类运行结果:今天开宝马

还有一点,@Autowired是Spring框架下的类,而@Resource是JDK的,也就是说使用@Resource不会与Spring框架产生耦合,哪一天你自己写了一个IOC容器要代替Spring框架,仍然可以使用@Resource注解,当然,如果你的项目本身就是基于Spring框架开发的,也不必去纠结这个问题,直接使用@Autowired就行。

2.4 JavaConfig

不知读者发现没有,上面基于注解的方式,虽然告别了xml文件中的一大堆配置,但是,仍然没有告别xml文件,JavaConfig,意即基于纯java来配置Spring框架,它就是我们告别xml文件的最后手段。

在基于注解的方式中,我们留下xml文件的唯一目的就是配置开启扫描功能,因此,使用JavaConfig第一件事就是新建一个java类来代替xml这个功能:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/2 18:27
 * @Description: spring 配置类
 */
/*
 * 此注解表示这是一个配置类,spring容器在扫描到这个类时会按照配置类的方式解析这个类
 */
@Configuration
/*
*  等同于xml的<context:componentscan basepakage=>
*/
@ComponentScan("com.richard")
public class SpringConfig { }

由于不再需要xml,于是主类中通过xml来启动IOC容器也要改成通过我们写的配置类来启动IOC容器:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        BeanFactory beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
//        BeanFactory beanFactory=new ClassPathXmlApplicationContext("classpath:spring.xml");//留在这里作对比
        People people = (People) beanFactory.getBean("people");
        people.drive();
    }
}

可以看到,bean工厂的实现也换了一个类,两个实现类的类名起得也十分直观优雅。

之前也说过,xml,annotation,javaConfig三种编码风格并不冲突,它们是搭配使用的,甚至可以三者一起使用,这里,我们已经使用了javaConfig+annotation,但如果实在需要在xml中配置某些东西,我们可以这样做:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/2 18:27
 * @Description: spring 配置类
 */
@Configuration
@ComponentScan("com.richard")
@ImportResource("classpath:spring.xml")//引入xml文件
public class SpringConfig { }

既然javaConfig可以完全代替xml,那么它当然可以完成xml中所有配置,比如配置一个bean:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/2 18:27
 * @Description: spring 配置类
 */
@Configuration
@ComponentScan("com.richard")
public class SpringConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() {
         SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
         factoryBean.setDataSource(dataSource());
         return factoryBean;
    }

    @Bean
    public DruidDataSource dataSource() {
        DruidDataSource  ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true");
        ds.setUsername("root");
        ds.setPassword("123456");
        ds.setInitialSize(5);
        return ds;
    }
}

上面例子中是使用MyBatis时所需要的配置,如果你不熟悉MyBatis,可以不用管它是什么,总之,不管SqlSessionFactory还是DruidDataSource,它们都是第三方提供的类,如果我们仍然在使用的xml方式,可以配置一个bean标签,写上它的全限定名交给IOC容器来管理,但此时我们使用javaConfig+annotation,没有xml文件,也不可能在人家源码上加上一个@Component注解,如果要把它们交给IOC容器管理方便我们使用,就可以使用以上方式。

三、IOC中的其他应用

以下的应用将以javaConfig+annotation方式来进行讲解,xml中基本上都有对应的使用方式,但这里为了节省篇幅不做两套演示,读者可自行查阅。

  1. @DependsOn
    一个类和另一个类没有依赖关系,即他们没有互相持有对方的引用,但其中一个类又必须在另一个类初始化之后才能初始化,那么称这个类depends on另一个类,说起来有点抽象,但代码很简单,首先新建一个上帝类:
/**
 * @Author: Richard Lv
 * @Date: 2021/2/8 16:46
 * @Description: 上帝
 */
@Component
public class God {
    public God(){
        System.out.println("上帝出现了!");
    }
}

有上帝才有人,所以人depends on上帝:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
@DependsOn("god")//必须上帝先初始化
public class People {
    @Resource(name = "BMWCar")
    private Car car;

    public People(){
        System.out.println("上帝造了个人");
    }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }
}
运行结果:
上帝出现了!
上帝造了个人
今天开宝马
  1. @Profile
    这个注解可以让我们很方便得切换整个IOC容器的环境,比如在测试环境下,我们有一套测试的增删改查逻辑,其中有些数据甚至是写死的,而在正式环境下,我们有另一套增删改查逻辑,难道我们要在正式与测试环境下不断注释/放开来切换代码?当然不用,就写两套,接着在测试和正式代码上加上@Profile注解来区分它们,最后初始化IOC容器时再设置一下大环境,就可以轻松搞定切换,说起来可能还是很抽象,代码其实也非常简单,这里就不搞增删改查那么麻烦的东西,举这个例子是因为这才是正式项目里@Profile的正确使用姿势,这里就还是用我的DEMO来演示,一看就能明白:
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:27
 * @Description: 奔驰车
 */
@Component
@Profile("online")//设置为正式线上环境使用这个Car
public class BenzCar implements Car{
    @Override
    public String getCarName() {
        return "奔驰";
    }
}
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 宝马车
 */
@Component
@Profile("test")//设置为测试环境使用这个Car
public class BMWCar implements Car{
    @Override
    public String getCarName() {
        return "宝马";
    }
}

上面的注解就表示了,如果我把IOC容器环境设置为"test"的话,那么,初始化IOC容器时,只有加了@Profile("test")和没加这个注解的类会被放入IOC容器(加@Component就不用单独解释吧)。

设置IOC容器环境:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext();
        beanFactory.getEnvironment().setActiveProfiles("online");//把环境设置为"online"
        beanFactory.register(SpringConfig.class);
        beanFactory.refresh();
        People people = (People) beanFactory.getBean("people");
        people.drive();
    }
}
运行结果:今天开奔驰

这里,由于BeanFactory类几乎是AnnotationConfigApplicationContext的顶级父类了,很多方法调用不到,我在之前文章里一直用BeanFactory来接收AnnotationConfigApplicationContext是为了让读者看出IOC的实质是一个bean工厂,大家在实际开发中还是要用AnnotationConfigApplicationContext本身来接收。

另外,我使用了AnnotationConfigApplicationContext的空参构造方法,并在设置环境后调用了register和refresh方法,原因可以参考源码:

/**
     * Create a new AnnotationConfigApplicationContext, deriving bean definitions
     * from the given component classes and automatically refreshing the context.
     * @param componentClasses one or more component classes &mdash; for example,
     * {@link Configuration @Configuration} classes
     */
    public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
        this();
        register(componentClasses);
        refresh();
    }

可以看到,原先new AnnotationConfigApplicationContext(SpringConfig.class)其实调用顺序也是空构造方法,然后register、refresh,只是执行完refresh后,整个IOC容器就已经准备好了,所以我设置环境必须在它准备好之前,因此我把它们拆开分别单独调用。

  1. @Scope
    我们知道,IOC容器中的bean默认都是单例的,我们可以通过@Scope注解改变它的模式:
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 宝马车
 */
@Component
@Scope("prototype")//改变为原型,也就是每次获取BMWCar都能获取到不一样的对象
public class BMWCar implements Car{
    @Override
    public String getCarName() {
        return "宝马";
    }
}

改个主类来测试:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
        BMWCar bmwCar = (BMWCar) beanFactory.getBean("BMWCar");
        System.out.println(bmwCar);
        bmwCar = (BMWCar) beanFactory.getBean("BMWCar");
        System.out.println(bmwCar);
        bmwCar = (BMWCar) beanFactory.getBean("BMWCar");
        System.out.println(bmwCar);
    }
}
运行结果:
com.richard.car.BMWCar@12d3a4e9
com.richard.car.BMWCar@240237d2
com.richard.car.BMWCar@25a65b77

可以看到,每次获取到的BMWCar都是不同的对象。

  1. @Lookup
    基于上面改成原型的BMWCar,那么,当一个单例类依赖了一个非单例(原型)类,会发生什么呢?回到我们的People类:
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
public class People {
    @Resource(name = "BMWCar")
    private Car car;

    /*
     * 这里追加打印一下BMWCar的hashcode
     */
    public void drive(){
        System.out.println("今天开"+car.getCarName()+car.hashCode());
    }
}

修改主类:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
        People people = (People) beanFactory.getBean("people");
        people.drive();
        people.drive();
        people.drive();
    }
}
运行结果:
今天开宝马315860201
今天开宝马315860201
今天开宝马315860201

可以看到,每次都是同一个宝马车,原因也容易想到,People类是单例的,那么它只会被初始化一次,它的属性car也只会被赋值一次,于是BMWCar的原型就相当于"失效了",于是 ,我们就要用@Lookup注解:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
public class People {
    public void drive(){
        Car car=getCar();
        System.out.println("今天开"+car.getCarName()+car.hashCode());
    }

    @Lookup("BMWCar")
    public Car getCar(){
        return null;
    }
}
现在运行主类的结果:
今天开宝马398690014
今天开宝马1526298704
今天开宝马1593180232

其实,被@Lookup注解的方法不用返回任何东西,因为IOC容器会代理重写这个方法,每次调用时,返回一个新的实例对象,作为一个有强迫症的程序员,写一个"retrun null"实在不能忍,太不雅观,于是People类可以写成下面这样:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
public abstract class People {
    public void drive(){
        Car car=getCar();
        System.out.println("今天开"+car.getCarName()+car.hashCode());
    }

    @Lookup("BMWCar")
    public abstract Car getCar();
}

除了@Lookup还有其他方式也可以实现这个功能,这个会在讲解源码时再提到。

  1. @Lazy
    被IOC管理的对象都是在IOC容器初始化时就创建好的,@Lazy注解可以使你的类在第一次使用时才初始化,合理使用这个注解,可以加快你的项目启动速度。
  2. @PostConstruct 和 @PreDestroy
    这两个注解属于bean生命周期的回调,分别是bean初始化后和bean销毁前会执行被加上注解的方法:
/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 16:31
 * @Description: 拥有car的people,且有drive方法
 */
@Component
@Lazy
public class People {
    @Resource(name = "BMWCar")
    private Car car;


    public People(){
        System.out.println("初始化");
    }

    @PostConstruct
    public void postConstruct(){
        System.out.println("初始化后");
    }

    @PreDestroy
    public void preDestroy(){
        System.out.println("销毁前");
    }

    public void drive(){
        System.out.println("今天开"+car.getCarName());
    }
}
执行结果:
初始化
初始化后
今天开宝马

由于销毁不太好演示,这里就没有演示销毁,总之,如果你需要在一个类实例化后或被销毁前执行什么逻辑就可以使用这两个注解。

看到这里,不知道原先以为IOC就是bean标签或者@Autowired注解的读者,是否对IOC的应用有了全新的认识,其实,关于IOC的应用还远不止这些,比如FactoryBean,ImportSelector,各种BeanPostProcessor和BeanFactoryPostProcessor的使用,我会在下一篇IOC进阶使用的文章中讲解,它们几乎都是参与了IOC容器实例化工厂或者实例化bean的过程,感兴趣的话可以关注笔者的后续文章。

《走进Spring框架系列—初识IOC》
《走进Spring框架系列—IOC应用》
《走进Spring框架系列—IOC应用进阶篇》
《走进Spring框架系列—IOC应用进阶实战》

本系列文章参考-------Spring官网以及Spring框架源码

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

推荐阅读更多精彩内容