Spring

Spring 简介

  • 简化开发,解耦,集成其它框架。
  • 低侵入式设计,代码污染级别较低。
  • Spring的DI机制降低了业务对象替换的复杂性,提高了软件之间的解耦。
  • Spring AOP支持将一些通用的任务进行集中式的管理,例如:安全,事务,日志等,从而使代码能更好的复用。

Spring bean

Spring 官方文档对 bean 的解释是:

在 Spring 中,构成应用程序主干并由Spring IoC容器管理的对象称为bean。bean是一个由Spring IoC容器实例化、组装和管理的对象。

可以这么理解,在spring中,一切东西都是bean

bean 的生命周期

  1. 读取class文件
  2. 实例化原始对象
  3. 属性填充(也是DI,依赖注入)
  4. 构造后初始化方法
    • 实现了 InitializingBean 接口会执行afterPropertiesSet方法
    • 或者执行有注解 @PostConstruct 的方法
  5. AOP 生成代理对象(有AOP切点的话)
  6. 放入bean池子

单例bean

spring中的单例bean,不等同于java中的单例模式。

spring单例bean,允许同一个类有多个名称不相同的实例。

单例bean查找过程

graph TB
    A((开始查找)) --> B("byType,根据type从spring容器里找到多个bean") 
    B -- 通过一系列筛选 --> C(是不是isAutowireCandidate)
    C -- true --> D(是不是符合Qualifier)
    C -- false --> A1([忽略掉false的bean])
    D -- 符合 --> AA((找到bean))
    D -- 不符合 --> E(是否有Primary标注的bean)
    E -- 有 --> AA
    E -- 没有 --> F("查找优先级,Priority值大的优先级高")
    F -- 找到优先级高的 --> AA
    F -- 多个优先级相同 --> G("byName,根据属性名字选出一个")
    G -- "找到对应名字(名字完全相同)" --> AA
    G -- 没有找到 --> A2([报错])

属性填充

Autowired 注解

  1. 进入 AutowiredAnnotationBeanPostProcessor
  2. 寻找被 @Autowired 注解了的方法和属性
  3. 方法注入点注入,属性注入点注入
  4. 查找对应的bean(方法根据参数类型和名字),参考上面的bean查找过程

Resource 注解

  1. 进入 CommonAnnotationBeanPostProcessor
  2. 寻找被 @Resource 注解了的方法和属性
  3. 方法注入点注入,属性注入点注入
  4. 如果 @Resource 指定了name属性,直接从spring容器中拿对应的bean,不存在则报错
  5. 如果 @Resource 没有指定name属性,但根据属性名字或setXXX()中的XXX(根据的不是参数),在spring容器中找到对应的bean,参考上面的bean的name查找过程

Spring IoC & DI

IoC(Inversion of Control)的意思是“控制反转”,它是Spring最核心的点,并且贯穿始终。

IoC并不是一门技术,而是一种设计思想。

在Spring框架中实现控制反转的是Spring IoC容器,其具体就是由容器来控制对象的生命周期和业务对象之间的依赖关系,而不是像传统方式(new 对象)中由代码来直接控制。程序中所有的对象都会在Spring IoC容器中登记,告诉容器你是个什么,你需要什么,然后IoC容器会在系统运行到适当的时候,把你要的对象主动给你,同时也把你交给其它需要你的对象。

也就是说控制对象生存周期的不再是引用它的对象,而是由Spring IoC容器来控制所有对象的创建、销毁。对于某个具体的对象而言,以前是它控制其它对象,现在是所有对象都被Spring IoC容器所控制,所以这叫控制反转。

控制反转最直观的表达就是,IoC容器让对象的创建不用去new了,而是由Spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法。控制反转的本质是控制权由应用代码转到了外部容器(IoC容器),控制权的转移即是所谓的反转

控制权的转移带来的好处就是降低了业务对象之间的依赖程度,即实现了解耦。即然控制反转中提到了反转,那么肯定有正转,正转和反转有什么区别呢?

正转:如果我们要使用某个对象,就需要自己负责对象的创建。
反转:如果要使用某个对象,只需要从Spring 容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架。

DI(Dependency Injection)的意思是”依赖注入”,它是IoC(控制反转)的一个别名。

IoC 和 DI 其实是同一个概念,只是从不同的角度描述 (IoC是一种思想,而DI则是一种具体的技术实现手段) 。

IoC 是目的 (它的目的是创建对象) ,DI是手段 (通过什么手段获取外部对象) 。

具体的DI流程在上面属性填充已经说过,那么如果存在循环依赖呢?

循环依赖

循环依赖是:A引用B,B引用C,C引用A,实例化A,B,C时存在循环引用。

在Spring中循环依赖发生在创建Bean的过程中,具体发生在doCreateBean方法中调用createBeanInstance和populateBean方法时。

在Spring中解决循环引用的方式是使用三级缓存策略:

名称 描述
singletonObjects 一级缓存,存放完整的Bean
earlySingletonObjects 二级缓存,存放提前暴露的Bean,且该Bean是不完整的
singletonFactories 三级缓存,存放的是Bean工厂,主要生产Bean的
  1. 一级缓存: 用于存储已创建的单例 Bean 实例,提高访问效率。
  2. 二级缓存: 是为了分离成熟bean和未注入属性bean的存放,防止多线程在bean还未创建完成时读取到,所以也是为了保证我们getbean是完整的bean。还有一个作用是为了存储三级缓存创建出来的早期bean,防止三级缓存重复执行。
  3. 三级缓存: 存储的是一个函数接口,函数接口实现创建动态代理调用beanPostProcessor,为了避免重复创建,调用把返回的动态对象或者原实例放到二级缓存中。

三级缓存的源码结构:

1
2
3
4
5
6
7
8
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

创建流程:

spring-三级缓存

举例(A,B相互依赖):

  1. 先创建A:第一次创建A;A在1.1中的缓存都找不到,所有缓存都是空。
  2. 实例化A:实例化A完成;1.3中把A放入了三级缓存。
  3. 初始化A:属性赋值,发现A依赖了B;沿图中红线,查找创建B。
  4. 创建B:第一次创建B;B在1.1中的缓存找不到,所有缓存B都是空。
  5. 实例化B:实例化B完成;1.3中把B放入三级缓存。
  6. 初始化B:属性赋值,发现B依赖了A;沿着图中红线,查找A。
  7. A在三级缓存中已经存在;1.1.3.1 把A放入到二级缓存,并把A从三级缓存中删除,返回A。
  8. 初始化B中获取到A之后,把A注入到B的依赖属性中。
  9. 初始化B完成:1.2.2,1.2.3 把B放入到一级缓存,并把B从三级缓存中删除,返回B。
  10. 初始化A中获取到B,把B注入到A的依赖属性中。
  11. 初始化A完成:1.2.2,1.2.3 把B放入到一级缓存,并把A从三级缓存中删除,返回A。

注意:
有循环依赖的时候AOP阶段会提前

Spring AOP

Spring的AOP实现原理其实很简单,就是通过动态代理实现的。

如果为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。

Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理,CGLib基于ASM实现。这些可以统一归于java字节码增强技术,详细参考:[java-jvm]

  1. Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。然后JDK动态代理需要使用的第二个组件就是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。
  2. JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。CGLib直接操作字节码,生成类的子类,重写类的方法完成代理。

Spring 事务

Spring事务就是对数据库事务的一层封装。

工作原理:

  1. 拿到对应代理对象
  2. 执行代理对象的对应方法(在原有的方法上进行字节码增强)
    1. 建立数据库连接conn
    2. conn.setAutocommit(false)
    3. 执行原生对象的该方法
    4. commit

事务传播

Spring 对事务的控制,是使用 aop 切面实现的,我们不用关心事务的开始,提交 ,回滚,只需要在方法上加 @Transactional 注解,这时候就有问题了:

场景一:serviceA 方法调用了 serviceB 方法,但两个方法都有事务,这个时候如果 serviceB 方法异常,是让 serviceB 方法提交,还是两个一起回滚。
场景二:serviceA 方法调用了 serviceB 方法,但是只有 serviceA 方法加了事务,是否把 serviceB 也加入 serviceA 的事务,如果 serviceB 异常,是否回滚 serviceA 。
场景三:serviceA 方法调用了 serviceB 方法,两者都有事务,serviceB 已经正常执行完,但 serviceA 异常,是否需要回滚 serviceB 的数据。

spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

同一个Service类中的方法相互调用需要使用注入的对象来调用,不能直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用。原因:spring 代理对象中包含原生对象的变量,在执行原生对象的该方法时用this.方法名,这时候this指向的是原生对象

  • propagation_requierd: 如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择,也是默认模式,它适合于绝大多数情况。
  • propagation_supports: 支持当前事务,如果没有当前事务,就以非事务方法执行。
  • propagation_mandatory: 使用当前事务,如果没有当前事务,就抛出异常。
  • propagation_required_new: 新建事务,如果当前存在事务,把当前事务挂起。
  • propagation_not_supported: 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • propagation_never: 以非事务方式执行操作,如果当前事务存在则抛出异常。
  • propagation_nested: 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

参考资料

  1. 浅析Spring中AOP的实现原理——动态代理
  2. Spring详解(三)—-认识IoC控制反转/DI依赖注入
  3. Spring循环依赖及三级缓存源码解析