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事务
- 会判断代理对象的方法中是否有@Transactional注解,如果有@Transactional注解,就开启事务(也是动态代理,进行事务开启)
- spirng事务管理器新建一个数据库连接,如果直接让JDBC,Mybatis这种建立数据库连接conn.autocomiit=true,会直接提交sql
- conn.autocomiit=false 关闭数据库自动提交,spring创建了一个threadLocal<Map<Datasource,conn>>,因为一个线程可能会执行很多方法,每个方法所要求的datasource连接可能不一致,所以用的DataSource不一样,可以根据key获取conn
- JDBCTemplate或者Mybatis拿到spring的数据库连接,然后执行sql
- 如果没有异常调用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
- creatingSet<"A">
- 实例化A对象->singletonFactories.put("A",()->getEarlybeanReference(beanName,mbd,Aservice对象))
- 准备填充B,放入二级缓存,去创建B对象
- B对象去一级缓存找(单例池),没找到,发现在CreatingSet中出现了(出现了就表示循环依赖了)
- 去二级缓存寻找,也没发现->去三级缓存中寻找,发现了对象->执行lambda表达式
- 获取到了A的代理对象,(对A进行代理的时候,会执行一个方法往ealyProxyReferences这个map中put对象)将A的代理对象放入二级缓存中,删除三级缓存中原来的A对象
- 返回B对象,完成A对象的创建
- 会进行判断A对象是否在ealyProxyReferences出现过,然后删除他,如果出现过的话就不去做AOP了
- 最后把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