返回顶部

AOP面向切面编程 重点内容汇总

我们总是听到AOP ,又称面向切面编程,那面向切面编程在日常开发中的应用场景有哪些呢 ?

我们来一起梳理一下:

什么时候会用到面向切面编程呢 ?其实就是有一些公共的逻辑,需要在很多地方用到,那这些代码如果在每个位置都写一下的话,当需要修改的时候,又必须将这些代码全都找出来进行修改,就会很冗余,为了解决这个问题:将公共的代码抽取出来,当代码运行到指定位置的时候,将公共的代码逻辑切入到相应的位置,即使用AOP的方式进行编程。

使用的是代理机制,在不改变原程序的基础上对代码进行增强操作,比如:统一的日志记录、性能监控、异常处理、参数校验 等等这些非业务核心的功能,被单独抽取出来,与业务代码分离,横切在核心业务代码之上AOP就是在某一个类或者方法执行的某个时机,声明在执行到这里之前、之后、中途要执行什么,执行完之后要接着执行什么。

 

 

我们其实可以利用自定义注解 和 切面来实现同样的效果。

 

 

一、要使用AOP切面编程的话,需要明确几个问题:

1、通过什么方式切入,是通过自定义注解,还是说直接指定某些包的位置。

2、在何时进行切入,这就涉及到我们考虑在所做业务的什么时机进行切入,执行相应的切面方法。

3、切入之后,需要做的公用操作是什么 ?日志记录、参数校验、权限校验、一异常处理等等。

二、专业术语

接下来,我们一起来明确一些切面编程上的专业术语:其中

 

连接点(Joinpoint)、切点(Pointcut)、通知(Advice)、切面(Aspect)为主要部分。

  • 连接点Joinpoint 

连接点描述的是位置

在程序的整个执行业务流程中,可以织入切面的位置。比如在某些待切方法的执行前后、参数调用处,异常抛出之后等位置。这些位置都可以理解成连接点。

  • 切点Pointcut

切点本质上指方法,就是哪个方法将要被切入,那这个方法可以叫做切点(所以方法叫做切点,也就是执行过程中真正被植入切面的方法)

  • 通知 Advice

通知其实就是指要实现增强效果的那些代码方法。后面就知道了,比如加了@Before方法的test()方法,那这个方法就是通知。

注意:

切面(切点+通知)就是在一个正常竖向做的业务流程中 ,在某些时机做横向拦截,然后做一些处理, 通过切点切入。链接点的链接,最终SpringAOP会将其定义的内容织入到约定的流程中,在动态代理中可以把它理解成一个拦截器

 三、常见的通知类型:

  • 前置通知(before):在动态代理反射原有对象方法执行前   ,或者执行环绕通知前执行的通知功能。
  • 后置通知(after):在动态代理反射原有对象方法执行后 ,  或者执行环绕通知后执行的通知功能。无论是否抛出异常,他都会被执行。
  • 返回通知(afterReturning):在原有对象方法正常返回后,或者执行环绕通知后正常返回(无异常)执行的通知功能。
  • 异常通知(afterThrowing):在动原有对象方法异常返回后,或者执行环绕通知产生异常后执行的通知功能。
  • 环绕通知(around):他可以转向当前被拦截对象的方法,并让之继续执行。

四、通知的执行顺序

spring4.x版本的通知的执行结果顺序是:  https://blog.csdn.net/weixin_38174052/article/details/125281813

@Around注解方法的前半部分业务逻辑
->@Before注解方法的业务逻辑
->目标方法的业务逻辑
->@Around注解方法的后半部分业务逻辑(@Around注解方法内的业务逻辑若对ProceedingJoinPoint.proceed()方法没做捕获异常处理,直接向上抛出异常,则不会执行Around注解方法的后半部分业务逻辑;若做了异常捕获处理,则会执行)。
->@After(不管目标方法有无异常,都会执行@After注解方法的业务逻辑)这里是指被切入的方法的异常情况,而不是指切面方法是否发生异常的情况!!!这个的是亲自验证了一下。
->@AfterReturning(若目标方法无异常,执行@AfterReturning注解方法的业务逻辑)
->@AfterThrowing(若目标方法有异常,执行@AfterThrowing注解方法的业务逻辑)


 

spring源码版本是5.2.14

:所以包含通知注解的执行结果如下,
@Around注解方法的前半部分业务逻辑
->@Before注解方法的业务逻辑
->目标方法的业务逻辑
->@AfterThrowing(若目标方法有异常,执行@AfterThrowing注解方法的业务逻辑)
->@AfterReturning(若目标方法无异常,执行@AfterReturning注解方法的业务逻辑)
->@After(不管目标方法有无异常,都会执行@After注解方法的业务逻辑)
->@Around注解方法的后半部分业务逻辑(@Around注解方法内的业务逻辑若对ProceedingJoinPoint.proceed()方法没做捕获异常处理,直接向上抛出异常,则不会执行Around注解方法的后半部分业务逻辑;若做了异常捕获处理,则会执行)。

 如果还是感觉不太清晰,这些顺序的代码验证请看:  https://blog.csdn.net/monkey_wei/article/details/105521389

 要被增强的类及方法:

 正常执行结果如下:

 抛异常结果如下:

到此通知的执行顺序搞清楚啦 。

五、接下来我们在看看切点表达式,的具体规则:

 

1、切点表达式   Pointcut

切点表达式就是用来定义 通知 (Advice) 往哪些方法上 切入的。

  • 用于定义通知切入
  • 而且不局限于某个方法,可以切入多个方法
  • 告诉程序,这个切点可以匹配哪些方法

2、切点表达式的格式

execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形参)) [异常])

  •    访问权限修饰符:可选

  默认是4个权限都包括

  只写public就表示只包括公开的方法。

  • 返回值类型:必填

  星号* 表示返回值类型为任意

  • 全限定名:可选

  也就是类名,而且是具体的地址
  两个点 .. 代表当前包及其子包下所有的类
  省略时,表示所有的类

  • 方法名:必填

  星号* 表示所有方法
  比如 set* 表示所有的set方法

  • 形式参数列表: 必填

    • () ——表示没有参数的方法
    • (..) ——参数类型和个数随意的方法
    • (*) —— 只有一个参数的方法
    • (*,String) —— 第一个参数随意,第二个参数是String的
  • 异常:可选

    • 省略时表示任意类型的异常
 
 
理解以下切点表达式   :
@Around(value = "execution(* com.xxx.xxxs.*.controller..*(..))")   这个就代表是返回值是任意类型,的 com.xxx.xxxs.*.controller包下的所有类及子包下的所有类,中的所有方法,而且是任意数量参数的方法都会被切入。
 
 

/**
     * 定义切点(织入点)
     *  execution(* com.nowcoder.community.controller.*.*(..))
     *      - 第一个 * 表示 支持任意类型返回值的方法
     *      - com.nowcoder.community.controller 表示这个包下的类
     *      - 第二个 * 表示 controller包下的任意类
     *      - 第三个 * 表示 类中的任意方法
     *      - (..) 表示方法可以拥有任意参数
     *   可以根据自己的需求替换。
     *
     */

 

 

 

 

上面的是对于单条匹配的规则,如果涉及到设置到多条匹配规则的话,可以用下面的进行组合,具体说明如下 :

 

由下列方式来定义或者通过 &&、 ||、 !、 的方式进行组合:

 

对于单条匹配规则中的关键属性介绍如下:

 

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

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

  • this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;        

  • target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;

  • args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;

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

  • @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;

  • @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;

  • @annotation:用于匹配当前执行方法持有指定注解的方法;

 

重点示例

  • 任意公共方法的执行:execution(public * *(..))
  • 任何一个以“set”开始的方法的执行:execution(* set*(..))
  • AccountService 接口的任意方法的执行:execution(* com.xyz.service.AccountService.*(..))
  • 定义在service包里的任意方法的执行: execution(* com.xyz.service.*.*(..))
  • 定义在service包和所有子包里的任意类的任意方法的执行:execution(* com.xyz.service..*.*(..))
  • 第一个*表示匹配任意的方法返回值, ..(两个点)表示零个或多个,第一个..表示service包及其子包,第二个*表示所有类, 第三个*表示所有方法,第二个..表示方法的任意参数个数
  • 定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")
  • pointcutexp包里的任意类: within(com.test.spring.aop.pointcutexp.*)
  • pointcutexp包和所有子包里的任意类:within(com.test.spring.aop.pointcutexp..*)
  • 实现了Intf接口的所有类,如果Intf不是接口,限定Intf单个类:this(com.test.spring.aop.pointcutexp.Intf)
  • 当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型
  • 带有@Transactional标注的所有类的任意方法: 
  • @within(org.springframework.transaction.annotation.Transactional)
  • @target(org.springframework.transaction.annotation.Transactional)
  • 带有@Transactional标注的任意方法:@annotation(org.springframework.transaction.annotation.Transactional)
  • @within和@target针对类的注解,@annotation是针对方法的注解
  • 参数带有@Transactional标注的方法:@args(org.springframework.transaction.annotation.Transactional)
  • 参数为String类型(运行是决定)的方法: args(String)

 

六、使用面向切面编程的可以解决的问题:

(1)前面提到了代理模式,可以极大的减少代码编写量,减少了代码耦合度,并且提高了可维护性

(2)对于复杂的业务,很多模块或者方法需要公用的逻辑:假设每个方法都需要日志/事务/权限,这些重复通用的业务和业务代码混杂在一起。交叉业务过多,就会导致以下两个问题

第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交又业务代码的话,需要修改多处。(维护不便)
第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。

结合老杜的横向编程的图,更好理解

 

面向切面编程的优点

  1. 将于业务逻辑无关的通用业务逻辑代码单独提取出来,减少了对业务代码的干扰
  2. 便于维护
  3. 减少了代码编写量(冗余
  4. 遵循了OCP开闭原则,并且实现了解耦,降低了代码的耦合度
 
posted @ 2024-03-30 14:05  fen斗  阅读(116)  评论(0编辑  收藏  举报