文章目录
spring
spring是面向Java的轻量级开发框架,核心模块主要是依赖注入(DI)、控制反转(IOC)和面向切面编程(AOP)
理解IOC和DI
IOC控制反转是一种设计思想。我们通常指的直接控制,是程序员设计类,然后创建对象去使用,对象往往是程序员先new出来再使用,因此这里的控制权在程序员手里。而反转的意思就是将对象创建的任务交给框架去完成,控制权也交给框架。
spring框架的控制反转就是通过IOC容器去完成的,用户去设计一个实体类,在使用这个类的实例时,不需要自己去创建,只需要通过配置文件或注解声明需要使用哪个实例,实例创建的工作转移到了IOC容器。
DI依赖注入是从另一个角度描述IOC容器,因为一个实例往往具有各种的成员,程序员创建对象时往往需要通过构造方法和set方法去为实例注入依赖,而IOC容器介入后,创建对象的工作以及注入依赖的工作都交给框架去做,我们需要做的仅是通过配置文件(property)或注解(autoWired/resource)去声明即可。
这种控制反转非常利用实例的管理(尤其是大型项目),它最大的特点就是解耦——编码时松散,运行时耦合。
没有IOC的时候,程序员通过反射或new关键字创建对象,通常在构造函数中注入依赖,这样最明显的表现就是,源码中的代码耦合度就很高,而采用IOC容器管理对象创建和依赖注入的形式,编码时没有显示的依赖注入语句,我们所要做的就是修改配置文件,而程序运行时,实例创建和依赖注入则依赖框架反射完成。
IOC代替我们管理实例的生命周期,而且默认是单例的,每个实例都能够被很好的复用,程序员可以更好的精力投入到业务代码中,提升效率。
IOC原理简述
IOC本质上是一个map的集合,IOC启动的时候会将配置文件加载为resource实例,用于生成beanDefinition的组件会读取resource并解析配置项,将声明的bean组件封装为一个beanDefinition实例保存在一个map中,这个beanDefinition可以看作bean实例的元信息,其中key是beanName,value是beanDefinition实例,IOC容器为我们返回实例或依赖注入的时候,都是通过读取beanDefinition实例描述的元信息并反射创建一个Object对象。(遍历beanDefinition集合,根据beanDefinition中保存的全限定类名反射创建一个空对象,并作为Object类型返回)
一个Bean在被创建之前,会优先创建存在依赖关系的bean、存在depend-on声明的bean。如果bean声明了工厂方法则会调用工厂方法去创建bean实例。
beanFactory通常注册了一些**后置处理器(BeanPostProcessor)**或一些其他观察者组件,bean创建后会触发这些组件,依赖注入和AOP动态代理都是基于后置处理器实现的(这里用到了观察者设计模式)。(实例创建和依赖注入是独立的过程,其实都是类似的,前者反射创建对象,后者反射调用set方法)
最终创建完毕的bean实例被作为Object类型存放在IOC容器其中的一个map中。
默认bean都是单例的,只有第一次getBean的时候才会执行创建和依赖注入的流程,之后都是直接从map中获取,线程不安全,但是一般声明的Bean都是无状态的偏工具类型的bean(某个服务接口、连接池、工具类等)。也可以通过protoType声明多例Bean,这样每次获取时都会返回一个新创建的Bean实例。
单例bean的生命周期由spring IOC容器管理,因为IOC容器总是有一个强引用指向它,而多例bean的生命周期和普通对象一样,由JVM管理,IOC仅为它保存的beanDefinition即bean的元信息。
单例bean的好处:减少创建实例的动作、由spring管理Bean生命周期,减轻GC的回收压力、能够实现单例缓存池的效果。
理解AOP
面向切面编程,切面的主要目的是为了复用和管理代码。将诸如记录日志、事务等与业务关联不是特别密切的系统性、功能性代码专门抽取出来,封装成一个切面。实现业务代码和功能性代码的解耦
AOP原理简述
AspectJ是静态代理,编译期将切面织入java字节码。Spring AOP是动态代理,编译后不会修改字节码,每次运行时会临时生成代理对象,代理对象包含目标对象的全部方法,同时在指定的切入点织入切面方法。
静态代理的代理对象是在编译期就存在的,而动态代理的代理对象是运行期生成,但是二者的被代理对象都需要实现接口,而cglib也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能。
AOP基于IOC提供的扩展点实现,IOC启动时AOP模块向beanFactory注册了后置处理器,后置处理器就是一个listener,观察到事件发生后将会被回调,会对创建好的bean进行判断,如果bean所在类在切面表达式的切入范围,则会为创建代理对象,最终放入IOC容器的是代理bean。最终bean执行目标方法本质上是在执行invocationHandler实例的invoke方法或者methodInterceptor实例的intercept方法,方法执行时会获取方法上的拦截链递归执行
SpringAOP使用了策略模式,代理工厂Bean组件中同时保存了cglib代理工厂和jdk代理工厂两种策略的实现方式。
JDK动态代理只提供实现了接口的目标类的代理,代理对象将同时实现接口和继承proxy类,proxy持有invocationHandler的引用,最终代理对象将被看作接口类型,方法本质调用invocationHandler实例的invoke方法。
JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起。接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
如果被代理类没有实现接口,那么springAOP将选择CGLIB动态代理,其基于子类进行代理,因此被代理类不可以是final修饰的,方法调用本质上也是执行methodInterceptor实例的intercept方法。(代理对象继承代理类并重写所有方法,本质上是拦截器调用intercept方法)
Cglib的原理——Enhancer生成一个原有类的子类,并且设置好callBack方法(指定实现了回调方法的接口实现类)则原有类的每个方法调用都会转为调用实现了methodInterceptor接口的代理对象的intercept方法。
使用cglib就是为了弥补jdk动态代理的不足——一定要实现接口
spring中大概用到的设计模式:工厂、单例、观察者、模板方法、代理、适配器、策略等,有机会再展开说
事务传播行为
spring还有一个比较特别的功能,就是设定传播行为。
传播行为大致就是:A是一个开启了事务的调用,而现在A被另一个方法B调用,那么A的事务行为如何在B中传播。
大致分为三种类型:
【1】支持当前事务的(有B事务A就加入,否则要么新建,要么不建,要么抛异常)
(默认)required:如果B是一个事务,那么A和B共用一个事务,否则A单独开启一个事务执行。(如果发生错误,则一起回滚)
Supports:如果B是事务那么加入,否则A也作为非事务调用运行
Mandatory:如果B是事务则加入,如果调用事务方法A的方法B没有开始事务就抛异常
【2】不支持当前事务类型的(挂起、异常)
Requires_new:如果B是事务则挂起(二者事务是相互独立的、互不影响,先执行A,即使A回滚了页不影响B),否则A新建一个事务执行。
Never:不允许事务,不以事务允许,如果B是事务,则抛异常。(mandatory则是强制要求B是一个事务,否则抛异常)
not_support。B是事务就挂起,然后A以非事务运行
【3】一些其他类型
(嵌套)nested:如果B是事务则作为其子事务(嵌套事务),如果没有就新建。如果B不是事务方法同required。如果B是一个事务方法就会嵌套事务,即B回滚事务则A一定会回滚,但是A回滚不影响B的主事务和其他子事务
bean生命周期
【1】IOC容器根据beanDefinition的信息反射创建一个对象,并作为object类型存储在IOC容器中。此时这个bean的生命周期由IOC容器管理,因为始终存在一个强引用
【2】依赖注入,IOC容器通过扫描autowired注解或者解析配置,为bean对象基于构造器或set方法反射注入依赖。
【3】bean的初始化。主要是一些扩展点方法。
bean的初始化不等于实例初始化,因为实例初始化在创建对象那一刻就完成了,但是作为一个bean组件,还有很多工作没做,例如触发各种listener的回调方法。
如果实现了aware接口,那么就会执行aware接口中的方法。
如果实现了后置处理器接口就需要实现两个方法,beforeInitialization和afterInitialization。
处理器需要单独作为一个bean注册进IOC。调用InitializationBean接口的方法比调用自定义init方法效率高,因为后者需要读取配置,反射获取方法对象,然后再反射执行
<bean class="com.sh.MyPostProcessor" />
两个方法执行中间还包括beanInitialization接口的方法和自定义的init方法。afterInitialization后bean就可以使用了。
IOC容器关闭时,会调用disposableBean接口的destroy方法,然后执行自定义的销毁方法。
为什么初始化前执行aware接口,因为aware接口中的方法提供了applicationContext、beanFactory、beanName等参数,初始化方法可能会使用/获取到这些,因此aware接口中的方法需要提前执行。
aware接口的方法可以用于向IOC容器(applicationContext/beanFactory对应实现类)中注册成员
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
补充:
Application和beanFactory
二者都是接口(规定了IOC容器的实现规范),application是beanFactory的子接口,同时还是继承了其他接口如资源访问、国际化消息访问。如果使用beanFactory接口,对应容器构造对象时是延迟加载的(第一次getBean才加载),而applicationContext容器构造对象是立即加载的(读取完配置就加载)
从面向对象方面,beanFactory是面向框架开发者的,而applicationContext面向框架使用者
Spring保存的和默认返回的都是单例bean,是线程不安全的,可以通过threadLocal实现线程隔离,或者直接声明多例bean。
多例bean的获取基于原型设计模式,spring在容器中保存一个原型,而每当用户向IOC申请bean时都会返回一个新的对象。
如果申请单例bean则先查询缓存,没有则创建。如果是多例bean直接根据beanDefinition反射创建对象并作为Object类型返回。
循环依赖问题
如果有两个Bean分别是A和B,其中A和B的依赖注入关系为A{B b}和B{A a},则存在循环依赖问题。
多例bean对于循环依赖的策略是抛出异常(否则递归造成栈溢出),默认单例属性set的注入场景支持循环依赖。
“循环依赖”问题的本质是“two sum”
简要描述:
IOC容器维护了一个“未完成Map”,当spring为一个Bean注入依赖时,会首先查询“未完成Map”是否存在所需要注入的类型,如果查到说明存在循环引用,这便是递归返回的核心,直接找到这个循环依赖bean对应的工厂Bean,调用getObject注入一个半成品Bean并返回。
否则将当前正在创建的Bean的引用放入Map中,然后进入递归,为“循环引用成员”去注入依赖,而当进入递归后能够通过“未完成Map”拿到所需类型的Bean,则完成注入工作并退出递归,退出递归后循环依赖的注入也完成了。
spring维护了一个临时的map缓存,存放未完成依赖注入的bean引用,和一个工厂bean的map缓存。当spring为某个bean的依赖(创建并)注入bean时,如果发现未完成bean缓存能够找到对应引用说明存在循环引用,则直接从工厂bean缓存拿到循环依赖bean的工厂bean,通过getObject注入依赖(此时还是半成品,全部返回后就是依赖关系完整的bean)后返回。当bean创建结束后,两个互相依赖的bean的字段中都保存一个完成的bean对象的引用。
注解的实现
spring开发中最常见的就是注解,注解可以看作框架提供给用户的一种特殊接口,用户使用注解对类型、方法、字段等进行标注。框架扫描到注解后,将会通过反射执行某种行为,例如读取参数创建对象。
模拟框架识别注解
我自定义3个注解@value、@autowired、@Qualifier和@component
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Value{
String value() default "";
}
其中@target和@retention元注解是用于修饰注解的注解,他们由JVM去识别,而我们的注解使用自己的实现去识别。@target指定了我们的@value注解能够修饰到哪里,@Retention指定主键的存活时间,因为我们需要在运行时反射拿到其中的内容,因此这里必须是runtime
@Target(ElementType<