Spring源码深入

IOC,DI的理解和底层实现

IOC,控制反转是由使用者来进行控制,有了spring之后,可以把整个对象交给spring来帮我们进行管理容器

Di:依赖注入,把对应的属性值注入到具体的对象中(@autowired)

容器:使用map结构存储对象,在spring中一般存在三级缓存,singletonObject存放完整bean对象,整个bean的生命周期从撞见使用到销毁过程全部都是由容器来管理

生命周期

加载字节码文件->无参构造->对象->依赖注入->@PostConstruct->初始化(afterPropertiesSet)->初始化后(AOP)->放入map(单例池)->bean对象

加载字节码文件

spring会根据定义的文件生成目录去获取文件的字节码,然后根据对象上的注解创建对象

  • 如果代理对象有接口,就用 JDK 动态代理,否则就是 Cglib 动态代理。
  • 如果代理对象没有接口,那么就直接是 Cglib 动态代理。

创建对象

如果一个类中有多个构造方法,那么spring会优先调用无参构造,如果有多个有参数的构造方法会直接报错

推断构造方法

在构造方法中加上@AutoWired能解决这个问题

调用有参构造时,如果要求传入的对象以及在容器中了,那么就会直接传入该对象,如果没有这个对象就会去创建这个对象

  • 传入的情况是先根据type寻找,再根据name寻找(name根据入参名来),如果只有一个对应的类型存在,就不会根据入参的名字进行筛选了

依赖注入

找到加了Autowired注解的属性,然后也是根据先寻找对象,再寻找参数name的规则进行依赖注入

初始化后AOP

  • cglib会产生一个原对象的代理类,作为需要代理进行代理类的父类,
  • 在代理类去增加一个被代理的对象叫做target, 将原来的对象赋值给target
  • 重写切入点的方法,然后调用target().切入点方法()

所以用断点去获取代理对象中的依赖注入属性是无法获取到的

JoinPoint中有俩个api 一个是getTarget拿到的是被代理类的普通对象,getThis拿到的就是代理对象

spring事务

  1. 会判断代理对象的方法中是否有@Transactional注解,如果有@Transactional注解,就开启事务(也是动态代理,进行事务开启)
  2. spirng事务管理器新建一个数据库连接,如果直接让JDBC,Mybatis这种建立数据库连接conn.autocomiit=true,会直接提交sql
  3. conn.autocomiit=false 关闭数据库自动提交,spring创建了一个threadLocal<Map<Datasource,conn>>,因为一个线程可能会执行很多方法,每个方法所要求的datasource连接可能不一致,所以用的DataSource不一样,可以根据key获取conn
  4. JDBCTemplate或者Mybatis拿到spring的数据库连接,然后执行sql
  5. 如果没有异常调用conn.commit()提交,有异常调用conn.rollback()回滚

事务为什么不生效

综上,一个方法上注解的@Transactional注解要考虑什么时候会生效,被调用时是哪个对象进行调用

比如图中的调用方式是没用的(PROPAGATION_NEVER:以非事物方式运行,如果当前存在事务,则抛出异常),因为真正执行a()是代理对象中的target属性的.test()方法,并不会被拦截到

所以在使用的时候,一定要事务嵌套时,执行的事务方法,都是从代理对象进入的(即能够被spring所拦截到)

为什么要给数据库配置类加上@Configuration

因为如果不给数据库加这个注解,例如JDBCTemplate无法正确根据Datasource拿到Conn,JDBCTemplate和事务管理器就是俩个不同的对象.(第4步无法执行,事务不生效)JDBCtemplate就是自己新建的数据库连接

@configuration,AOP,@Lazy都是基于动态代理的

只有加上了该注解才会去spring里面找连接对象

循环依赖

三级缓存

一级缓存:singletonObjects 经过完整生命周期的bean对象

二级缓存:earlySingletonObjects 提前产生的代理对象,半成品对象

三级缓存:singletonFactories key是对象名value是lambda表达式(函数式接口),调用会返回对象,put时并不会执行

还有creatingSet<"Object">用来判断是否出现过已经创建的对象

在需要注入对象的时候,依次从一级到三级寻找,如果三级缓存找到了的话,获取三级缓存中的lambda表达式,删除获取到的内容,然后放到二级缓存中(操作加锁)

比如现在A,B循环依赖
https://juejin.cn/post/7078283031781310478

  1. creatingSet<"A">
  2. 实例化A对象->singletonFactories.put("A",()->getEarlybeanReference(beanName,mbd,Aservice对象))
  3. 准备填充B,放入二级缓存,去创建B对象
  4. B对象去一级缓存找(单例池),没找到,发现在CreatingSet中出现了(出现了就表示循环依赖了)
  5. 去二级缓存寻找,也没发现->去三级缓存中寻找,发现了对象->执行lambda表达式
  6. 获取到了A的代理对象,(对A进行代理的时候,会执行一个方法往ealyProxyReferences这个map中put对象)将A的代理对象放入二级缓存中,删除三级缓存中原来的A对象
  7. 返回B对象,完成A对象的创建
  8. 会进行判断A对象是否在ealyProxyReferences出现过,然后删除他,如果出现过的话就不去做AOP了
  9. 最后把A添加到单例池(一级缓存),从creatingset中删除

简而言之
1、A创建过程中需要B,于是先将A放到三级缓存,去实例化B。

2、B实例化的过程中发现需要A,于是B先查一级缓存寻找A,如果没有,再查二级缓存,如果还没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。

3、B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中的状态)。然后回来接着创建A,此时B已经创建结束,可以直接从一级缓存里面拿到B,去完成A的创建,并将A放到一级缓存。

@Lazy

如果加了@lazy注解,那么在注入的时候,会注入当前属性的代理对象,只是创建代理对象不会有循环依赖问题,等到真正调用的时候,才会去生成对应的被代理对象

如果是构造方法出现循环依赖,那么spring默认无法解决,但是可以通过@lazy解决,在真正调用对象方法的时候才会去对应真正容器里找对应的bean

spring扫描原理

扫描过程

在configurationClassParser类中,会进行扫描,得到的BeanDefinition会注册到spring容器中,并会检查是不是配置类并进行解析

循环遍历所有componentScans,(一个类可以有多个componentScans注解,和往里头传数组一样)

扫描和解析获取beanDefinitionHolder对象, 然后检查扫描出来的BeanDefinition对象是不是一个配置类

比如只要加了Component,ComponentScan,Import,ImportResource都是配置类,或者类中的方法由@bean注解也是配置类

命名

会先找@componentScan上有没有nameGennerator注解,这个注解中有一个类参数,这类需要实现,BeanNameGenerator接口

实现这个接口后就可以对类的名称进行自定义,

默认情况下BeanNameGenerator,是根据@component中指定的名字来的,没有设置的话,就是类名转大驼峰命名

所以说,类名一样的俩个配置类可能会导致spring启动失败

@scoped

spring默认有五种scope,加了@scoped注解就和在xml文件中配置<bean scope="">是一样的效果,默认有五种

  • singleton:默认的,单例模式,获取的每个对象
  • prototype:每一次都创建一个新的对象
  • request 应用在web项目中,spring创建这个类之后,将类存入到request中
  • session应用在web项目中,spring创建这个类之后,将类存入到session中
  • globalsession应用在web项目中,必须在porlet(基于java的web组件,子域名)环境下使用.如果没有这种环境,就相对于session

比如A是启动时创建,B是请求时创建,如果在B中注入了A,那么B中会先存放A的代理对象,等实际收到请求在把对象创建出来

@scoped也可以配置proxyMode参数,决定到底用哪种方式创建对象,Interfaces就是JDK,TARGET_CLASS就是CGLIB,如果没有配置的话,会使用@ComponentScan上的的scopedProxy参数

注意:只有在A无法找到B的情况下(A是singleton,B是Request),才会用上述方式生成代理代理对象,和spring默认使用的动态代理生成方式没有关系

创建流程

通过ASM拿到字节码后

isCandidateComponent中会判断当前类是不是和excludeFilters的match方法是否匹配,如果匹配上一个就说明不是需要放入容器的bean,返回false

然后判断能不能匹配includefilters这个list,匹配上一个就返回true

匹配的机制有比如类上有Compoent注解

this.includeFilters.add(new AnnotationTypeFilter(Component.class))

就能匹配成功,简单理解为excludeFilters中的就是排除项,includeFilters中的就是包含项

这个判断之后会生成一个beanDefinition类进入isCandidateComponent进行判断是不是独立类,抽象类,接口

独立类:类不在其他的类中,spring不会把内部类当做bean去处理的(静态内部类是独立类,因为静态内部类不需要通过创建父类创建)

非独立类,抽象类,接口都不能被当做bean(mybatis是通过动态代理去生成代理对象),抽象类中有Lookup注解才能被注入

LockUp

Lockup注解不是只能加在抽象类中,可以加在普通的方法中

抽象类如果加上之后,返回的代理对象,执行void方法时就会报错,执行有返回值对象,也无法正常运行

需要在里面传入需要注入的bean,然后返回对应的bean,没档次调用方法发挥的对象都是新创建的对象

放入对象

如果有俩个一样名字的对象的话,spring会通过isCompatible()判断是否兼容,如果兼容(isCompatible(返回true)返回false表示不会重新注册到spring容器中,如果冲突就直接抛异常

isCompatible():判断俩个类是否是同一个,如果是同一个返回true(重复扫描到了一个类,不需要抛异常),如果类不一样,返回false

Last modification:January 31, 2023
如果觉得我的文章对你有用,请随意赞赏