Spring系列-8 AOP使用与原理
创始人
2024-05-29 06:57:33
0

背景

按照软件重构的思想,当多个类中存在相同的代码时,需要提取公共部分来消除代码坏味道。Java的继承机制允许用户在纵向上通过提取公共方法或者公共部分(模版方法方式)至父类中以消除代码重复问题;日志、访问控制、性能监测等重复的非业务代码揉杂在业务代码之中无法横向抽取,AOP技术为其提供了一个解决方案。
AOP技术将这些重复的非业务代码抽取出为一个模块,通过技术整合还原代码的逻辑和功能;即:在代码层面上消除了重复度,提高了可维护性,并在功能层面上得到还原。抽取重复代码作为一个模块是用户的问题,然而技术整合(对目标织入增强逻辑,后文介绍)以实现功能还原是AOP的目标和工作重心,Spring AOP是该技术的一种实现。

本文作为Spring系列的第八篇,介绍Spring框架中AOP的使用、注意事项和实现原理,原理部分会结合Spring框架源码进行。

Spring系列的后续文章如Spring系列-9 Async注解使用与原理和Spring系列-10 事务机制其底层原理都是Spring AOP。

1.AOP

常见的AOP实现方案有Spring AOP和AspectJ:相对于Spring AOP而言,AspectJ是一种更成熟、专业的AOP实现方案。AOP的技术整合(织入增强逻辑)可以发生在编译器、类加载期以及运行期:AspectJ在编译器(ajc)和类加载器(使用特定的类加载器)实现;Spring AOP在运行时通过动态代理方式实现。AspectJ提供了完整了AOP方案,而Spring AOP从实用性出发未常见的应用场景提供了技术方案,如不支持静态方法、构造方法等的AOP。

本文考虑到文章篇幅,下文暂不涉及其他AOP技术,后续会在《AspectJ使用与原理》中单独介绍AspectJ

Spring AOP构建于IOC之上,与IOC一起作为Spring框架的基石。Spring AOP底层使用动态代理技术实现,包括:JDK动态代理与CGLIB动态代理;JDK动态代理技术要求被代理对象基于接口,而CGLIB动态代理基于类的继承实现代理,从而要求被代理类不能为final类且被代理的方法不能被final、staic、private等修饰。二者都有局限性,在一定程度上相互弥补。

1.1 基本概念

[1] 执行点:在Spring AOP中指代目标类中具体的方法;
[2] 连接点:包含位置信息的执行点,位置信息包括:方法执行前、后、前后、异常抛出等;
[3] 切点:根据指定条件(类是否符合、方法是否符合等)过滤出的执行点的集合;
[4] 通知/增强:为目标对象增加的新功能,如在业务代码中引入日志、访问控制等功能;
[5] 切面:切面由切点和通知组成;
[6] 织入:将切面织入目标对象,形成代理对象的过程。

1.2 增强类型

Spring中使用Advise标记接口表示增强,Spring根据方位信息(方法执行前后、环绕、异常抛出等)为其定义了不同的子类接口。

public interface Advice {}

1.2.1 增强类型相关接口

[1] 前置增强
BeforeAdvice接口表示前置增强,由于Spring当前仅支持方法增强,所以可用的接口为MethodBeforeAdvice.

//同Advise接口,BeforeAdvice也是个空接口
public interface MethodBeforeAdvice extends BeforeAdvice {void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}

如上所示,MethodBeforeAdvice接口中仅有一个before方法,入参分别是方法对象、参数数组、目标对象;该方法会在目标对象的方法调用前调用。
[2] 后置增强

public interface AfterReturningAdvice extends AfterAdvice {void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}

该方法中仅有一个afterReturning方法,入参比before多处一个返回值;该方法会在目标对象的方法调用后调用。

[3] 环绕增强

@FunctionalInterface
// Interceptor 是Advise的字接口,且是空接口
public interface MethodInterceptor extends Interceptor {@NullableObject invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}

可通过invocation.proceed()语句调用目标对象方法并获得放回值,可在前后自定义逻辑,相对于前置和后置有更高的灵活性。

[4] 异常抛出增强

public interface ThrowsAdvice extends AfterAdvice {
}

ThrowsAdvice是一个空接口,起标签作用。在运行期间Spring通过反射调用afterThrowing接口,该接口可以被定义为:void afterThrowing(Method method, Object[] args, Object target, Throwable exception);
其中method、args和target是可选的,exception参数是必选的;在目标方法抛出异常后,实施增强。

除此之外,框架还定义了一种引介增强,用于在目标类中添加一些新的方法和属性。

1.2.2 案例介绍

case 1:前置、后置、环绕增强
定义前置通知:

@Slf4j
public class MyBeforeAdvice implements MethodBeforeAdvice {@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {LOGGER.info("----before----");}
}

定义后置通知:

@Slf4j
public class MyAfterReturningAdvice implements AfterReturningAdvice {@Overridepublic void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {LOGGER.info("----after----");}
}

定义环绕通知:

@Slf4j
public class MyRoundAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {LOGGER.info("====round before====");Object result = invocation.proceed();LOGGER.info("====round after====");return result;}
}

测试用例如下:

public class AdviceAopTest {@Testpublic void testAdvice() {ProxyFactory proxyFactory = new ProxyFactory();TaskService taskService = new TaskServiceImpl();proxyFactory.setTarget(taskService);proxyFactory.setInterfaces(TaskService.class);// 添加前置增强proxyFactory.addAdvice(new MyBeforeAdvice());// 添加后置增强proxyFactory.addAdvice(new MyAfterReturningAdvice());// 添加环绕增强proxyFactory.addAdvice(new MyRoundAdvice());// 获取代理对象TaskService proxy = (TaskService)proxyFactory.getProxy();proxy.sync();}
}

运行结果如下所示:
在这里插入图片描述

case 2:异常抛出增强
修改目标类代码逻辑:

@Slf4j
public class TaskServiceImpl implements TaskService{@Override@SneakyThrowspublic void sync() {LOGGER.info("[sync data]");throw new Exception("");}
}

测试用例如下:

public class ThrowsAdviceTest {@Testpublic void testAdvice() {ProxyFactory proxyFactory = new ProxyFactory();TaskService taskService = new TaskServiceImpl();proxyFactory.setTarget(taskService);proxyFactory.setInterfaces(TaskService.class);proxyFactory.addAdvice(new MyThrowsAdvice());TaskService proxy = (TaskService)proxyFactory.getProxy();proxy.sync();}
}

结果如下:
在这里插入图片描述

1.3 切点类型

框架定义切点是为了从目标类的连接点(执行点)中过滤出符合条件的部分,为此在切点类的内部提供类两个过滤器:ClassFilter和MethodMatcher,分别对类型和方法进行过滤。

public interface Pointcut {ClassFilter getClassFilter();MethodMatcher getMethodMatcher();// Pointcut.TRUE 对象表示所有目标类的所有方法均满足条件// (实例对应的ClassFilter和MethodMatcher对象的match方法均返回true)Pointcut TRUE = TruePointcut.INSTANCE;
}

Pointcut切点接口定义如上所示,Spring并基于此扩展出了多种切点类型;使得可以根据方法名、参数、是否包含注解以及表达式等进行过滤。

1.4 切面类型

Spring使用Advisor表示切面类型,可以分为3类:一般切面Advisor、切点切面PointcutAdvisor、引介切面IntroductionAdvisor;一般切面Advisor仅包含一个Advice, 即表示作用对象是所有目标类的所有方法;PointcutAdvisor包含Advice和Pointcut信息,可以通过切点定位出满足Pointcut过滤条件的执行点集合;IntroductionAdvisor对应于引介切点和增强。
其中:PointcutAdvisor及其子类DefaultPointcutAdvisor是较为常见的切面类型,源码如下:

public class DefaultPointcutAdvisor extends AbstractGenericPointcutAdvisor implements Serializable {private Pointcut pointcut = Pointcut.TRUE;private Advice advice = EMPTY_ADVICE;public DefaultPointcutAdvisor() {}public DefaultPointcutAdvisor(Advice advice) {this(Pointcut.TRUE, advice);}public DefaultPointcutAdvisor(Pointcut pointcut, Advice advice) {this.pointcut = pointcut;setAdvice(advice);}
}

DefaultPointcutAdvisor包含一个切点和一个增强类型属性:Pointcut的默认值为Pointcut.TRUE表示所有目标类的所有方法均为连接点;Advice的默认值为EMPTY_ADVICE:Advice EMPTY_ADVICE = new Advice() {};, 即表示不进行增强。

章节-1.2测试用例中为ProxyFactory添加切面部分逻辑为:proxyFactory.addAdvice(new MyBeforeAdvice()); 等价于 proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new MyBeforeAdvice()));.

2.使用方式

章节-1中涉及的ProxyFactory代理工厂提供了基于切面构造代理对象的能力,Spring框架结合IOC对此进行了一层封装以适应多种场景。封装后为用户提供了一套Spring风格的“API”(使用方式),该部分是本章节的重点部分。

2.1 xml配置方式

引入AOP的schema:


方式1:使用aop:aspect标签

定义目标类:

@Slf4j
public class ApplicationRunner {public void run() {LOGGER.info("exec...");}
}

引入增强类:

@Slf4j
public class EnhanceLog {public void beforeExec() {LOGGER.info("before exec");}public void afterExec() {LOGGER.info("after exec");}
}

在xml文件中配置AOP:



方式2:使用aop:advisor标签

引入增强类LogAdvice,需要实现org.aopalliance.intercept.MethodInterceptor接口:

@Slf4j
public class LogAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {LOGGER.info("before intercept exec");methodInvocation.proceed();LOGGER.info("after intercept exec");return null;}
}

在xml文件中配置AOP:

    

其中:advisor将所有的逻辑都封装在了MethodInterceptor的invoke方法中,通过方法完成增强;aspect通过配置对外展示需要增强逻辑,而不需要实现MethodInterceptor等Advice系列接口。相对而言,aspect的代码侵入性较低。

2.2 注解方式

Spring通过整合AspectJ为AOP提供了注解形式的使用方式;因此使用注解时,需要添加对aspectjweaver的依赖(由org.aspectj提供)。

2.2.1 切面注解 @Aspect

注解在类上用于标记切面类;其他注解都可以添加在该类中的方法上。

2.2.2 增强注解 @Before @After @Around @AfterThrowing @AfterReturing

被增强注解的方法内容作为增强。@Before表示前置增强,@AfterReturing表示后置增强,@Around表示环绕增强,@AfterThrowing表示异常抛出增强,@After表示方法正常执行完或者异常抛出都会执行的增强逻辑;与Spring中定义的增强类型基本保持一致。
被上述注解标注的方法可以增加一个JoinPoint类型(Around为ProceedingJoinPoint类型)的参数(也可不加)用于获取上下文信息;另外,@AfterThrowing还可添加异常类型的参数,而@AfterReturing可以添加一个Object类型的参数(表示运行结果),以下通过案例的形式进行介绍。

添加配置类:

@Configuration
@ComponentScan(basePackages = "com.seong.demo.annotation")
@EnableAspectJAutoProxy
public class AopDemoConfiguration {
}

添加目标类:

@Component
@Slf4j
public class DataTask {public String syncData() {LOGGER.info("start sync data");return "success";}
}

添加测试用例:

public class Application {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopDemoConfiguration.class);DataTask dataTask = (DataTask)context.getBean("dataTask");dataTask.syncData();}
}

case 1: 前置、后置
切面配置如下所示:

@Component
@Aspect
@Slf4j
public class MyNormalAspect {@Before("execution(public String com.seong.demo.annotation.DataTask.*(..))")public void beforeExec(JoinPoint joinPoint) {LOGGER.info("[Before DataTask], joinPoint is {}.", joinPoint);}@AfterReturning("execution(public String com.seong.demo.annotation.DataTask.*(..))")public void afterExec(JoinPoint joinPoint) {LOGGER.info("[After DataTask], result is {}, joinPoint is {}.", joinPoint);}
}

得到如下运行结果:
在这里插入图片描述

case 2: 环绕通知
在切面中配置环绕增强,如下所示:

@Component
@Aspect
@Slf4j
public class MyAroundAspect {@SneakyThrows@Around("execution(public String com.seong.demo.annotation.DataTask.*(..))")public void aroundExec(ProceedingJoinPoint joinPoint) {LOGGER.info("[Around DataTask] call before.");Object result = joinPoint.proceed();LOGGER.info("[Around DataTask] call end, result is {}.", result);}
}

得到如下运行结果:
在这里插入图片描述
case 3: 异常抛出增强
在切面中配置异常抛出增强,如下所示:

@Component
@Aspect
@Slf4j
public class MyExceptionAspect {@AfterThrowing("execution(public String com.seong.demo.annotation.DataTask.*(..))")public void afterThrowingExec(JoinPoint joinPoint) {LOGGER.info("[AfterThrow DataTask], joinPoint is {}.", joinPoint);}
}

得到如下运行结果:在这里插入图片描述

2.4 expression表达式

参考文章:《AspectJ使用与原理》

3.实现原理

略,待补充

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...