Spring基本使用及原理剖析之IoC

Spring 是个轻量级开源框架,主要以 IoC(Inverse Of Control: 控制反转)和 AOP(Aspect Oriented Programming:面向切面编程)为内核的容器框架。作为业界使用框架中的基础框架,我一直只是简单应用而未曾有过深入挖掘,这次有些时间就做一个学习总结, 对 Spring 内部原理进行剖析并分享出来~

分享内容大致分为:IoC 的基本使用,IoC 的配置技巧,IoC 基本原理剖析

一、IoC 的基本使用

Spring主要是以IoCAOP为内核的容器框架,那么IoC是干什么用的,AOP又是干什么用的呢?

在讲SpringIoCAOP之前我们先介绍下“耦合”这个概念,所谓程序的耦合就是程序之间的依赖关系,例如类与类之间的依赖、方法与方法之间的依赖等。而耦合严重会使开发效率低下、后续维护困难,造成部分功能变更影响到所有功能的改动,简直是一改毁所有,所以我们需要解耦来降低程序之间的依赖关系。

那么哪种情况算是耦合?我们又如何解耦呢?首先我们应避免使用new来创建对象,推荐使用反射来创建对象;其次在代码中避免将一些参数配置写死,而推荐使用读取配置文件的方式来加载参数。这里我们可以通过工厂模式来实例化对象,搭配单例模式还可避免消耗过多的资源,这样一波骚操作后我们降低了程序之间的依赖关系达到了解耦的目的,而让我们为每个对象去编写这样太麻烦了,所以SpringIoC控制反转就闪亮登场了。

IoC控制反转的作用正是削减程序间的耦合,解除代码中的依赖关系。它可以理解为是一个工厂类,内部有一个Map存放着对象,使用时直接获取即可。为什么管它叫控制反转呢?因为原来我们获取对象时都是采用new的方式,是主动的;现在获取对象时跟工厂要,由工厂为我们查找或创建对象,是被动的;这种被动接收的方式获取对象的思想就叫做控制反转。下面我们来看看IoC是如何使用的~

1. 新建工程并导入jar包

为了简单方便,我们通过Maven来管理jar包。spring-context中包含了spring-corespring-beansspring-expressionspring-aop,因此仅配置spring-context即可

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

2. 编写相关接口及实现

持久层接口。随便写个save方法作为测试方法

/**
 * 持久层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringDao {

    /**
     * 保存数据
     */
    void save();
}

持久层接口实现类。通常情况下此实现类将省略不会编写,一般会使用MyBatisXML映射文件配置SQL,或者Hibernate自动生成SQL,这里通过打印结果来模拟访问数据库

/**
 * 持久层接口实现
 *
 * @author Wenx
 * @date 2021/3/27
 */
public class SpringDaoImpl implements ISpringDao {
    public SpringDaoImpl() {
        System.out.println("SpringDaoImpl对象实例化了……");
    }

    public void save() {
        System.out.println("保存成功!");
    }
}

业务层接口。为调用持久层方法写个同名方法(最烦起名字……)

/**
 * 业务层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringService {

    /**
     * 保存数据
     */
    void save();
}

业务层接口实现类。我们应避免使用new来创建对象,这里使用Spring的构造函数注入来为成员变量赋值,还可以使用set方法注入、@autowired注解注入等方式,其他方式在后面会有相应介绍

/**
 * 业务层接口实现
 *
 * @author Wenx
 * @date 2021/3/27
 */
public class SpringServiceImpl implements ISpringService {
    private ISpringDao springDao;

    /**
     * 构造函数注入(依赖注入)
     *
     * @param springDao 持久层接口实现
     */
    public SpringServiceImpl(ISpringDao springDao) {
        this.springDao = springDao;

        System.out.println("SpringServiceImpl对象实例化了……");
    }

    public void save() {
        springDao.save();
    }
}

3. 配置文件的相关设置

配置文件SpringConfig.xml(自定义名称)。用来设置Bean所在的全限定类名和相应变量的依赖注入,DI(Dependency Injection: 依赖注入)它是Spring框架核心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和class属性(bean所在的全限定类名) -->
    <bean id="springDao" class="com.wenx.demo.dao.impl.SpringDaoImpl"></bean>
    <!-- 有成员变量赋值还需配置依赖注入,通过spring框架来赋值 -->
    <bean id="springService" class="com.wenx.demo.service.impl.SpringServiceImpl">
        <!-- 构造函数注入 -->
        <constructor-arg name="springDao" ref="springDao"></constructor-arg>
    </bean>
</beans>

4. 编写IoC测试代码

我们应避免使用new来创建对象,所以通过Spring来管理Bean对象。使用时通过IoC容器来获取Bean对象,这样做的目的是为了削减程序间的耦合,解除代码中的依赖关系

/**
 * @author Wenx
 * @date 2021/3/27
 */
public class SpringDemo {
    public static void main(String[] args) {
        // 1.获取IoC核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("SpringConfig.xml");
        // 2.获取bean对象
        ISpringService ss = ac.getBean("springService", ISpringService.class);
        // 3.调用对象方法
        ss.save();

        System.out.println(ss);
    }
}

二、IoC 的配置技巧

IoC有两种配置方式,分别是基于XML配置和基于注解配置。XML配置文件像个目录让Bean之间的关系一目了然但配置稍繁琐,注解方式配置较简便但Bean之间的关系展现不清晰,使用XML配置还是注解配置需要根据实际情况选择

1. 基于XML的Bean配置

Spring通过XML配置文件来描述BeanBean之间的依赖关系,了解XML配置中的标签及属性有利于深入理解IoC运行方式,有助于熟练掌握IoC使用技巧

1). 导入XML配置约束

XML配置约束可以从官网找到,然后直接在XML文件中导入Bean的约束

<?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">    
</beans>
2). 实例化Bean的三种方式

创建Bean对象根据不同情况有三种方式:默认无参构造函数、静态工厂方法、实例工厂方法

<?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:指定bean的id,用于从容器中获取对象
         class:指定类的全限定类名,用于反射创建对象,默认调用无参构造函数
         scope:指定对象的作用范围。singleton:单例(默认);prototype:多例
         lazy-init:是否懒加载(延迟加载),默认立即加载
         init-method:指定类中的初始化方法
         destroy-method:指定类中销毁方法
    -->
    <bean id="beanService"
          class="com.wenx.demo.service.impl.BeanServiceImpl"
          scope="singleton"
          lazy-init="true"
          init-method="init"
          destroy-method="destroy">
    </bean>

    <!-- 实例化Bean的三种方式 -->
    <!-- 1.使用默认无参构造函数。
         bean标签仅配置id和class属性,但未配置其他属性和标签时,使用默认构造函数创建bean对象。
         如果类中没有默认构造函数,则对象创建失败。
    -->
    <bean id="bean1Service" class="com.wenx.demo.service.impl.BeanServiceImpl"></bean>

    <!-- 2.使用静态工厂的方法创建对象(使用某个类中的静态方法创建对象,并交给spring容器管理)
         id:指定bean的id,用于从容器中获取对象
         class:指定静态工厂的全限定类名
         factory-method:指定生产对象的静态方法
    -->
    <bean id="bean2Service" class="com.wenx.demo.factory.StaticFactory" factory-method="getBeanService"></bean>

    <!-- 3.使用实例工厂的方法创建对象(使用某个类中的方法创建对象,并交给spring容器管理)
         先创建工厂交给spring容器管理,然后调用工厂里面的方法来创建bean对象
         id:指定bean的id,用于从容器中获取对象
         class:指定静态工厂的全限定类名
         factory-bean:指定实例工厂bean的id
         factory-method:指定实例工厂中创建对象的方法
    -->
    <bean id="instanceFactory" class="com.wenx.demo.factory.InstanceFactory"></bean>
    <bean id="bean3Service" factory-bean="instanceFactory" factory-method="getBeanService"></bean>
</beans>

业务层接口。使用之前的测试接口即可

/**
 * 业务层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringService {

    /**
     * 保存数据
     */
    void save();
}

业务层接口实现类。我们为不同生命周期编写了触发方法

/**
 * @author Wenx
 * @date 2021/3/31
 */
public class BeanServiceImpl implements ISpringService {

    public BeanServiceImpl() {
        System.out.println("BeanServiceImpl对象实例化了……");
    }

    public void save() {
        System.out.println("BeanServiceImpl的save方法执行了……");
    }

    public void init() {
        System.out.println("BeanServiceImpl对象初始化了……");
    }

    public void destroy() {
        System.out.println("BeanServiceImpl对象销毁了……");
    }
}

静态工厂类。通过使用静态工厂的方式创建对象

/**
 * 模拟一个静态工厂,创建业务层实现类(部分类可能存在jar包中无默认构造函数)
 *
 * @author Wenx
 * @date 2021/3/31
 */
public class StaticFactory {

    public static ISpringService getBeanService() {
        return new BeanServiceImpl();
    }
}

实例工厂类。通过使用实例工厂的方式创建对象

/**
 * 模拟一个实例工厂,创建业务层实现类(部分类可能存在jar包中无默认构造函数)
 * 此工厂创建对象,必须现有工厂实例对象,再调用方法
 *
 * @author Wenx
 * @date 2021/3/31
 */
public class InstanceFactory {

    public ISpringService getBeanService() {
        return new BeanServiceImpl();
    }
}

测试代码

/**
 * @author Wenx
 * @date 2021/3/31
 */
public class BeanDemo {
    public static void main(String[] args) {
        // ApplicationContext没有close方法,为触发对象的销毁方法我们使用ClassPathXmlApplicationContext
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("BeanConfig.xml");
        // bean配置中lazy-init置为true开启延时加载,此处可断点测试
        ISpringService ss1 = (ISpringService) ac.getBean("beanService");
        // 调用对象方法
        ss1.save();
        System.out.println(ss1);

        // 再次获取bean检测是否单例
        ISpringService ss2 = ac.getBean("beanService", ISpringService.class);
        System.out.println(ss2);

        // 单例对象随容器的销毁而消亡,多例对象由Java的垃圾回收器回收
        ac.close();
    }
}
3). spring的依赖注入

DI(Dependency Injection: 依赖注入)它是Spring框架核心IoC的具体实现,它有三种注入方式:构造函数注入、set方法注入、注解方式注入(后面会有相应介绍)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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">
    <!-- 依赖注入的三种方式 -->
    <!-- 1.构造函数注入,使用类中的构造函数给成员变量赋值,通过配置的方式让spring框架来为我们注入。
         标签:constructor-arg
         其属性:
                index:指定参数在构造函数参数列表的索引位置
                type:指定参数在构造函数中的数据类型
                name:指定参数在构造函数中的名称
                value:指定参数的值,可以是基本数据类型和String类型
                ref:指定参数的值,可以是其他配置的bean等引用类型
    -->
    <bean id="di1Service" class="com.wenx.demo.service.impl.DI1ServiceImpl">
        <constructor-arg name="name" value="张三"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
        <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>

    <bean id="now" class="java.util.Date"></bean>

    <!-- 2.set方法注入,使用set方法给类中的成员变量赋值,通过配置的方式让spring框架来为我们注入。
         标签:property
         其属性:
                name:指定类中set方法后面的部分
                value:指定参数的值,可以是基本数据类型和String类型
                ref:指定参数的值,可以是其他配置的bean等引用类型
    -->
    <bean id="di21Service" class="com.wenx.demo.service.impl.DI2ServiceImpl">
        <property name="name" value="张三"></property>
        <property name="age" value="18"></property>
        <property name="birthday" ref="now"></property>
    </bean>

    <!-- 使用名称空间注入数据,本质还是set方法注入 -->
    <bean id="di22Service"
          class="com.wenx.demo.service.impl.DI2ServiceImpl"
          p:name="张三" p:age="18" p:birthday-ref="now">
    </bean>

    <!-- 注入集合属性,本质还是set方法注入 -->
    <bean id="di3Service" class="com.wenx.demo.service.impl.DI3ServiceImpl">
        <!-- 在注入集合数据时,只要结构相同,标签可以互换 -->
        <!-- 给数组注入数据 -->
        <property name="strings">
            <array>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </array>
        </property>
        <!-- 注入list集合数据 -->
        <property name="lists">
            <list>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </list>
        </property>
        <!-- 注入set集合数据 -->
        <property name="sets">
            <set>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </set>
        </property>
        <!-- 注入Map数据 -->
        <property name="maps">
            <map>
                <entry key="testA" value="aaa"></entry>
                <entry key="testB">
                    <value>bbb</value>
                </entry>
            </map>
        </property>
        <!-- 注入properties数据 -->
        <property name="props">
            <props>
                <prop key="testA">aaa</prop>
                <prop key="testB">bbb</prop>
            </props>
        </property>
    </bean>

    <!-- 3.使用注解方式注入,后面会有相应介绍。 -->
</beans>

业务层接口。使用之前的测试接口即可

/**
 * 业务层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringService {

    /**
     * 保存数据
     */
    void save();
}

构造函数注入。通过编写有参构造函数的方式进行注入

/**
 * @author Wenx
 * @date 2021/3/31
 */
public class DI1ServiceImpl implements ISpringService {
    private String name;
    private Integer age;
    private Date birthday;

    /**
     * 构造函数注入(依赖注入)
     *
     * @param name     姓名
     * @param age      年龄
     * @param birthday 生日
     */
    public DI1ServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public void save() {
        System.out.println("DI1ServiceImpl的save方法执行了……\r\n" +
                "姓名:" + name + ",年龄:" + age + ",生日:" + birthday);
    }
}

set方法注入。通过编写set方法的方式进行注入

/**
 * @author Wenx
 * @date 2021/3/31
 */
public class DI2ServiceImpl implements ISpringService {
    private String name;
    private Integer age;
    private Date birthday;

    /**
     * set方法注入(依赖注入)
     *
     * @param name 姓名
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * set方法注入(依赖注入)
     *
     * @param age 年龄
     */
    public void setAge(Integer age) {
        this.age = age;
    }

    /**
     * set方法注入(依赖注入)
     *
     * @param birthday 生日
     */
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public void save() {
        System.out.println("DI2ServiceImpl的save方法执行了……\r\n" +
                "姓名:" + name + ",年龄:" + age + ",生日:" + birthday);
    }
}

复杂类型注入,也称集合类型注入。本质上还是set方法注入,通过编写set方法的方式进行注入

/**
 * @author Wenx
 * @date 2021/3/31
 */
public class DI3ServiceImpl implements ISpringService {
    private String[] strings;
    private List<String> lists;
    private Set<String> sets;
    private Map<String, String> maps;
    private Properties props;

    public void setStrings(String[] strings) {
        this.strings = strings;
    }

    public void setLists(List<String> lists) {
        this.lists = lists;
    }

    public void setSets(Set<String> sets) {
        this.sets = sets;
    }

    public void setMaps(Map<String, String> maps) {
        this.maps = maps;
    }

    public void setProps(Properties props) {
        this.props = props;
    }

    public void save() {
        System.out.println("DI3ServiceImpl的save方法执行了……");
        System.out.println("strings:" + Arrays.toString(strings));
        System.out.println("lists:" + lists);
        System.out.println("sets:" + sets);
        System.out.println("maps:" + maps);
        System.out.println("props:" + props);
    }
}

依赖注入测试代码

/**
 * @author Wenx
 * @date 2021/3/31
 */
public class DIDemo {
    public static void main(String[] args) {
        // 1.获取IoC核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("DIConfig.xml");
        // 2.获取bean对象
        ISpringService ss = ac.getBean("di1Service", ISpringService.class);
        // 3.调用对象方法
        ss.save();

        System.out.println(ss);
    }
}

2. 基于注解的IoC配置

基于注解配置是通过使用注解的方式来达到使用XML文件配置相同的效果,相比基于XML配置来说基于注解的IoC配置更为简便,所以对于厌烦了通过XML文件进行配置的同学来讲基于注解配置也是个不错的选择

1). 配置Spring扫描包的范围

基于注解配置是通过Spring自动扫描包中带注解的类来实例化Bean对象,虽然不需要向基于XML配置那样手动配置Bean,但还是要在XML文件中指定下Spring扫描包的范围。那么有同学就说了这不还是使用XML文件了吗?全部用注解进行配置脱离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"
       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
                           http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 指定Spring在创建Bean时要扫描的包 -->
    <context:component-scan base-package="com.wenx.demo"></context:component-scan>
</beans>
2). 用于创建对象的注解

@Component:用于把当前类对象交给Spring来管理,相当于<bean id="" class="">

@Component注解的value属性用于指定beanid,不指定时默认就是当前类名,且首字母变小写。

为了明确三层结构Spring框架还提供了@Controller@Service@Repository这三个注解,他们与@Component的使用方法及作用完全一样只是名称各有不同,主要用来区分表现层、业务层和持久层。

/**
 * 表现层
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Controller
public class AnnotationController {
    public void save() {
        System.out.println("AnnotationController的save方法执行了……");
    }
}
/**
 * 业务层
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Service("annotationService")
public class AnnotationServiceImpl implements ISpringService {
    public void save() {
        System.out.println("AnnotationServiceImpl的save方法执行了……");
    }
}
/**
 * 持久层
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Repository("annotationDao")
public class AnnotationDaoImpl implements ISpringDao {
    public void save() {
        System.out.println("AnnotationDaoImpl的save方法执行了……");
    }
}
3). 用于注入数据的注解

@Autowired:用于按照变量类型匹配bean对象,若匹配结果唯一则注入成功,否则注入失败。相当于<property name="" ref=""><property name="" value="">,但不需要编写set方法。

@Qualifier:用于在按照变量类型基础上再按照变量名称匹配bean对象并自动注入,@Qualifier注解的value属性用于指定beanid

@Resource:用于直接指定beanid注入,其属性为的name属性。

@Value:用于注入基本数据类型和String类型,前三个注解只能注入bean对象,而集合类型的注入只能使用XML配置。@Value注解的value属性用于指定数据的值,它还可以使用SpEL表达式(写法:${表达式})

/**
 * 业务层
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Service("annotationService")
public class AnnotationServiceImpl implements ISpringService {

    @Autowired
    private ISpringDao annotationDao1;

    @Autowired
    @Qualifier("annotationDao")
    private ISpringDao annotationDao2;

    @Resource(name = "annotationDao")
    private ISpringDao annotationDao3;

    @Value("root")
    //@Value("${jdbc.username}")
    private String username;

    public void save() {
        System.out.println("AnnotationServiceImpl的save方法执行了……");

        annotationDao1.save();
        annotationDao2.save();
        annotationDao3.save();

        System.out.println(username);
    }
}
4). 用于改变作用范围的注解

@Scope:用于指定bean的作用范围,@Scope注解的value属性包含: singleton 单例(默认)、prototype 多例,相当于<bean id="" class="" scope="">

/**
 * 业务层
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Service("annotationService")
@Scope("singleton")
public class AnnotationServiceImpl implements ISpringService {
    public void save() {
        System.out.println("AnnotationServiceImpl的save方法执行了……");
    }
}
5). 和生命周期相关的注解

@PostConstruct:用于指定类中的初始化方法, 相当于<bean id="" class="" init-method="" />

@PreDestroy:用于指定类中销毁方法, 相当于<bean id="" class="" destroy-method="" />

/**
 * 业务层
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Service("annotationService")
public class AnnotationServiceImpl implements ISpringService {
    public void save() {
        System.out.println("AnnotationServiceImpl的save方法执行了……");
    }

    @PostConstruct
    public void init() {
        System.out.println("AnnotationServiceImpl对象初始化了……");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("AnnotationServiceImpl对象销毁了……");
    }
}
6). 脱离XML文件的注解配置

在上面配置Spring扫描包的范围时我们还是要在XML文件中指定,这违背了我们选择注解方式配置的初衷,我既然使用了注解配置就不想使用XML文件了,那我该怎么弄呢?我们可以使用Spring的注解配置类。

@Configuration:用于指定当前类是一个配置类,当配置类作为AnnotationConfigApplicationContext对象创建的参数时该注解可以不写,获取容器时需要使用AnnotationApplicationContext(有@Configuration注解的类.class)

@ComponentScan:用于指定Spring在初始化容器时要扫描的包,@ComponentScan注解的value属性用于指定要扫描的包,相当于<context:component-scan base-package="com.xxxx"></context:component-scan>

@PropertySource:用于加载.properties文件中的配置,@PropertySource注解的value属性用于指定properties文件位置,若在类路径下需要加上classpath:前缀。

@Import:用于导入其他的配置类,@Import注解的value属性用于指定其他配置类的字节码,被引入的其他配置类可以不用再写@Configuration注解。

@Bean:用于把当前方法的返回值作为bean对象存入SpringIoC容器中,@Bean注解的name属性用于指定beanid,不指定时默认就是当前方法名称。

/**
 * @author Wenx
 * @date 2021/4/2
 */
public class AnnotationDemo {
    public static void main(String[] args) {
        // ApplicationContext没有close方法,为触发对象的销毁方法我们使用AnnotationConfigApplicationContext
        //ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("AnnotationConfig.xml");
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        // 获取bean对象
        ISpringService ss1 = (ISpringService) ac.getBean("annotationService");
        // 调用对象方法
        ss1.save();
        System.out.println(ss1);

        // 再次获取bean检测是否单例
        ISpringService ss2 = ac.getBean("annotationService", ISpringService.class);
        System.out.println(ss2);

        // 单例对象随容器的销毁而消亡,多例对象由Java的垃圾回收器回收
        ac.close();
    }
}

SpringConfiguration配置类

/**
 * Spring配置类
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Configuration
@ComponentScan("com.wenx.demo")
@PropertySource("classpath:SpringConfig.properties")
@Import(JdbcConfig.class)
public class SpringConfiguration {
}

JdbcConfig子配置类

/**
 * @author Wenx
 * @date 2021/4/2
 */
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean(name = "createBean")
    public String createBean() {
        return "AAA";
    }
}

SpringConfig.properties配置文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/study_demo
jdbc.username=root
jdbc.password=1234
7). Spring整合Junit4的单元测试

Spring整合Junit4进行单元测试时还会用到几个注解,这里也简单介绍一下。

@RunWith:用于指定启动时的运行器,替换成Spring提供的运行器,在启动时运行Spring容器从而支持IoC相关操作。

@ContextConfiguration:用于指定启动时配置文件,locations属性:指定xml文件的位置,在类路径下需加上classpath:前缀;classes属性:指定注解类的字节码。

@Before:指定执行测试之前运行的方法。

@After:指定执行测试之后运行的方法。

@Test:指定执行的测试方法。

注:先导入Junitjar包,使用spring 5.x版本的时候,要求Junitjar必须是4.12及以上

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.4</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.4</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

单元测试代码

/**
 * Spring整合Junit4进行单元测试
 *
 * @author Wenx
 * @date 2021/4/2
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AnnotationServiceImplTest {

    @Autowired
    private ISpringService annotationService;

    @Before
    public void setUp() throws Exception {
        System.out.println("执行测试方法之前运行……");
    }

    @After
    public void tearDown() throws Exception {
        System.out.println("执行测试方法之后运行……");
    }

    @Test
    public void save() {
        annotationService.save();
    }
}

三、IoC 基本原理剖析

在上面我们讲了SpringIoC的基本使用和IoC的配置技巧,这里我们将对IoC的基本原理进行剖析。Spring通过配置文件来描述BeanBean之间的依赖关系,利用反射实例化Bean对象,并使用IoC容器来管理。下面通过对源码断点跟踪进行简要分析,从而了解其内部的基本运行原理

1. IoC核心容器初始化

通常我们使用ApplicationContext来获取Bean对象,查看类图发现它是由BeanFactory接口派生而来,而BeanFactory就是我们所说的IoC容器。ApplicationContext接口的实现排除Abstract前缀的抽象类,常用的有ClassPathXmlApplicationContextFileSystemXmlApplicationContextAnnotationConfigApplicationContext这三种。通过对源码的简要分析发现可以将BeanFactory理解为是一个使用Map存放Bean对象的工厂,调用getBean方法可从Map中获取Bean对象

// 1.通过加载类路径下的配置文件,获取IoC核心容器对象
ApplicationContext ac1 = new ClassPathXmlApplicationContext("SpringConfig.xml");

// 2.通过加载磁盘路径下的配置文件(需访问权限),获取IoC核心容器对象
ApplicationContext ac2 = new FileSystemXmlApplicationContext("D:\\SpringConfig.xml");

// 3.通过读取注解配置类,获取IoC核心容器对象
ApplicationContext ac3 = new AnnotationConfigApplicationContext(SpringConfiguration.class);

2. ClassPathXmlApplicationContext实现类

本次主要分析ClassPathXmlApplicationContext类这种实现方式,按住ctrl键对着ClassPathXmlApplicationContext一路连点,发现super(parent)中没什么有用的,而起作用的是this.refresh()这行,此方法在AbstractApplicationContext抽象类中

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    this.setConfigLocations(configLocations);
    if (refresh) {
        this.refresh();
    }
}

3. AbstractApplicationContext抽象类

AbstractApplicationContext类的refresh方法中对ApplicationContext容器进行了逐步初始化,在this.finishBeanFactoryInitialization(beanFactory)方法中发现是利用反射将配置中的全限定类名实例化对象并存放到Map

public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
        this.prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        this.prepareBeanFactory(beanFactory);
        try {
            this.postProcessBeanFactory(beanFactory);
            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
            this.invokeBeanFactoryPostProcessors(beanFactory);
            this.registerBeanPostProcessors(beanFactory);
            beanPostProcess.end();
            this.initMessageSource();
            this.initApplicationEventMulticaster();
            this.onRefresh();
            this.registerListeners();
            this.finishBeanFactoryInitialization(beanFactory);
            this.finishRefresh();
        } catch (BeansException var10) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
            }
            this.destroyBeans();
            this.cancelRefresh(var10);
            throw var10;
        } finally {
            this.resetCommonCaches();
            contextRefresh.end();
        }
    }
}

AbstractApplicationContext类的finishBeanFactoryInitialization方法中beanFactory.preInstantiateSingletons()这行内部发现是通过单例模式来实例化对象。Spring默认是单例模式并立即加载对象,可通过配置文件进行相关设置

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    if (beanFactory.containsBean("conversionService") && beanFactory.isTypeMatch("conversionService", ConversionService.class)) {
        beanFactory.setConversionService((ConversionService)beanFactory.getBean("conversionService", ConversionService.class));
    }
    if (!beanFactory.hasEmbeddedValueResolver()) {
        beanFactory.addEmbeddedValueResolver((strVal) -> {
            return this.getEnvironment().resolvePlaceholders(strVal);
        });
    }
    String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
    String[] var3 = weaverAwareNames;
    int var4 = weaverAwareNames.length;
    for(int var5 = 0; var5 < var4; ++var5) {
        String weaverAwareName = var3[var5];
        this.getBean(weaverAwareName);
    }
    beanFactory.setTempClassLoader((ClassLoader)null);
    beanFactory.freezeConfiguration();
    beanFactory.preInstantiateSingletons();
}

4. DefaultListableBeanFactory类

DefaultListableBeanFactory类的preInstantiateSingletons方法中,先通过Bean定义尝试获取Bean对象,若获取为null则实例化对象并存放到Map

public void preInstantiateSingletons() throws BeansException {
    if (this.logger.isTraceEnabled()) {
        this.logger.trace("Pre-instantiating singletons in " + this);
    }
    List<String> beanNames = new ArrayList(this.beanDefinitionNames);
    Iterator var2 = beanNames.iterator();
    while(true) {
        String beanName;
        Object bean;

        // 省略部分代码……
        
                this.getBean(beanName);
        
        // 省略部分代码……
    }
}

5. AbstractBeanFactory抽象类

AbstractBeanFactory类的doGetBean方法中,通过this.getSingleton(beanName)方法从Map中获取对象,若获取为null则使用this.createBean(beanName, mbd, args)创建Bean对象并存放到Map中,createBean内部就是实例化对象这里不再过多分析

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    String beanName = this.transformedBeanName(name);
    Object sharedInstance = this.getSingleton(beanName);
    Object beanInstance;
    if (sharedInstance != null && args == null) {
        // 省略部分代码……
    } else {
        // 省略部分代码……
        try {
            // 省略部分代码……
            
            if (mbd.isSingleton()) {
                sharedInstance = this.getSingleton(beanName, () -> {
                    try {
                        return this.createBean(beanName, mbd, args);
                    } catch (BeansException var5) {
                        this.destroySingleton(beanName);
                        throw var5;
                    }
                });
                beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            } else if (mbd.isPrototype()) {
            	// 省略部分代码……
            } else
            	// 省略部分代码……
            }
        } catch (BeansException var32) {
            // 省略部分代码……
        } finally {
            // 省略部分代码……
        }
    }
    return this.adaptBeanInstance(name, beanInstance, requiredType);
}

6. DefaultSingletonBeanRegistry类

DefaultSingletonBeanRegistry类的getSingleton方法中,singletonFactory.getObject()这行就是在用工厂方法实例化对象,最后通过addSingleton方法添加到Map

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized(this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            // 省略部分代码……
            
            try {
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            } catch (IllegalStateException var16) {
                // 省略部分代码……
            } catch (BeanCreationException var17) {
                // 省略部分代码……
            } finally {
                // 省略部分代码……
            }

            if (newSingleton) {
                this.addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

DefaultSingletonBeanRegistry类的addSingleton方法中,可以看到this.singletonObjects.put(beanName, singletonObject)这行在进行put操作,查看singletonObjects的定义发现它其实就是个Map

private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized(this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

7. 获取Bean对象

回到最外层的测试代码,通过ApplicationContextgetBean方法可获取Bean对象,其内部就是从Map中获取对象

// 通过加载类路径下的配置文件,获取IoC核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("SpringConfig.xml");

// 获取bean对象
ISpringService ss = ac.getBean("springService", ISpringService.class);

// 调用对象方法
ss.save();

总结: 我们本次由浅入深的讲解了IoC的基本使用、配置技巧和基本原理剖析,当然还有更深度的原理及使用技巧等着大家在使用中去自行发现。在此感谢大家能看将本篇文章看完,后面还有篇关于SpringAOP的文章还正在酝酿中,敬请期待~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值