整合总结,备忘…
What’s AOP
Aspect Oriented Programming,面向切面编程。以 OOP 来对比理解:
纵向关系 OOP,横向角度 AOP
以 日志记录 为例,在没有AOP之前,如果需要在多个方法中进行日志记录,需要在每个方法中都重复编写同一段日志操作代码,哪怕日志操作记录的方法被封装到工具类(LogUtils),仅需要一行调用即可,这样的操作还是对业务代码有侵入性,而类似日志统计、性能分析等这一类就被称为侵入性业务,使原本的业务逻辑代码跟日志操作这类代码有耦合,并且往往这类代码横跨并嵌入众多模块里边,在各个模块里分散得很厉害,到处都能见到,造成代码维护困难。
> > 从对象组织角度来讲,我们一般采用的分类方法都是使用类似生物学分类的方法,以「继承」关系为主线,我们称之为纵向,也就是OOP。设计时只使用 OOP思想可能会带来两个问题: > * 对象设计的时候一般都是纵向思维,如果这个时候考虑这些不同类对象的共性,不仅会增加设计的难度和复杂性,还会造成类的接口过多而难以维护(共性越多,意味着接口契约越多)。 > * 需要对现有的对象 动态增加 某种行为或责任时非常困难。 > 而AOP就可以很好地解决以上的问题,怎么做到的?除了这种纵向分类之外,我们从横向的角度去观察这些对象,无需再去到处调用 LogUtils 了,声明哪些地方需要打印日志,这个地方就是一个切面,AOP 会在适当的时机为你把打印语句插进切面。AOP用处
参数校验和判空
系统之间在进行接口调用时,往往是有入参传递的,入参是接口业务逻辑实现的先决条件,有时入参的缺失或错误会导致业务逻辑的异常,大量的异常捕获无疑增加了接口实现的复杂度,也让代码显得雍肿冗长,因此提前对入参进行验证是有必要的,可以提前处理入参数据的异常,并封装好异常转化成结果对象返回给调用方,也让业务逻辑解耦变得独立。
权限控制
避免到处都是申请权限和处理权限的代码。
无痕埋点
围绕方法调用前后进行接口调度次数统计的埋掉操作。
安全控制
比如全局的登录状态流程控制。
日志记录
常用于方法进入前、执行结果后的日志记录。
性能统计
检测方法耗时其实已经有一些现成的工具。痛点是这些工具使用起来都比较麻烦,效率低下,而且无法针对某一个块代码或者某个指定的sdk进行查看方法耗时。可以采用 AOP 思想对每个方法做一个切点,在执行之后打印方法耗时。
事务处理
声明方法,为特定方法加上事务,指定情况下(比如抛出异常)回滚事务。
异常处理
替代防御性的 try-Catch。
缓存
缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。留意一些常用的缓存框架的使用方式,即可发现AOP的应用,譬如guava cache/caffeine,可以在真实的DAO方法前冠上@Cacheable便可以指定方法返回值来被缓存。
设计模式
代理模式
代理模式又分静态代理与动态代理。
静态代理
代理模式上,基本上有Subject角色,RealSubject角色,Proxy角色。其中:Subject角色负责定义RealSubject和Proxy角色应该实现的接口;RealSubject角色用来真正完成业务服务功能;Proxy角色负责将自身的Request请求,调用realsubject 对应的request功能来实现业务功能,自己不真正做业务。
动态代理
通常,我们会使用代理模式来实现 AOP,这就意味着代理模式可以优雅的解决侵入性业务问题。之所以优雅,其中一个点就在于,「动态」二字。较之静态,动态就体现出更加灵活,在运行时动态地对某些东西代理,代理它去做了一些其他的事情。这种动态依赖的是反射机制。因为静态代理需要预先定义好代理类的代码实现,而当大量使用静态代理时,就可能产生大量的代理类,随着类的数量及规模增大,代码维护成本也随之增大,为了解决这个问题,就有了动态地创建代理类的想法。
在运行期的代码中生成二进制字节码
由于JVM通过字节码的二进制信息加载类的,那么,如果我们在运行期系统中,遵循Java编译系统组织.class文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样,就完成了在代码中,动态创建一个类的能力了。
在运行时期可以按照Java虚拟机规范对class文件的组织规则生成对应的二进制字节码。当前有很多开源框架可以完成这些功能,如ASM,Javassist。
Proxy角色在执行代理业务的时候,无非是在调用真正业务之前或者之后做一些“额外”业务。
上图可以看出,代理类处理的逻辑很简单:在调用某个方法前及方法后做一些额外的业务。换一种思路就是:在触发(invoke)真实角色的方法之前或者之后做一些额外的业务。那么,为了构造出具有通用性和简单性的代理类,可以将所有的触发真实角色动作交给一个触发的管理器,让这个管理器统一地管理触发。这种管理器就是Invocation Handler。
动态代理模式的结构跟上面的静态代理模式有所不同的地方,就在于多引入了一个InvocationHandler角色。
在静态代理中,代理Proxy中的方法,都指定了调用了特定的realSubject中的对应的方法:
在上面的静态代理模式下,Proxy所做的事情,无非是调用在不同的request时,调用触发realSubject对应的方法;更抽象点看,Proxy所作的事情;在Java中 方法(Method)也是作为一个对象来看待了,
动态代理工作的基本模式就是将自己的方法功能的实现交给 InvocationHandler角色,外界对Proxy角色中的每一个方法的调用,Proxy角色都会交给InvocationHandler来处理,而InvocationHandler则调用具体对象角色的方法。如下图所示:
在这种模式之中:代理Proxy 和RealSubject 应该实现相同的功能,这一点相当重要。(这里说的功能,可以理解为某个类的public方法)
在面向对象的编程之中,如果想要约定Proxy 和RealSubject可以实现相同的功能,有两种方式:
a. 一个比较直观的方式,就是定义一个功能接口,然后让Proxy 和RealSubject来实现这个接口。
b. 还有比较隐晦的方式,就是通过继承。因为如果Proxy继承自RealSubject,这样Proxy则拥有了RealSubject的功能,Proxy还可以通过重写RealSubject中的方法,来实现多态。
其中JDK中提供的创建动态代理的机制,是以a这种思路设计的(基于接口),而cglib 则是以b思路设计的(基于类继承)。
资源出自动态代理的神总结文章:Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)
装饰者模式
装饰者模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰者来包裹真实的对象。
装饰者模式的实现上,与静态代理极为相似,都是透过增强被代理者的功能来做到扩展对象。关键的不同点在于:代理强调的是为其他对象提供一种代理以控制对这个对象的访问,而装饰者仅仅强调扩展,并不强调访问控制。而动态代理比装饰者模式更加灵活,被增强的对象和增强的内容都是可以更换的,动态化的。
AOP术语
AOP具体的通知包含
- @Before,前置通知,执行方法前执行
- @AfterReturn,返回通知,正常返回方法后执行
- @After,后置通知,方法最终结束后执行,相当于finaly
- @Around,环绕通知,围绕整个方法
- @AfterThrowing,异常通知,抛出异常后执行
开发者在命中连接点时,可以通过以上不同的通知,执行对应方法。这就是AOP中的Advisor。
一图胜千言:
辅助记忆各种通知类型:
具体术语包括
- Aspect,切面,一个关注点的模块。
例子中,LogAspect就是切面。 - JoinPoint, 连接点,程序执行中的某个点,某个位置。
例子中,testBean.getName()是连接点。 - PointCut,切点,切面匹配连接点的点,一般与切点表达式相关,就是切面如何切点。
例子中,@PointCut注解就是切点表达式,匹配对应的连接点 - Advice,通知,指在切面的某个特定的连接点上执行的动作,也叫增强行为。
例子中,before()与after()方法中的代码。 - TargetObject,目标对象,指被切入的对象,也就是被代理的对象。
例子中,从ctx中取出的testBean则是目标对象。 - Weave,织入,将Advice作用在JoinPoint的过程。
一图胜千言(图片源自网络):
Spring AOP过程
1、Spring加载自动代理器AnnotationAwareAspectJAutoProxyCreator,当作一个系统组件。
2、当一个bean加载到Spring中时,会触发自动代理器中的bean后置处理
3、bean后置处理,会先扫描bean中所有的Advisor
4、然后用这些Adviosr和其他参数构建ProxyFactory
5、ProxyFactory会根据配置和目标对象的类型寻找代理的方式(JDK动态代理或CGLIG代理)
6、然后代理出来的对象放回context中,完成Spring AOP代理配置,等待被代理类的被调用
7、响应被代理类被调度,设置拦截器回调(JDK Proxy透过InvocationHandler、CGLIB透过DynamicAdvisedInterceptor完成回调)。
源码分析
以AspectJ实现为例:
Maven POM依赖配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>定义一个待代理的业务Bean
1
2
3
4
5
6
7
8
9
10
11
12
13public class TestBean {
private String name;
public String getName() {
return name;
}
public void setName( String name ) {
this.name = name;
}
}定义一个切面Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class LogAspect {
public void getName() {
}
// 指向上面的getName()切点,透过getName()的PointCut注解指向真实被代理的方法,也就是execution里的表达式所指向的方法
public void before( JoinPoint jp ) {
String clazzName = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName();
System.out.println( "before " + clazzName + "." + methodName + " executing" );
}
public void after( JoinPoint jp ) {
String clazzName = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName();
System.out.println( "after " + clazzName + "." + methodName + " executing" );
}
// org.springframework.aop.aspectj.AspectJAfterThrowingAdvice
// @AfterThrowing( throwing = "ex", pointcut = "getName()" )
// public void afterThrowing( JoinPoint jp, Throwable ex ) {
// String clazzName = jp.getTarget().getClass().getName();
// String methodName = jp.getSignature().getName();
// System.out.println( "throw exception when " + clazzName + "." + methodName + " executing" );
// }
// org.springframework.aop.aspectj.AspectJAroundAdvice
// @Around( value = "getName()" )
// // 除了around通知,其余类型的通知都不能用ProceedingJoinPoint,只能用普通的JoinPoint
// public void around( ProceedingJoinPoint pjp ) throws Throwable {
// String clazzName = pjp.getTarget().getClass().getName();
// String methodName = pjp.getSignature().getName();
// System.out.println( "before " + clazzName + "." + methodName + " executing" );
// Object result = pjp.proceed();
// System.out.println( "after " + clazzName + "." + methodName + " executing, result = " + result );
// }
}配置业务Bean与切面Bean(aop-test.xml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy/>
<bean id="testBean" class="com.xu.test.aop.TestBean"/>
<bean class="com.xu.test.aop.LogAspect"/>
</beans>测试代理类执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class TestAOP {
public static void main( String[] args ) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop-test.xml");
TestBean tb = ctx.getBean( "testBean", TestBean.class );
tb.setName( "xxx" );
String name = tb.getName();
System.out.println( name );
}
}运行结果
before com.xu.test.aop.TestBean.getName executing
after com.xu.test.aop.TestBean.getName executing
xxx
上面的例子之所以能完成AOP的代理,只因为Spring的xml配置里面加了这一句
< aop : aspectj-autoproxy / >
加上了这一个配置,使得整个Spring项目拥有了AOP的功能。全局搜索下aspectj-autoproxy这个字段,可以发现,是这个类AspectJAutoProxyBeanDefinitionParser解析了这个元素。
其中的parse方法调用的是AopNamespaceUtils类中的registerAspectJAnnotationAutoProxyCreatorIfNecessary。这个方法作用是初始化一个AOP专用的Bean,并且注册到Spring容器中。
1 | class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { |
1 | public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( |
解析这三个操作,
1、第一句,注册一个AnnotationAwareAspectJAutoProxyCreator(称它为自动代理器),这个Creator是AOP的操作核心,也是扫描Bean,代理Bean的操作所在。
2、第二句,解析配置元素,决定代理的模式。其中有JDK动态代理,还有CGLIB代理。
3、第三句,作为系统组件,把Creator这个Bean,放到Spring容器中。让Spring实例化,启动这个Creator。
自动代理器
自动代理器 AnnotationAwareAspectJAutoProxyCreator 继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator里边实现了BeanPostProceesor接口的postProcessAfterInitialization方法,这个方法是在一个Bean被加载并注册到Spring IOC容器后,由BeanFactory回调执行的,也就是说,切面是在目标对象被实例化的时候织入目标对象的,更准确的说,切面是在目标对象的bean在完成初始实例化之后,由bean工厂回调bean的后置处理器将切面织入到目标对象中的。
1 | /** |
里边的wrapIfNecessary方法会生成一个新的代理对象,返回context(容器上下文)中加载。
AOP最核心的逻辑就在这个 wrapIfNecessary方法里边,里边主要是获取通知(advice/advisor)放到一个名为specificInterceptors的数组里,然后作为参数去调用createProxy方法,创建对应的代理对象:
1 | /** |
很显然,这里边有两个核心方法,就是getAdvicesAndAdvisorsForBean(获取通知),还有createProxy(创建代理)
获取通知
getAdvicesAndAdvisorsForBean,顾名思义,就是获取被代理的Bean所关联的advice及advisor,自然地,这里有个疑问,advice与advisor是什么关系,为什么获取通知不是只需要获取advice即可?Spring使用org.springframework.aop.Advisor接口表示切面的概念,当完成对目标对象方法的增强行为操作(也就是通知,Advice)和切入点(Point)的设计开发之后,需要一个对象将目标对象、增强行为和切入点三者结合起来,而Advisor(通知器)就是一个实现这个功能的对象,即通过Advisor通知器,可以定义哪些目标对象的哪些方法在什么地方使用这些增强的行为。简单来讲,Advisor=Advice+Point。
1 | public interface Advisor { |
事实上,debug进去AbstractAdvisorAutoProxyCreator对getAdvicesAndAdvisorsForBean的实现代码,可以看到,其实这里确实只是获取Advisor通知器而已,如上文说的,每个Advisor对象持有一个Advice通知,一步步debug,进入到AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(),找到BeanFactoryAspectJAdvisorsBuilder中的buildAspectJAdvisors方法,这个方法里边就是寻找AspectBean,然后返回AspectBean中的所有Advisor的过程实现:
1 | /** |
譬如,例子中的LogAspect就是AspectBean,它定义了Before与After两个通知(或者说增强行为),那么最终便返回LogAspect中的Before与After对应的Advisor通知器。
创建代理
1 | /** |
把Advisor丢到proxyFactory(ProxyConfig)之后,最后要从proxyFactory里获取一个代理对象。也就是,ProxyFactory的getProxy方法,一路debug进去方法,最终可以在DefaultAopProxyFactory中createAopProxy的实现中,看到代理对象是怎么被生成的:
1 | public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { |
ProxyFactory会根据配置与目标对象的类型,选择用JDK动态代理,还是CGLIB的代理,代理后的对象会放回context中,然后等到程序执行时,会直接调用这个代理类。
留意到这里的JDK动态代理 or CGLIB动态代理的选择逻辑,体现到了目标为接口时使用JDK动态代理,目标为类时使用CGLIB动态代理的意思。
至此,整个代理的织入、连接过程就已完成。接下来的问题是,调用时怎么给目标类(被代理类)作访问拦截的。
AOP代理拦截
上面的代码分析中,已经知道,在Spring AOP通过JDK的Proxy方式或者CGLIB方式生成代理对象的时候,相关的拦截器已经生成并配置到代理对象中去了。
那么,拦截器的回调,是怎么设置的呢?
有两种方式:
- JDK的Proxy方式生成代理对象:JdkDynamicAopProxy会通过连接点(ReflectiveMethodInvocation)来调用拦截器链中的拦截器(也就是调用通知方法)
- CGLIB方式生成代理对象:根据CGLIB使用要求,通过DynamicAdvisedInterceptor来完成回调。
在《Spring技术内幕(第2版)》中有截取到两种方式的拦截器在Spring代码中的实现。
JdkDynamicAopProxy的invoke拦截
具体实现看org.springframework.aop.framework.JdkDynamicAopProxy.invoke(Object, Method, Object[])源码:
1 | /** |
1 | /** |
1 | public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable { |
Cglib2AopProxy的intercept拦截
具体实现看org.springframework.aop.framework.Cglib2AopProxy.DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy)源码
1 | /** |
AOP拦截器的调用
两种方式对拦截器的调用都是在ReflectiveMethodInvocation中通过proceed方法实现。在proceed方法中逐个实现拦截器的拦截方法。每个拦截器在执行之前,需要对代理方法完成一个匹配判断(即Pointcut切点中需要进行matches匹配过程)。
1 |
|
至此,整个AOP实现的来龙去脉,便走完了个大致流程。
JVM级别的AOP
Reference
扫描二维码,分享此文章