[demo 版本 ]
[TOC]
0x01 Spring 自动发现Bean配置的时间(Spring的启动流程)
首先通过资源定位来找到配置文件(xml注入),然后使用BeanDefinition载入和解析、注册BeanDefinition、接着对Bean进行实例化、在Bean被使用的时候(getBean()调用时再注入熟悉,懒加载的特性)进行依赖注入
0x02 什么时候需要@ComponentScan注解
1、 @ComponentScan注解是什么
其实很简单,@ComponentScan主要就是定义扫描的路径从中找出标识了需要装配的类自动装配到spring的bean容器中
2、@ComponentScan注解的详细使用
做过web开发的同学一定都有用过@Controller,@Service,@Repository注解,查看其源码你会发现,他们中有一个共同的注解@Component,没错@ComponentScan注解默认就会装配标识了@Controller,@Service,@Repository,@Component注解的类到spring容器中,好下面咱们就先来简单演示一下这个例子
所以在Spring中我们需要一个Config类来启动组件扫描
1 | // DEMO |
3、SpringBoot中的@ComponetScan
在SpringBoot 工程中Application类的位置。默认情况下就不需要配置@ComponentScan这个注解了。 因为Application类,在启动的时候,默认是加载和Application类所在同一个目录下的所有类,包括所有子目录下的类。所以一般情况下,启动类的位置是
有特殊要求的。
重要⚠️
假设写的模块,编译成了jar包,并且上传到了私服。在pom中以第三方包的形式依赖进来。
如果jar包中也存在注解,为了spring boot启动的时候,注解可以被扫描到,需要做的就是
在spring boot启动类中配置上 @ComponentScan注解,并且指定第三方jar包的根路径。
所以一般情况下,公司级的第三包前面的路径名最好一直。比方说 com.pa 这样就不会出现遗漏的情况。还有一个说法是,第三方包中最好不要使用注解,因为注解会带来调用者使用不方便,需要配置扫描注解等。如果遗漏指定包扫描路径,那么就会出bean加载失败的情况。所以我们看到的最基础的第三方jar包,几乎是没有注解的。
0x03 谈谈自己对于Spring IOC 和AOP的理解
IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spirng 特有。Spring 实现IOC的方式主要是通过 依赖注入(DI)来实现
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
0x04 Spring Bean实例的生命周期 && 初始化流程
实例化Bean对象
Spring容器根据配置中的Bean Definition来实例化Bean对象
Bean Definition 可以通过 XML,Java 注解或 Java Config 代码提供。
Spring 使用依赖注入填充所有属性,如 Bean 中所定义的配置。
Aware 相关的属性,注入到 Bean 对象
- 如果 Bean 实现 BeanNameAware 接口,则工厂通过传递 Bean 的 beanName 来调用
#setBeanName(String name)
方法。 - 如果 Bean 实现 BeanFactoryAware 接口,工厂通过传递自身的实例来调用
#setBeanFactory(BeanFactory beanFactory)
方法。
- 如果 Bean 实现 BeanNameAware 接口,则工厂通过传递 Bean 的 beanName 来调用
调用相应的方法,进一步初始化 Bean 对象
- 如果存在与 Bean 关联的任何 BeanPostProcessor 们,则调用
#preProcessBeforeInitialization(Object bean, String beanName)
方法。 - 如果 Bean 实现 InitializingBean 接口,则会调用
#afterPropertiesSet()
方法。 - 如果为 Bean 指定了 init 方法(例如
<bean />
的init-method
属性),那么将调用该方法。 - 如果存在与 Bean 关联的任何 BeanPostProcessor 们,则将调用
#postProcessAfterInitialization(Object bean, String beanName)
方法。
- 如果存在与 Bean 关联的任何 BeanPostProcessor 们,则调用
0x05 Spring IOC 解决循环依赖的方式
0x06 JDK 动态代理 && CGLIB 动态代理
Spring加载 bean 最初始的方法 AbstractBeanFactory 的 #doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
方法开始。
在 #doGetBean(...)
方法中,首先会根据 beanName
从单例 bean 缓存中获取,如果不为空则直接返回。其中调用了#getSingleton(String beanName, boolean allowEarlyReference)
方法,从单例缓存中获取。代码如下:
1 | // DefaultSingletonBeanRegistry.java |
- 这个方法主要是从三个缓存中获取,分别是:
singletonObjects
、earlySingletonObjects
、singletonFactories
。三者定义如下:
1 | // DefaultSingletonBeanRegistry.java |
singletonObjects
:单例对象的 Cache 。 (1级缓存)earlySingletonObjects
:提前曝光的单例对象的 Cache 。(2级缓存)singletonFactories
: 单例对象工厂的 Cache 。(3级缓存)
#getSingleton(String beanName, boolean allowEarlyReference)
方法,整个过程如下:
首先,从一级缓存
singletonObjects
获取。如果,没有且当前指定的
beanName
正在创建,就再从二级缓存earlySingletonObjects
中获取。如果,还是没有获取到且允许
singletonFactories
通过#getObject()
获取,则从三级缓存singletonFactories
获取。如果获取到,则通过其#getObject()
方法,获取对象,并将其加入到二级缓存earlySingletonObjects
中,并从三级缓存singletonFactories
删除。代码如下:1
2
3
4
5// DefaultSingletonBeanRegistry.java
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);- 这样,就从三级缓存升级到二级缓存了。
- 😈 所以,二级缓存存在的意义,就是缓存三级缓存中的 ObjectFactory 的
#getObject()
方法的执行结果,提早曝光的单例 Bean 对象。
3级缓存中数据的来源 addSingleonFactory
上面是从缓存中获取,但是缓存中的数据从哪里添加进来的呢?一直往下跟会发现在 AbstractAutowireCapableBeanFactory 的 #doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
方法中,有这么一段代码:
1 | // AbstractAutowireCapableBeanFactory.java |
- 当一个 Bean 满足三个条件时,则调用
#addSingletonFactory(...)
方法,将它添加到缓存中。三个条件如下:- 单例
- 运行提前暴露 bean
- 当前 bean 正在创建中
具体的诸如1级缓存在哪获得bean就不展开分析了
@@@@@@@@@@@@@@@@@@@@@@
小结
Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories
缓存中)。这样,一旦下一个 bean 创建的时候需要依赖 bean ,则直接使用 ObjectFactory 的 #getObject()
方法来获取了。
循环依赖Spring 的解决方案:
- 首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建出来
- 然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来
- 这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于是尝试 get(A),这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存
singletonFactories
),通过 ObjectFactory 提前曝光,所以可以通过ObjectFactory#getObject()
方法来拿到 A 对象,C 拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中- 回到 B ,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始化。到这里整个链路就已经完成了初始化过程了
0x07 Spring IOC 的初始化过程
a. 加载配置文件,解析成 BeanDefinition 放在 Map 里。
b. 调用 getBean 的时候,从 BeanDefinition 所属的 Map 里,拿出 Class 对象进行实例化,同时,如果有依赖关系,将递归调用 getBean 方法 —— 完成依赖注入。
0x08 Spring AOP 理解
0x09 Spring 中的单例 bean 的线程安全问题了解吗?
大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。
常见的有两种解决办法:
- 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
- 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
0x10 BeanFactory 和 ApplicationContext 的区别
BeanFactory 可以理解为含有bean集合的工厂类。BeanFactory 包含了种bean的定义,以便在接收到客户端请求时将对应的bean实例化。同时具有Bean生命周期的控制、初始化方法和销毁方法。
ApplicationContext是对BeanFactory的封装,除了具有所有BeanFactory的功能外,还提供自动注册功能(BF是采用懒加载的形式、而AC是在容器启动会时一次性创建所有的Bean)、国际化的消息访问(MessageSource)、资源访问扩展了ResourceLoader接口(URL和文件、可以用来加载多个Resource)、事件传播机制
0x11 Spring 的隔离级别
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个事务未提交的数据。(脏读、不可重复读、幻读可能发生)
③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新。(不可重复读和幻读可能发生)
④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新。(幻读可能发生)
⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。(避免以上所有问题,但是效率影响很大)
Mysql 默认:可重复读
Oracle 默认:读已提交
- 本文作者: Noisy
- 本文链接: http://Metatronxl.github.io/2019/11/26/Spring-Collection/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!