聊一聊 AOP:表现形式与基础概念

aop 终于提上日程来写一写了。

本系列分为 上、中、下三篇。上篇主要是介绍如果使用 AOP ,提供了demo和配置方式说明;中篇来对实现 AOP 的技术原理进行分析;下篇主要针对Spring中对于AOP的实现进行源码分析。

项目地址

项目地址:glmapper-ssm-parent

这个项目里面包含了下面几种 AOP 实现方式的所有代码,有兴趣的同学可以fork跑一下。这个demo中列举了4中方式的实现:

基于代码的方式

基于纯POJO类的方式

基于Aspect注解的方式

基于注入式Aspect的方式

目前我们经常用到的是基于Aspect注解的方式的方式。下面来一个个了解下不同方式的表现形式。

基于代理的方式

这种方式看起来很好理解,但是配置起来相当麻烦;小伙伴们可以参考项目来看,这里只贴出比较关键的流程代码。

1、首先定义一个接口:GoodsService

2、GoodsService 实现类

3、定义一个通知类 LoggerHelper,该类继承 MethodBeforeAdvice和 AfterReturningAdvice。

4、重点,这个配置需要关注下。这个项目里面我是配置在applicationContext.xml文件中的。

5、使用:注解注入方式

6、使用:工具类方式手动获取bean

这个方式是通过一个SpringContextUtil工具类来获取代理对象的。

7、SpringContextUtil 类的定义

这个还是有点坑的,首先SpringContextUtil是继承ApplicationContextAware这个接口,我们希望能够SpringContextUtil可以被Spring容器直接管理,所以,需要使用 @Component 标注。标注了之后最关键的是它得能够被我们配置的注入扫描扫到(亲自踩的坑,我把它放在一个扫不到的包下面,一直debug都是null;差点砸电脑…)

8、运行结果

上面就是最最经典的方式,就是通过代理的方式来实现AOP的过程。

纯POJO切面

注意这里和LoggerHelper的区别,这里的LoggerAspect并没有继承任何接口或者抽象类。

1、POJO 类定义

2、配置文件

注意这里LoggerAspect中的before和afterReturning如果有参数,这里需要处理下,否则会报0 formal unbound in pointcut异常。

@AspectJ 注解驱动方式

这种方式是最简单的一种实现,直接使用 @Aspect 注解标注我们的切面类即可。

1、定义切面类,并使用 @Aspect 进行标注

2、使用方式1:配置文件方式声明 bean

3、客户端使用:

4、使用方式2:使用@component注解托管给IOC

5、客户端代码:

6、比较完整的一个LoggerAspectInject,在实际工程中可以直接参考

注入式 AspectJ 切面

这种方式我感觉是第二种和第三种的结合的一种方式。

1、定义切面类

2、XML 配置

3、结果

## 表达式

从上面的例子中我们都是使用一些正则表达式来指定我们的切入点的。在实际的使用中,不仅仅是execution,还有其他很多种类型的表达式。下面就列举一些:

1、execution

用于匹配方法执行的连接点;

execution()表达式的主体;

第一个 "*" 符号表示返回值的类型任意;

com.glmapper.book.web.controller AOP所切的服务的包名,即,我们的业务部分

包名后面的"." 表示当前包及子包

第二个"*" 表示类名,即所有类

.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型

2、within

用于匹配指定类型内的方法执行;

@within:用于匹配所以持有指定注解类型内的方法;

任何目标对象对应的类型持有AuthAnnotation注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。

3、this

用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;this中使用的表达式必须是类型全限定名,不支持通配符;

4、target

用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;target中使用的表达式必须是类型全限定名,不支持通配符;

@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;任何目标对象持有Secure注解的类方法;这个和@within一样必须是在目标对象上声明这个注解,在接口上声明的对它同样不起作用。

5、args

用于匹配当前执行的方法传入的参数为指定类型的执行方法;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;

@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解AuthAnnotation;动态切入点,类似于arg指示符;

6、@annotation

使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;

还有一种是bean的方式,没用过。有兴趣可以看看。

例子在下面说到的基础概念部分对应给出。

基础概念

基础概念部分主要将 AOP 中的一些概念点捋一捋,这部分主要参考了官网上的一些解释。

AOP

, 即面向切面编程, 它与 ( , 面向对象编程) 相辅相成, 提供了与 不同的抽象软件结构的视角。在 中,我们以类(class)作为我们的基本单元, 而 中的基本单元是Aspect(切面)

横切关注点():独立服务,如系统日志。如果不是独立服务(就是与业务耦合比较强的服务)就不能横切了。通常这种独立服务需要遍布系统各个角落,遍布在业务流程之中。

Target Object

目标对象。织入 advice 的目标对象。 目标对象也被称为 。

因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object);注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类。

织入(Weave)

即应用在的过程,这个过程叫织入。从另外一个角度老说就是将 和其他对象连接起来, 并创建 的过程。

根据不同的实现技术, 织入有三种方式:

编译器织入,这要求有特殊的编译器

类装载期织入, 这需要有特殊的类装载器

动态代理织入, 在运行期为目标类添加增强( )生成子类的方式。

Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期

代理

Spring AOP默认使用代理的是标准的JDK动态代理。这使得任何接口(或一组接口)都可以代理。

Spring AOP也可以使用CGLIB代理。如果业务对象不实现接口,则默认使用CGLIB。对接口编程而不是对类编程是一种很好的做法;业务类通常会实现一个或多个业务接口。在一些特殊的情况下,即需要通知的接口上没有声明的方法,或者需要将代理对象传递给具体类型的方法,有可能强制使用CGLIB。

Introductions

我们知道Java语言本身并非是动态的,就是我们的类一旦编译完成,就很难再为他添加新的功能。但是在一开始给出的例子中,虽然我们没有向对象中添加新的方法,但是已经向其中添加了新的功能。这种属于向现有的方法添加新的功能,那能不能为一个对象添加新的方法呢?答案肯定是可以的,使用introduction就能够实现。

introduction:动态为某个类增加或减少方法。为一个类型添加额外的方法或字段。Spring AOP 允许我们为 引入新的接口(和对应的实现)。

Aspect

切面:通知和切入点的结合。

切面实现了cross-cutting(横切)功能。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。

而将散落于各个业务对象之中的Cross-cutting concerns 收集起来,设计各个独立可重用的对象,这些对象称之为Aspect;在上面的例子中我们根据不同的配置方式,定义了四种不同形式的切面。

Joinpoint

Aspect 在应用程序执行时加入业务流程的点或时机称之为 Joinpoint ,具体来说,就是 Advice 在应用程序中被呼叫执行的时机,这个时机可能是某个方法被呼叫之前或之后(或两者都有),或是某个异常发生的时候。

Joinpoint & ProceedingJoinPoint

环绕通知 = 前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的。

环绕通知 ProceedingJoinPoint 执行 proceed 方法 的作用是让目标方法执行 ,这 也是环绕通知和前置、后置通知方法的一个最大区别。

Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法;暴露出这个方法,就能支持这种切面(其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。

在环绕通知的方法中是需要返回一个Object类型对象的,如果把环绕通知的方法返回类型是void,将会导致一些无法预估的情况,比如:404。

Pointcut

匹配 的谓词。与切入点表达式相关联, 并在切入点匹配的任何连接点上运行。(例如,具有特定名称的方法的执行)。由切入点表达式匹配的连接点的概念是的核心,默认使用切入点表达式语言。

在 中, 所有的方法都可以认为是, 但是我们并不希望在所有的方法上都添加 , 而 的作用就是提供一组规则(使用 来描述) 来匹配, 给满足规则的 添加 。

Pointcut 和 Joinpoint

在 中, 所有的方法执行都是 。 而 是一个描述信息,它修饰的是 , 通过 ,我们就可以确定哪些 可以被织入。 因此 和 本质上就是两个不同维度上的东西。

是在 上执行的, 而 规定了哪些 可以执行哪些 。

Advice

概念

Advice 是我们切面功能的实现,它是切点的真正执行的地方。比如像前面例子中打印时间的几个方法(被@Before等注解标注的方法都是一个通知);Advice 在 Jointpoint 处插入代码到应用程序中。

分类

BeforeAdvice,AfterAdvice,区别在于Advice在目标方法之前调用还是之后调用,Throw Advice 表示当目标发生异常时调用Advice。

before advice: 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)

after return advice: 在一个 join point 正常返回后执行的 advice

after throwing advice: 当一个 join point 抛出异常后执行的 advice

after(final) advice: 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.

around advice:在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.

Advice、JoinPoint、PointCut 关系

下面这张图是在网上一位大佬的博客里发现的,可以帮助我们更好的理解这些概念之间的关系。

图片源自网络

上面是对于AOP中涉及到的一些基本概念及它们之间的关系做了简单的梳理。

一些坑

在调试程序过程中出现的一些问题记录

1、使用AOP拦截controller层的服务成功,但是页面报错404

这里需要注意的是再使用环绕通知时,需要给方法一个返回值。

2、0 formal unbound in pointcut

在spring 4.x中 提供了aop注解方式 带参数的方式。看下面例子:

比如说这里,Before中有两个int类型的参数,如果此时我们在使用时没有给其指定参数,那么就会抛出:Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut异常信息。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180625A020K600?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券