【Spring】Spring 原理

1. 概述

本章主要介绍 Spring 以及 SpringBoot 框架当中一些关于 Bean 的常见面试题,主要分为以下三部分:

  1. Bean 的作用域
  2. Bean 的生命周期
  3. Bean 的自动装配流程

2. Bean 的作用域

我们都知道 Spring 框架内部使用 IoC 容器来管理 Bean,那么如果在不同的类中同时使用@Autowire注解或者使用 ApplicationContext 获取 Bean 的时候,获取到的对象是同一个吗?实际上 Spring 提供了六种作用域可供配置,后面四种只有在 SpringMVC 环境下才会生效

  1. singleton(单例):这是默认作用域,即实例只会创建一份
  2. prototype(原型):区别于单例,该模式每次会创建新的实例
  3. request(请求):在 web 环境下,同一请求内部只有一个实例,多个请求则会创建多个实例
  4. session(会话):在 web 环境下,同一会话内部的享有一份实例,不同会话则创建新实例
  5. application(应用):在 web 环境下,同一应用程序内部的享有一份实例,不同应用程序则创建新实例
  6. websocket:每个 WebSocket 生命周期都会创建新的实例

2.1 准备工作

  1. 准备 Dog 实体类
public class Dog {
    private String name;

    public Dog() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  1. 准备 Dog 的配置类
@Configuration
public class DogConfig {
    @Bean
    public Dog singleDog() {
        return new Dog();
    }
}

2.2 singleton 作用域

在 controller 中编写如下代码:

@RestController
@RequestMapping("/dog")
public class DogController {
    @Autowired
    private ApplicationContext context;
    @Autowired
    private Dog singleDog;

    @RequestMapping("/single")
    public String testSingleDog() {
        Dog contextDog = context.getBean("singleDog", Dog.class);
        return "singleDog:" + singleDog + ",contextDog:" + contextDog;
    }
}

运行代码并在浏览器上进行测试,验证 ApplicationContext 中获取的和依赖注入的对象是否相同,运行效果如下

可以发现,IoC 容器中获取的与依赖注入的对象是一致的,并且无论运行多少次代码,获取到的对象的 hashcode 均是一致的,说明默认的作用域是单例的,接下来我们观察如何显式指定使用singleton作用域,只需要在装配 Bean 的时候添加@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)注解,代码如下:

@Configuration
public class DogConfig {
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Dog singleDog() {
        return new Dog();
    }
}

2.3 prototype 作用域

接下来测试 prototype 作用域的情况,只需要将对应的注解修改为@Scope=ConfigurableBeanFactory.SCOPE_PROTOTYPE,对应 controller 新增部分代码如下:

@RestController
@RequestMapping("/dog")
public class DogController {
    //... 省略其余代码
    @Autowired
    private Dog prototypeDog;

    @RequestMapping("/prototype")
    public String testPrototypeDog() {
        Dog contextDog = context.getBean("prototypeDog", Dog.class);
        return "prototype:" + prototypeDog + ",contextDog:" + contextDog;
    }
}

运行多次,效果如上图所示,可以发现每次运行从 ApplicationContext 中获取到的对象都不一样,说明此时采用的是多例(原型)模式

💡 温馨提示:这里@Autowired注入的对象相同是因为,注入阶段在容器启动时,此时该对象是唯一的

2.4 request 作用域

接下来测试 request 作用域的情况,只需要将对应的注解修改为@RequestScope,对应 controller 新增部分代码如下:

@RestController
@RequestMapping("/dog")
public class DogController {
    @Autowired
    private Dog requestDog;

    @RequestMapping("/request")
    public String testRequestDog() {
        Dog contextDog = context.getBean("requestDog", Dog.class);
        return "request:" + requestDog + ",contextDog:" + contextDog;
    }
}

运行效果如上图所示,此时每次请求获取的对象是不一样的,但是ApplicatiuonContext 与依赖注入的对象是相同的

2.5 session 作用域

接下来测试 session 作用域的情况,只需要将对应的注解修改为@SessionScope,对应 controller 新增部分代码如下:

@RestController
@RequestMapping("/dog")
public class DogController {
    @Autowired
    private Dog sessionDog;

    @RequestMapping("/session")
    public String testSessionDog() {
        Dog contextDog = context.getBean("sessionDog", Dog.class);
        return "request:" + sessionDog + ",contextDog:" + contextDog;
    }
}

运行效果如上图所示,此时同一个会话内部多次请求获取的对象是一样的,且ApplicatiuonContext 与依赖注入的对象也相同的,但是当使用另一个浏览器窗口建立新会话时,获取到的对象是不同的,说明新建会话会创建新的对象

2.6 application 作用域

下来测试 application 作用域的情况,只需要将对应的注解修改为@ApplicationScope,对应 controller 新增部分代码如下:

@RestController
@RequestMapping("/dog")
public class DogController {
    @Autowired
    private Dog applicationDog;

    @RequestMapping("/application")
    public String testApplicationDog() {
        Dog contextDog = context.getBean("applicationDog", Dog.class);
        return "application:" + applicationDog + ",contextDog:" + contextDog;
    }
}

运行效果如上图所示,此时多个浏览器窗口访问得到的对象都是一样的,说明在整个应用程序内部实例只有一份

💡 温馨提示:application作用域与singleton的区别就是,singleton在servlet作用域内是单例的,但是在一个web容器内部可以有多个应用程序

3. Bean 的生命周期

3.1 生命周期

Bean 生命周期:指的是一个 Bean 实例从诞生到消亡的全过程,虽然官网没有明确指出 Bean 的生命周期,但是根据源码分析可以看出主要经历会以下五个阶段:

  1. 实例化(为 Bean 开辟内存空间、调用构造方法)
  2. 属性赋值:内部如果有属性需要注入则进行装配
  3. 初始化:
    1. 执行各种通知方法,比如BeanNameAwareBeanFactoryAware等接口方法
    2. 执行初始化方法,比如 XML 定义的 init-method 方法,@PostConstruct@BeanPostProcessor
  4. 使用阶段
  5. 销毁阶段,比如@PreDestroy注解

3.2 代码演示

接下来就通过编写代码的方式来验证上述生命周期的执行流程,相关代码如下:

@Slf4j
@Component
public class BeanLifeComponent implements BeanNameAware {
    private static final Logger logger = LoggerFactory.getLogger(BeanLifeComponent.class);
    public Dog dog;

    public BeanLifeComponent() {
        logger.info("执行构造方法...");
    }

    @Autowired
    @Qualifier("singleDog")
    public void setDog(Dog dog) {
        logger.info("执行属性注入...");
        this.dog = dog;
    }

    public void use() {
        logger.info("bean使用中...");
    }

    @Override
    public void setBeanName(String name) {
        logger.info("设置BeanName为:"+ name);
    }

    @PostConstruct
    public void postConstruct() {
        logger.info("执行后置方法...");
    }

    @PreDestroy
    public void preDestroy() {
        logger.info("即将执行销毁方法...");
    }
}
@RestController
@RequestMapping("/life")
public class BeanLifeController {
    @Autowired
    private BeanLifeComponent beanLifeComponent;

    @RequestMapping("/use")
    public String use() {
        beanLifeComponent.use();
        return "OK";
    }
}

代码运行效果如下所示:

该效果与我们列举的生命周期全流程相符合

3.3 源码分析

具体代码在<font style="color:rgb(31,35,41);">AbstractAutowireCapableBeanFactory</font>#createBean方法中,在这个方法中调用了核心方法:<font style="color:rgb(31,35,41);">doCreateBean</font>,内部代码经过抽象提炼后如下所示:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
    if (instanceWrapper == null) {
        // 1.实例化流程(省略其他代码...)
        instanceWrapper = this.createBeanInstance(beanName, mbd, args);
    }
    Object exposedObject = bean;
    try {
        // 2.属性装配流程(省略其他代码...) 
        this.populateBean(beanName, mbd, instanceWrapper);
        // 3.初始化流程(省略其他代码...) 
        exposedObject = this.initializeBean(beanName, exposedObject, mbd);
    } catch (Throwable var18) {
        if (var18 instanceof BeanCreationException bce) {
            if (beanName.equals(bce.getBeanName())) {
                throw bce;
            }
        }
        throw new BeanCreationException(mbd.getResourceDescription(), beanName, var18.getMessage(), var18);
    }
}

其中 createBeanInstance、populateBean、initializeBean 各自对应三个阶段,我们继续追进initializeBean方法,内部代码如下:

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
    // 1. 调用通知方法
    this.invokeAwareMethods(beanName, bean);
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
    }
    // 2. 调用初始化方法
    try {
        this.invokeInitMethods(beanName, wrappedBean, mbd);
    } catch (Throwable var6) {
        throw new BeanCreationException(mbd != null ? mbd.getResourceDescription() : null, beanName, var6.getMessage(), var6);
    }

    if (mbd == null || !mbd.isSynthetic()) {
        wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }

    return wrappedBean;
}

可以发现在初始化流程中,主要进行了调用各类通知方法以及初始化方法

4. Bean 自动装配流程

4.1 装配代码

现在有一个问题,在 SpringBoot 项目中我们可以直接注入一些特殊的 Bean ,比如 redis 组件的 RedisTemplate,说明该 bean 已经被注册到 IoC 容器当中,如果我们想要注入自己的依赖应该怎么做呢?

4.1.1 方式一

MyBean.java:模拟外部包需要注册到 Ioc 容器的bean

package org.test;

import org.springframework.stereotype.Component;

@Component
public class MyBean {
}

MyBeanController:使用 MyBean 的类(依赖注入)

@RestController
public class MyBeanController {
    @Autowired
    private MyBean bean;
}

PrincipleApplication:启动类,配置组件扫描路径

@ComponentScan({"org.test", "com.rice.principle"})
@SpringBootApplication
public class PrincipleApplication {

    public static void main(String[] args) {
        SpringApplication.run(PrincipleApplication.class, args);
    }

}
4.1.2 方式二

除了上述使用 ComponentScan 主动配置扫描策略外,还可以使用@Import注解主动导入所需的类,代码如下:

@Import(MyBean.class)
@SpringBootApplication
public class PrincipleApplication {
    public static void main(String[] args) {
        SpringApplication.run(PrincipleApplication.class, args);
    }
}
4.1.3 方式三

方式一与方式二缺点:

  • 使用方需要主动使用@ComponentScan以及@Import配置扫描路径和导入类
  • 使用方导入一个外部包就需要配置一次扫描路径和导入一个文件

可以考虑另一种策略,由第三方依赖管理所有需要被扫描的路径,这就需要第三方依赖实现@ImportSelector接口及其对应方法

MyBeanImportSelector:告诉启动类需要扫描哪些路径

@Component
public class MyBeanImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{
                "org.test.MyBean"
        };
    }
}

PrincipleApplication:启动类代码

@Import(MyBeanImportSelector.class)
@SpringBootApplication
public class PrincipleApplication {
    public static void main(String[] args) {
        SpringApplication.run(PrincipleApplication.class, args);
    }
}
4.1.4 方式四

上述代码还可以继续优化,实际上可以由第三方依赖提供注解(将所需要的 bean 装配到 spring 当中),解决策略就是让第三方依赖提供一个注解,启动类只需要使用该注解即可

EnableMyBean:封装注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyBeanImportSelector.class)
public @interface EnableMyBean {
}

EnableMyBean:启动类,使用该注解

@EnableMyBean
@SpringBootApplication
public class PrincipleApplication {
    public static void main(String[] args) {
        SpringApplication.run(PrincipleApplication.class, args);
    }
}

这也是 Spring 使用的方式!

4.2 源码分析

剩下的问题就是 SpringBoot 是如何将所依赖的组件的 bean 装配到 IoC 容器当中的呢?这一切都要从 @SpringBootApplication这个注解说起,让我们一起来分析源码:

可以发现有如下三个子注解:

  1. @ComponentScan:配置扫描包的路径,这里没有显式声明则表示扫描启动类下的包
  2. @SpringBootConfiguration:内部使用了@Configuration注解,标识这是一个 Bean
  3. @EnableAutoConfiguration:这是自动装配的核心注解

我们继续深入@EnableAutoConfiguration注解分析,发现内部又有下面子注解:

  1. @Import(AutoConfigurationImportSelector.class)这个代码我们已经很熟悉了,现在我们看看内部到底导入了哪些扫描路径,

其中有一行代码是值得关注的:

  • List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

从该源码可以看出其中一个扫描配置路径就是META-INF/spring/...

  1. @AutoConfigurationPackage该注解导入配置文件 AutoConfigurationPackages.Registrar.class

所以该注解主要导入了启动类包下全部的组件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值