AOP 面向切面编程
在不改变原有方法的前提下,添加新的功能,一般是日志、事务或其他非业务的逻辑。
代理模式
属于经典的二十三种设计模式之一,为结构型模式。
作用为不直接调用目标类方法,而是通过代理类调用目标类方法。
一、引入的例子
- 起初,我们有一个计算器的模拟类,实现了整数的加减乘除;
public interface Calculator { int add(int a, int b); int sub(int a, int b); int mul(int a, int b); int div(int a, int b); } public class CalculatorImpl implements Calculator{ @Override public int add(int a, int b) { int result = a + b; System.out.println("a = " + a + ", b = " + b +", a + b = " + result); return result; } @Override public int sub(int a, int b) { int result = a - b; System.out.println("a = " + a + ", b = " + b +", a - b = " + result); return result; } @Override public int mul(int a, int b) { int result = a * b; System.out.println("a = " + a + ", b = " + b +", a × b = " + result); return result; } @Override public int div(int a, int b) { int result = a / b; System.out.println("a = " + a + ", b = " + b +", a ÷ b = " + result); return result; } }
- 主要的功能我们已经实现。但是现在,我们想在所有计算操作的前后加上操作日志,此时我们需要修改每个方法,如下以add为例,其他类似:
public int add(int a, int b) { System.out.println("[日志] add 方法开始执行...") int result = a + b; System.out.println("a = " + a + ", b = " + b +", a + b = " + result); System.out.println("[日志] add 方法结束...") return result; }
- 回顾一下,我们发现如下几个问题:
- 我们为了增加日志,侵入了核心代码,破坏了原有方法的稳定性;
- 所有的方法都需要加类似的代码,重复劳动,代码冗余;
- 如果后面要修改一个日志,就要动核心代码,耦合性太高。
- 为解决上述问题,我们能想到的方法就是
解耦
:将附加功能,即此处的日志,从业务代码中抽离出去。 - 但是如何将这些附加功能抽离呢?如果只是单纯的提取积累并不能解决这个问题,所以我们需要引入新的技术——代理模式。
二、代理模式
代理模式,也叫 虚拟代理,远程代理, Protection Proxy 等,是一种结构型设计模式,用于提供对象的替代品,以便在代理对象中提供一些附加能力。
术语解释:
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象和方法。
- 目标:被代理“套用”了非核心逻辑代码的类、对象和方法。
不使用代理模式 |
使用代理模式后 |
|
|
(1) 静态代理
静态代理是指在编译期间就确定好代理类,代理类和目标类是强耦合的。这就使得代理类不具备通用性和灵活性。
package com.sheeprunner.aop.aband; /** * @description: * @author: RunningSheep * @date: 2024-12-05 23:48 * @version: V1.0 */ public class CalculatorStaticProxy implements Calculator{ // 将被代理的目标对象声明为成员变量 private Calculator target; public CalculatorStaticProxy(Calculator target) { this.target = target; } @Override public int add(int a, int b) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] add 方法开始了,参数是:" + a + "," + b); // 通过目标对象来实现核心业务逻辑 int addResult = target.add(a, b); System.out.println("[日志] add 方法结束了,结果是:" + addResult); return addResult; } @Override public int sub(int a, int b) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] sub 方法开始了,参数是:" + a + "," + b); // 通过目标对象来实现核心业务逻辑 int subResult = target.add(a, b); System.out.println("[日志] sub 方法结束了,结果是:" + subResult); return subResult; } @Override public int mul(int a, int b) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] mul 方法开始了,参数是:" + a + "," + b); int mulResult = target.mul(a, b); System.out.println("[日志] mul 方法结束了,结果是:" + mulResult); return mulResult; } @Override public int div(int a, int b) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] div 方法开始了,参数是:" + a + "," + b); int divResult = target.div(a, b); System.out.println("[日志] div 方法结束了,结果是:" + divResult); return divResult; } }
(2)动态代理
动态代理是指在运行期间,根据需要创建代理类,代理类和目标类是弱耦合的。
public class DynamicProxyFactory { private Object target; public DynamicProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { /** * newProxyInstance():创建一个代理实例 * 其中有三个参数: * 1、classLoader:加载动态生成的代理类的类加载器 * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组 * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法 */ ClassLoader classLoader = target.getClass().getClassLoader(); Class<?>[] interfaces = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /** * proxy:代理对象 * method:代理对象需要实现的方法,即其中需要重写的方法 * args:method所对应方法的参数 */ Object result = null; try { System.out.println("[DynamicProxy][log]" + method.getName() + ",参数:" + Arrays.toString(args)); result = method.invoke(target, args); System.out.println("[DynamicProxy][log]" + method.getName() + ",结果:" + result); } catch (Exception e) { e.printStackTrace(); System.out.println("[DynamicProxy][log]" + method.getName() + ",异常:" + e.getMessage()); } finally { System.out.println("[DynamicProxy][log]" + method.getName() + ",执行完毕!"); } return result; } }; return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler); } }
三、AOP
AOP(Aspect Oriented Programming),即面向切面编程,是一种编程思想,它把应用程序切分为模块,这些模块之间通过接口或公共的编程语言元素进行通信,例如方法调用。
AOP 的主要思想是把应用程序的各个模块进行解耦,把公共的功能抽取出来,形成新的模块,然后把各个模块通过接口或公共的编程语言元素进行通信,从而实现复用。
AOP是对面向对象的补充和完善,它通过引入新的概念和模式,为对象行为添加额外的功能,而无需修改源代码。
降低耦合,提供复用性,提高开发效率。
基本概念
(1)横切关注点
分散在各个模块中解决同一问题的代码,例如日志记录、事务管理、权限控制、数据缓存、用户校验等。
(2)通知(增强)
增强,就是我们要增强的功能,比如安全、事务、日志等。
每个横切关注点上要做的事情都要写一个方法来实现,这写方法叫通知方法。
- 前置通知:在被代理目标执行前执行
- 返回通知:在被代理目标执行结束后执行(“寿终正寝”)
- 异常通知:在被代理目标执行异常后执行(“死于非命”)
- 后置通知:在被代理目标执行最后结束后执行(“盖棺定论”)
- 环绕通知:使用try……catch……finally……结构围绕整改被代理的目标方法,包括上面四种通知所对应的位置
(3)切面
封装通知方法的类。
(4)目标
被代理的对象。
(5)代理
向目标对象应用通知之后创建的代理对象。
(6)连接点
逻辑概念:把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
通俗说,就是spring允许你使用通知的地方
(7)切入点
点位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
作用
- 简化代码:把方法中的重复代码抽取出来,让被抽取方法只关注自己的核心功能,提高内聚性。
- 代码增强:把特定的功能封装到切面类中,哪里有需要就往上套,被套用了切面逻辑的方法就被切面增强了。
四、基于注解的AOP
- 分为两种动态代理:JDK动态代理和CGLIB动态代理。
- 目标类有接口时JDK动态代理和cglib动态代理都适用,没有接口则只能使用cglib动态代理。
- JDK动态代理生成的代理类在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口。
- cglib生成的代理类会在目标类的同一个包下,并继承目标类。
- 动态代理(InvocationHandler):JDK原生实现,需要被代理的目标类必须实现接口,因为这个技术要求代理对象和目标对象实现同样接口(兄弟拜把子模式)。
- cglib通过继承目标类实现代理,故无需接口(任干爹模式)。
- AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类的编译后字节码文件中,所以最终效果是动态的。weaver就是织入器。Spring借用了AspectJ的注解,未使用这种实现。
(1)实现步骤
1)引入相关依赖:
<!--spring aop依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.39</version> </dependency> <!--spring aspects依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.39</version> </dependency>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.sheeprunner.aop.bannotation" /> <!-- 基于注解的AOP的实现: 1、将目标对象和切面交给IOC容器管理(注解+扫描) 2、开启AspectJ的自动代理,为目标对象自动生成代理 3、将切面类通过注解@Aspect标识 --> <aop:aspectj-autoproxy /> </beans>
2)创建目标接口、实现目标类:
同上Calculator
和CalculatorImpl
3)创建切面类
package com.sheeprunner.aop.bannotation; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @description: 实现日志切面 * @author: RunningSheep * @date: 2024-12-08 14:46 * @version: V1.0 */ // @Aspect表示这是一个切面类 @Aspect // @Component注解保证这个切面类能够放到IOC容器中 @Component public class LogAspect { public static final Logger logger = LoggerFactory.getLogger(LogAspect.class); @Before("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); String argstr = Arrays.toString(args); logger.info("LogAspect-前置通知,方法名:{},参数:{}", methodName, argstr); } @After("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") public void afterMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); logger.info("LogAspect-后置通知,方法名:{}", methodName); } @AfterReturning(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))",returning = "result") public void afterReturnMethod(JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); logger.info("LogAspect-返回通知,方法名:{},结果:{}", methodName, result); } @AfterThrowing(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))", throwing = "e") public void afterThrowingMethod(JoinPoint joinPoint, Throwable e) { String methodName = joinPoint.getSignature().getName(); logger.info("LogAspect-异常通知,方法名:{},异常信息:{}", methodName, e); } @Around("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { logger.info("环绕通知-->目标对象方法执行之前"); //目标对象(连接点)方法的执行 result = joinPoint.proceed(); logger.info("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); logger.info("环绕通知-->目标对象方法出现异常时"); } finally { logger.info("环绕通知-->目标对象方法执行完毕"); } return result; } }
4)运行结果
@Test public void test01() { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = context.getBean("calculatorImpl", Calculator.class); // Calculator calculator = context.getBean("intelligent", Calculator.class); // Calculator calculator = context.getBean(Calculator.class); calculator.add(2, 5); // calculator.div(1, 0); }
5)解释说明
各种通知
- 前置通知:使用@Before注解标识,在被代理的目标方法前执行
- 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
各种通知的执行顺序:
- Spring版本5.3.x以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
切入点表达式语法:
@Before("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
- 用*号代替“权限修饰符”和“返回值”部分 表示“权限修饰符”和“返回值”不限
- 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
- 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
- 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
- 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分
- 例如:*Service匹配所有名称以Service结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
- 例如:*Operation匹配所有方法名以Operation结尾的方法
- 在方法参数列表部分,使用(..)表示参数列表任意
- 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
- 例如:execution(public int ..Service.(.., int)) 正确
例如:execution( int *..Service.(.., int)) 错误
重用切入点表达式:
// 内部定义的切入点表达式 @Pointcut("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") public void beforePointCutInner() { } // 三种使用方法 // @Before("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") // @Before("beforePointCutInner()") @Before("com.sheeprunner.aop.bannotation.CalculatorBeforePointCut.beforePointCutOutter()") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); String argstr = Arrays.toString(args); logger.info("LogAspect-前置通知,方法名:{},参数:{}", methodName, argstr); } // 外部定义的切入点表达式 public class CalculatorBeforePointCut { @Pointcut("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") public void beforePointCutOutter() { } }
获取通知的相关信息
- 获取连接点信息
@Before("com.sheeprunner.aop.bannotation.CalculatorBeforePointCut.beforePointCutOutter()") public void beforeMethod(JoinPoint joinPoint) { // 获取连接点的签名信息,getName获取方法名 String methodName = joinPoint.getSignature().getName(); // 获取目标方法的实参信息 Object[] args = joinPoint.getArgs(); String argstr = Arrays.toString(args); logger.info("LogAspect-前置通知,方法名:{},参数:{}", methodName, argstr); }
- 获取目标方法返回值
returnning="result"
@AfterReturning(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))",returning = "result") public void afterReturnMethod(JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); logger.info("LogAspect-返回通知,方法名:{},结果:{}", methodName, result); }
- 获取目标方法异常
throwing="e"
@AfterThrowing(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))", throwing = "e") public void afterThrowingMethod(JoinPoint joinPoint, Throwable e) { String methodName = joinPoint.getSignature().getName(); logger.info("LogAspect-异常通知,方法名:{},异常信息:{}", methodName, e); }
- 环绕通知
@Around("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { logger.info("环绕通知-->目标对象方法执行之前"); //目标对象(连接点)方法的执行 result = joinPoint.proceed(); logger.info("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); logger.info("环绕通知-->目标对象方法出现异常时"); } finally { logger.info("环绕通知-->目标对象方法执行完毕"); } return result; }
切面的优先级
@Order:有一个参数,value为int类型,数字越小,优先级越高,默认为2147483647
在未修改原来的LogAspect.java情况下,增加了LogAspect02.java,Order设为2,如下;
@Aspect @Component @Order(2) public class LogAspect02 {}
结果如下:LogAspect02是包裹在LogAspect外侧的,既内外嵌套,如上图所示。
2024-12-09 23:12:53 020 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - 环绕通知02-->目标对象方法执行之前 2024-12-09 23:12:53 041 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - LogAspect02-前置通知,方法名:add,参数:[2, 5] 2024-12-09 23:12:53 042 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - 环绕通知-->目标对象方法执行之前 2024-12-09 23:12:53 042 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - LogAspect-前置通知,方法名:add,参数:[2, 5] [inner] a = 2, b = 5, a + b = 7 2024-12-09 23:12:53 042 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - LogAspect-返回通知,方法名:add,结果:7 2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - LogAspect-后置通知,方法名:add 2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - 环绕通知-->目标对象方法返回值之后 2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - 环绕通知-->目标对象方法执行完毕 2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - LogAspect02-返回通知,方法名:add,结果:7 2024-12-09 23:12:53 045 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - LogAspect02-后置通知,方法名:add 2024-12-09 23:12:53 045 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - 环绕通知02-->目标对象方法返回值之后 2024-12-09 23:12:53 045 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - 环绕通知02-->目标对象方法执行完毕
五、基于XML实现AOP
<!-- 基于XML的AOP实现: --> <bean id="calculatorCustomImpl" class="com.sheeprunner.aop.cxml.CalculatorCustomImpl" /> <bean id="logAspect03" class="com.sheeprunner.aop.cxml.LogAspect03" /> <aop:config> <!-- 配置切面类 --> <aop:aspect ref="logAspect03"> <aop:pointcut id="pointCut" expression="execution(* com.sheeprunner.aop.cxml.Calculator.*(..))"/> <aop:before method="beforeMethod" pointcut-ref="pointCut" /> <aop:after method="afterMethod" pointcut-ref="pointCut" /> <aop:after-returning method="afterReturnMethod" pointcut-ref="pointCut" returning="result" /> <aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointCut" throwing="e" /> <aop:around method="aroundMethod" pointcut-ref="pointCut" /> </aop:aspect> </aop:config>