技术分享4期Spring AOP 深入剖析

Posted 众安金融技术部allstar

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了技术分享4期Spring AOP 深入剖析相关的知识,希望对你有一定的参考价值。

前言:AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了将不同的关注点分离出来的效果。本文深入剖析Spring的AOP的原理。

1

AOP相关的概念

1)Aspect:切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面;

2)Join point:连接点,也就是可以进行横向切入的位置;

3)Advice:通知,切面在某个连接点执行的操作(分为:Before advice,After returning advice,After throwing advice,After (finally) advice,Around advice);

4)Pointcut:切点,符合切点表达式的连接点,也就是真正被切入的地方;


2

AOP的实现原理

AOP分为静态AOP和动态AOP。静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。动态AOP是指将切面代码进行动态织入实现的AOP。Spring的AOP为动态AOP,实现的技术为:JDK提供的动态代理技术  CGLIB(动态字节码增强技术)。尽管实现技术不一样,但都是基于代理模式都是生成一个代理对象


1.JDK动态代理

主要使用到 InvocationHandler 接口和 Proxy.newProxyInstance() 方法。JDK动态代理要求被代理实现一个接口,只有接口中的方法才能够被代理。其方法是将被代理对象注入到一个中间对象,而中间对象实现InvocationHandler接口,在实现该接口时,可以在 被代理对象调用它的方法时,在调用的前后插入一些代码。而 Proxy.newProxyInstance() 能够利用中间对象来生产代理对象。插入的代码就是切面代码。所以使用JDK动态代理可以实现AOP。我们看个例子:

被代理对象实现的接口,只有接口中的方法才能够被代理:

被代理对象:

【技术分享4期】Spring AOP 深入剖析

代理中间类:

【技术分享4期】Spring AOP 深入剖析

测试:

【技术分享4期】Spring AOP 深入剖析

执行结果:

【技术分享4期】Spring AOP 深入剖析

我们看到在 UserService接口中的方法addUser 和 getUser方法的前面插入了我们自己的代码。这就是JDK动态代理实现AOP的原理。

我们看到该方式有一个要求,被代理的对象必须实现接口,而且只有接口中的方法才能被代理

2.CGLIB(code generate libary)

字节码生成技术实现AOP,其实就是继承被代理对象,然后Override需要被代理的方法,在覆盖该方法时,自然是可以插入我们自己的代码的。因为需要Override被代理对象的方法,所以自然CGLIB技术实现AOP时,就必须要求需要被代理的方法不能是final方法,因为final方法不能被子类覆盖。我们使用CGLIB实现上面的例子:

【技术分享4期】Spring AOP 深入剖析

【技术分享4期】Spring AOP 深入剖析

输出结果:

【技术分享4期】Spring AOP 深入剖析

我们看到达到了同样的效果。它的原理是生成一个父类enhancer.setSuperclass(this.target.getClass())的子类enhancer.create(),然后对父类的方法进行拦截enhancer.setCallback(this). 对父类的方法进行覆盖,所以父类方法不能是final的。


3.spring实现AOP的相关源码


【技术分享4期】Spring AOP 深入剖析

从上面的源码我们可以看到:

【技术分享4期】Spring AOP 深入剖析


如果被代理对象实现了接口,那么就使用JDK的动态代理技术,反之则使用CGLIB来实现AOP,所以Spring默认是使用JDK的动态代理技术实现AOP的


JdkDynamicAopProxy的实现其实很简单:

【技术分享4期】Spring AOP 深入剖析

3

Spring AOP的配置

Spring中AOP的配置一般有两种方法,一种是使用 <aop:config> 标签在xml中进行配置,一种是使用注解以及@Aspect风格的配置。


1.基于<aop:config>的AOP配置

下面是一个典型的事务AOP的配置:

【技术分享4期】Spring AOP 深入剖析

再看一个例子


【技术分享4期】Spring AOP 深入剖析

<aop:aspect> 配置一个切面;<aop:pointcut>配置一个切点,基于切点表达式;<aop:before>,<aop:after>,<aop:around>是定义不同类型的advise. aspectBean 是切面的处理bean:


【技术分享4期】Spring AOP 深入剖析

2.基于注解和@Aspect风格的AOP配置

首先我们启用基于注解的事务配置

【技术分享4期】Spring AOP 深入剖析

然后扫描Service包

【技术分享4期】Spring AOP 深入剖析

最后在service上进行注解

【技术分享4期】Spring AOP 深入剖析

搞定。这种事务配置方式,不需要我们书写pointcut表达式,而是我们在需要事务的类上进行注解。但是如果我们自己来写切面的代码时,还是要写pointcut表达式。下面看一个例子(自己写切面逻辑)


首先去扫描 @Aspect 注解定义的 切面:

【技术分享4期】Spring AOP 深入剖析

启用@AspectJ风格的注解:

【技术分享4期】Spring AOP 深入剖析

这里有两个属性,<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>, proxy-target-class="true" 这个最好不要随便使用,它是指定只能使用CGLIB代理,那么对于final方法时会抛出错误,所以还是让spring自己选择是使用JDK动态代理,还是CGLIB. expose-proxy="true"的作用后面会讲到。

切面代码:

【技术分享4期】Spring AOP 深入剖析


我们使用到了 @Aspect 来定义一个切面;@Component是配合<context:component-scan/>,不然扫描不到;@Order定义了该切面切入的顺序因为在同一个切点,可能同时存在多个切面,那么在这多个切面之间就存在一个执行顺序的问题。该例子是一个切换数据源的切面,那么他应该在 事务处理 切面之前执行,所以我们使用 @Order(0) 来确保先切换数据源,然后加入事务处理。@Order的参数越小,优先级越高,默认的优先级最低:

【技术分享4期】Spring AOP 深入剖析

关于数据源的切换可以参加专门的博文:http://www.cnblogs.com/digdeep/p/4512368.html

3.切点表达式(pointcut)

上面我们看到,无论是 <aop:config> 风格的配置,还是 @Aspect 风格的配置,切点表达式都是重点。都是我们必须掌握的。

 1> pointcut语法形式(execution):

【技术分享4期】Spring AOP 深入剖析

带有 ? 号的部分是可选的,所以可以简化成:

ret-type-pattern name-pattern(param_pattern) 返回类型,方法名称,参数三部分来匹配配置起来其实也很简单: * 表示任意返回类型,任意方法名,任意一个参数类型; .. 连续两个点表示0个或多个包路径,还有0个或多个参数。就是这么简单。看下例子:

【技术分享4期】Spring AOP 深入剖析

注意这里,将类名和包路径是一起来处理的,并没有进行区分,因为类名也是包路径的一部分。

参数param-pattern部分比较复杂: () 表示没有参数,(..)参数不限,(*,String) 第一个参数不限类型,第二参数为String.

2> within() 语法:

within()只能指定(限定)包路径(类名也可以看做是包路径),表示某个包下或者子报下的所有方法:

【技术分享4期】Spring AOP 深入剖析

3> this() 与 target():

this是指代理对象,target是指被代理对象(目标对象)。所以 this() 和 target() 分别限定 代理对象的类型和被代理对象的类型:

this(net.aazj.service.UserService): 实现了UserService的代理对象(中的所有方法);

target(net.aazj.service.UserService): 被代理对象实现了UserService(中的所有方法);

4> args():

限定方法的参数的类型:

args(net.aazj.pojo.User): 参数为User类型的方法

5> 

@target(), @within(), @annotation(), @args():

这些语法形式都是针对注解的,比如带有某个注解的类,带有某个注解的方法,参数的类型带有某个注解:

@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)

两者都是指被代理对象类上有 @Transactional 注解的(类的所有方法),(两者似乎没有区别???)

@annotation(org.springframework.transaction.annotation.Transactional): 方法 带有@Transactional 注解的所有方法

@args(org.springframework.transaction.annotation.Transactional):参数的类型 带有@Transactional 注解的所有方法

6> bean(): 指定某个bean的名称

【技术分享4期】Spring AOP 深入剖析

4

向注解处理方法传递参数

有时我们在写注解处理方法时,需要访问被拦截的方法的参数。此时我们可以使用 args() 来传递参数,下面看一个例子:


【技术分享4期】Spring AOP 深入剖析

【技术分享4期】Spring AOP 深入剖析

方法:

【技术分享4期】Spring AOP 深入剖析

它会拦截 net.aazj.service 包下或者子包下的getUser方法,并且该方法的第一个参数必须是int型的,那么使用切点表达式args(userId,..)就可以使我们在切面中的处理方法before3中可以访问这个参数。

before2方法也让我们知道也可以通过 JoinPoint 参数来获得被拦截方法的参数数组。JoinPoint 是每一个切面处理方法都具有的参数,@Around类型的具有的参数类型为ProceedingJoinPoint。通过JoinPoint或者ProceedingJoinPoint参数可以访问到被拦截对象的一些信息(参见上面的before2方法)。


5

Spring AOP的缺陷

因为Spring AOP是基于动态代理对象的,那么如果target中的方法不是被代理对象调用的,那么就不会织入切面代码,看个例子:


【技术分享4期】Spring AOP 深入剖析

看到上面的 addUser() 方法中,我们调用了 getUser() 方法,而getUser() 方法是谁调用的呢?是UserServiceImpl的实例,不是代理对象,那么getUser()方法就不会被织入切面代码。

切面代码如下:

【技术分享4期】Spring AOP 深入剖析

执行如下代码:

【技术分享4期】Spring AOP 深入剖析

输出结果如下

【技术分享4期】Spring AOP 深入剖析

虽然 getUser()方法 被调用了,但是因为不是代理对象调用的,所以 AOPTest.m1() 方法并没有执行。这就是Spring aop的缺陷。解决方法如下:


首先: 将 <aop:aspectj-autoproxy /> 改为:

【技术分享4期】Spring AOP 深入剖析

然后,修改UserServiceImpl中的 addUser() 方法:

【技术分享4期】Spring AOP 深入剖析

((UserService)AopContext.currentProxy()).getUser(2);先获得当前的代理对象,然后在调用 getUser() 方法,就行了。
expose-proxy="true" 表示将当前代理对象暴露出去,不然 AopContext.currentProxy() 或得的是 null .

修改之后的运行结果

【技术分享4期】Spring AOP 深入剖析

以上是关于技术分享4期Spring AOP 深入剖析的主要内容,如果未能解决你的问题,请参考以下文章

Spring Ioc源码深入剖析预备知识

深入浅出Spring原理及实战「原理分析专题」不看源码就带你剖析AOP容器核心流程以及运作原理

死磕Spring AOP系列4:剖析AOP schema方式原理

免费深入浅出解读 Spring 源码:IOC/AOP 篇 | Chat · 预告

spring源码剖析AOP实现原理剖析

Spring框架深入--AOP面向切面