文章目录
学习到SpringBoot,有必要更进一步理解和学习Spring更深层的东西。这就学习和整理了这一篇文章。
参看:
- 《深入理解JVM虚拟机》第三版
- https://spring.io
- https://mp.weixin.qq.com/s/WpRSitDqtgOuU9GnI1-HDw
- https://blog.csdn.net/xiaofeng10330111/article/details/105631666
- https://mp.weixin.qq.com/s/NXZp8a3n-ssnC6Y1Hy9lzw
- https://cloud.tencent.com/developer/article/1584491
中文版官方文档分享
https://www.jianshu.com/p/b3da0c8a22fe
一、Spring IOC
DI和IOC
Bean是包装的Object,无论是控制反转还是依赖注入,它们的主语都是object。Bean是Spring的主角。
Dependency Injection,依赖注入:容器动态的将某个依赖关系注入到组件之中。通过简单的配置,无需任何代码就可指定目标需要的资源,无需关心资源来自何处。**且被注入对象依赖IOC容器配置依赖对象。**可以说DI是IOC设计思想的实现。
Inversion of Control,控制反转。控制反转就是把创建和管理 bean 的过程转移给Spring IoC Container,对于 IoC 来说,最重要的就是容器。容器管理着 bean 的生命,控制着 bean 的依赖注入。
有了ioc容器,我们每次需要使用对象的时候,直接从容器中取出、使用即可,不用去关心如何创建、销毁等。
DI和IOC的关系
同一个概念的不同描述,IOC是一种设计思想,DI是一种实现。DI是IOC思想的实现,即容器管理Bean使用依赖注入技术实现。
当IOC容器实现了Bean的资源定位、载入和解析注册后,会对所管理的Bean进行依赖注入,发生依赖注入的情况:
- 第一次调用BeanFactory的getBean()方法时,ioc容器触发依赖注入。
- 关闭lazy-init懒加载时,容器在解析注册bean的时候会继续预实例化,触发依赖注入。
IOC容器初始化
分为四步:资源定位、载入、解析注册和依赖注入。
IOC容器初始化的两个基本步骤:
- 初始化的入口由 容器中的refresh()方法 调用完成。
- 对Bean定义载入IOC容器使用的方法是 loadBeanDefinition()。
大致过程:
- 通过ReasourceLoader来完成资源文件的定位,DefaultResourceLoader是默认的实现,同时上下文本身就给出了ResourceLoader的实现,可以通过类路径、文件系统、URL等方式来定位资源。
- 如果XmlBeanFactory作为IOC容器,那么需要为它指定Bean定义的资源,也就是说Bean定义文件是通过抽象成Resource来被IOC容器处理,容器通过BeanDefinitionReader来完成定义信息的解析和Bean信息的注册,往往使用XmlBeanDefinitionReader来解析Bean的XML定义文件—实际的处理过程是委托给BeanDefinitionParserDelegate来完成的,从而得到Bean的定义信息,这些信息在Spring中使用BeanDefinition来表示(这个名字可以让我们想到loadBeanDefinition()、registerBeanDefinition()这些相关的方法,他们都是为处理BeanDefinition服务的)。
- 解析得到BeanDefinition以后,需要在IOC容器中注册,这由IOC实现BeanDefinitionRegister接口来实现,注册过程就是在IOC容器内容维护一个HashMap来保存得到的BeanDefinition的过程,这个HashMap是IOC容器持有Bean信息的场所,以后Bean的操作都是围绕这个HashMap来实现。
- 之后我们通过BeanFactory和ApplicationContext来享受Spring IOC的服务了,在使用IOC容器的时候我们注意到,除了少量粘合代码,绝大多数以正确IOC风格编写的应用程序代码完全不关心如何到达工厂,因为容器将把这些对象与容器管理的其他对象钩在了一起,基本的策略是把工厂放到已知的地方,最好放在对预期使用的上下文有意义的地方,以及代码要实际访问工厂的地方。
- Spring本身提供了对声明式载入Web应用程序用法的应用程序上下文,并将其存储在ServletContext的框架实现中。
二、Spring AOP
aspect oriented programming,面向切面编程。在不修改源代码的前提下,通过 预编译+运行时动态代理 实现在不同的业务组件添加某些通用功能。
它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)。
1.名词解释
JoinPoint(连接点)
程序在执行过程中,一个个的时间点,比如调用时、抛出异常时等,这些时间点可以注入切面代码(可选)。
Pointcut(切点)
JoinPoint表示切面代码可以被织入的地方,根据需求切入到某些点,就需要用到 Pointcut
定义规则匹配JoinPoint(Aspect Pointcut expression language 描述规则)。
Advice(增强)
在Pointcut匹配到JoinPoint后,就得通过 Advice
确定具体时机与逻辑,是切面代码真正被执行的地方,主要织入时机有:
- Before Advice:前置增强,在目标方法执行前织入。
- After Advice:后置增强,在目标方法执行后织入(不管是否有异常都会织入)。
- After returning advice:返回后织入,在目标方法执行正常后织入(抛出异常则不会被织入)。
- After throwing advice: 异常织入,目标方法执行过程中抛出异常后织入。
- Around Advice:环绕织入,在目标方法执行前后都可织入切面代码,也可以选择是否执行原有正常的逻辑,如果不执行原有流程,它甚至可以用自己的返回值代替原有的返回值,甚至抛出异常。
在Around Advice中,环绕方法需要传入ProceedingJoinPoint对象,此对象调用proceed()就表示系统要执行的方法,在它上下添加自己想添加的代码逻辑即可实现环绕增强。
Aspect切面就是Pointcut+Advice实现的,它可以指定了哪些JoinPonit上织入哪些代码。
切点函数
execution()表达式
匹配指定的类、方法。
表达式切入点,配合 Pointcut 使用,它有一下三个符号:
-
*
:0-n个字符。 -
..
:用在方法参数中,表示任意个参数。用在包命后,表示当前及其子包路径。 -
+
: 用在类名后,表示当前及其子类。用在接口后,表示当前接口及其实现类。
示例(定义在 service 包里的任意类的任意方法):execution(* com.kkb.service.*.*(..))
@Pointcut(value = "execution(* com.pdh.service..*.*(..))")
private void test(){}
@annotation()
匹配被注解标注的方法。示例:@annotation(com.pdh.aop.LogAnnotation)
。
@Pointcut(value = "@annotation(com.pdh.aop.LogAnnotation)")
private void test(){}
@target()
匹配所有标注了指定注解的类。如 @target(org.springframework.stereotype.Service) 表示所有标注了@Service的类的所有方法。
当然,还有其他的切点函数,可以去查看官方使用手册。
2.快速使用
使用全注解配置使用Spring AOP功能。下列是在SpringBoot项目中使用Spring aop功能,SpringBoot自动整合了Spring中的依赖。
2.1 Spring AOP注解
列出一些常用注解:
- @Aspect:切面。此注解用在切面类上,表示该类作为切面供容器读取。
- @Pointcut:切点。此注解作用在方法上,表示该方法为一个切点,并且需要传入切点函数表达式,匹配指定的增强目标。
还有就是Advice增强的注解:@Before、@After、@AfterReturning、@AfterThrowing、@Around。
2.2 使用示例
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GlobalErrorCatch {
// 参数
}
自定义Aspect
Pointcut+Advice(around)
@Aspect //使用@Aspect表示它是一个切面
@Component
public class TestAspect {
// 1. 定义所有带有 GlobalErrorCatch 的注解的方法为 Pointcut
@Pointcut("@annotation(com.example.demo.annotation.GlobalErrorCatch)")
private void globalCatch(){}
// 2. 将 around advice 作用于 globalCatch(){} 此 PointCut
@Around("globalCatch()")
public Object handlerGlobalError(ProceedingJoinPoint point) throws Throwable {
try {
long beginTime = System.currentTimeMillis();
// 执行方法
return point.proceed();
long time = System.currentTimeMillis() - beginTime;
System.out.println("执行时长" + (time - beginTime) + "ms");
} catch (Exception e) {
System.out.println("执行错误" + e);
return ServiceResultTO.buildFailed("系统错误");
}
}
}
自定义Service
public class TestService {
@Override
@GlobalErrorCatch
public String test() {
// 此处写服务里的执行逻辑
retrun "我执行";
}
}
通过这样的方式,所有标记着 GlobalErrorCatch 注解的方法都会统一在 handlerGlobalError 方法里执行,而不用在每一个业务代码处使用try-catch处理异常。
在体会到SpringAop的效果之后,我还想了解它的原理,你给我写!
3.Proxy代理
在学习和理解AOP的运行原理之前,先了解一下代理。
3.1 代理简介
先看一个场景,我要买一个手机,我不会直接和手机厂商对接,而是会和手机店打交道(也可以是其他渠道),手机店就是代理,手机厂商就是目标对象,我就是调用者,代理不仅实现了目标对象的行为,还可以添加自己的动作。
UML图:
Client直接和Proxy打交道。Proxy和RealSubject都实现了Subject接口,RealSubject类就是目标类,这里假定它有request()方法,但是通过Proxy后就加上了额外的方法(preRequest()、afterRequest()、… …)。
3.2 代理分类
代理分为静态代理和动态代理,而动态代理又分为 JDK动态代理和CGLIB动态代理。
我们都知道Java程序的运行机制:Java源代码 -> 字节码 -> JVM类加载 -> 内存区域执行代码。【点击我查看类加载机制】。由Java程序执行原理来看,字节码是关键,静态和动态的区别就在于字节码生成的时机。
静态代理
代理类由开发人员编写或工具自动生成源代码,在编译时将代理类(委托类)就会确定下来,在程序运行前代理类的.class
文件就已经存在。字节码文件会被修改(相当于未进行静态代理的情况)。
这里可以根据上面的 UML 图,可以手写实现静态代理。
静态代理主要两大劣势:
- 代理类只能代理一个委托类(单一职责原则),由于静态代理必须在编译前确定代理类,当需要代理多个委托类时,就要编写多个代理类。
- 当需要代理类实现某种功能,比如执行时间计算的时候,在相应的方法开始前、开始后分别织入计算时间的代码,那么每个方法都会重复这一计算时间的代码。
这些缺点能不能解决?能,基于动态代理动态生成代理类,就不用重复编写代码。
动态代理
Spring AOP支持 JDK动态代理 和 CGLIB动态代理
在程序运行之前是没有代理对象产生的,在程序运行后通过反射创建生成字节码再由 JVM 加载而成。动态代理避免了静态代理那样的硬编码。动态代理分为 JDK动态代理 和 CGLIB动态代理,Spring AOP使用的是CGLIB动态代理。
JDK动态代理
原理:JDK动态代理只提供接口的代理,不支持类的代理,要求委托类(目标对象)实现任意接口(目标对象中需要代理的方法必须是接口有的),代理方式调用时,还必须传入委托类实现的接口对象。
JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类,当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起。
这样可避免静态代理代码重复,代码可维护性极大提高。
但由于使用JDK动态代理必须要传递委托类实现的接口对象,这点并不算好,就有了继承自委托类的 CGLIB动态代理。
CGLIB动态代理
JDK 动态代理使用 Proxy 来创建代理类,增强逻辑写在 InvocationHandler.invoke()
里,CGlib 动态代理也提供了类似的 Enhance 类,增强逻辑写在 MethodInterceptor.intercept()
中,委托类的非final方法都会被方法拦截器拦截。
简单来说,CGLIB动态代理是由拦截器拦截住委托类的要执行的方法,继承委托类,重写委托类的非final方法,在重写方法中实现代码增强。
JDK动态代理通过反射执行委托类的方法(反射效率较低),而 CGlib 采用了FastClass 的机制来实现对被拦截方法的调用【FastClass 机制是对一个类的方法建立索引,通过索引来直接调用相应的方法】
综合来看CGLIB动态代理具有:动态生成代理对象、不用继承接口、FastClass执行效率高于反射等优点。
4.AOP运行流程
在学习了解到上面提到的名词解释、使用示例、Proxy代理之后,这里可以梳理一下AOP的运行步骤。
- 解析xml配置 或 获取被
@Aspect
注解修饰的类信息。 - 实例化所有的bean。
- 解析aop的config信息:
- 解析aop aspect得到aspect引用的对象(逻辑提供类);
- 解析aop aspect里面的每一个切面。
- 得到该aspect对应的pointcut切点,再得到pointcut对应的pointcut的切点表达式。
- 使用切点表达式中用于匹配类型部分,去spring ioc容器中与所有的bean的类型进行匹配,把匹配到的对象作为spring动态代理的目标对象。
- 如果目标对象实现了接口,使用JDK的动态代理包装;如果目标对象没有实现接口,使用cglib包装。
- 得到配置的 拦截时机 + 逻辑提供类的对应方法(从method解析) + Pointcut表达式中方法的匹配模式 创建一个拦截器。
- 再把该拦截器使用对应的动态代理机制转换为代理对象,替换spring容器中的对应bean的实例,执行对应的方法。
这就是Spring AOP的运行流程,其中很多的点,Spring官方都采用了非常精妙的设计,推荐去阅读一下SpringAop的源码,收获颇多。
JDK动态代理图:
CGLIB动态代理图: