[Spring] Spring AOP
🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. AOP概述
- 2. Spring AOP快速入门
- 2.1 引入AOP依赖
- 2.2 编写AOP程序
- 3. Spring AOP详解
- 3.1 核心概念
- 3.1.1 切点(PointCut)
- 3.1.2 连接点(Join Point)
- 3.1.3 通知(Advice)
- 3.1.4 切面(Aspect)
- 3.2 通知类型
- 3.3 @Pointcut
- 3.4 切面优先级@Order
- 3.5 切点表达式
- 3.5.1 execution表达式
- 3.5.2 @annotation(翻译:注解)
- 4. Spring AOP原理
- 4.1 代理模式
- 4.1.1 静态代理
- 4.1.2 动态代理
- 4.1.3 两种代理模式有什么区别?
- 4.1.4 Spring AOP什么时候会失效?
- 5. Spring AOP的常见应用场景
- 5.1 日志记录
- 5.2 事务管理
- 5.3 权限校验
- 5.4 性能监控
- 5.5 异常处理
1. AOP概述
- 什么是AOP?
所谓AOP,就是面相切面的编程.什么是面向切面的编程呢,切面就是指定某一类特定的问题,所以,面向切面的编程就是正对于同一类问题进行编程.
简单来说,就是针对某一类事情的集中处理.
- 什么是Spring AOP?
AOP是一种思想,实现AOP的方法有很多,有Spring AOP,有AspectJ,有CGLIB等.Spring AOP是其中的一种实现方式.
某种程度上,他和我们前面提到的统一功能处理的效果差不多,但是,统一功能处理并不等同于SpringAOP,拦截器的作用维度是URL,即一次请求响应,但是AOP的作用维度更加细致,可以对包括包,类,方法,参数等进行统一功能的处理,可以实现更加复杂的业务逻辑.
举例:
现在有一些业务执行效率比较低,我们需要对接口进行优化,第一步需要定位出耗时较长的业务方法,在针对业务额来进行优化.我们就可以在每一个方法的结束和开始的地方加上时间戳,之后作差展示出来.但是在一个项目中,我们有好多接口,此时在接口中一个个低添加时间戳又不是很现实,此时我们就需要用到AOP.
接下来,我们就来看看AOP如何使用.
2. Spring AOP快速入门
需求: 统计图书管理系统各个接口和方法的执行时间.
2.1 引入AOP依赖
在pom文件中添加依赖:
org.springframework.boot spring-boot-starter-aop
2.2 编写AOP程序
记录Controller中每个方法的执行时间.
package com.jrj.books.component; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect//表示切面 @Component @Slf4j public class TimeAspect { @Around("execution(* com.jrj.books.controller.*.*(..)))")//切点表达式 public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//参数是连接点 long begin = System.currentTimeMillis(); Object ret = proceedingJoinPoint.proceed();//执行连接点方法 long end = System.currentTimeMillis(); log.info("执行耗时:" + (end-begin)); return ret; } }
上述代码的测试结果如下:
我们看到了接口返回了执行耗时.
下面我们对上面的代码进行详细的解释:
-
@Aspect : 表示的是切面.表示这个类是一个切面类.
-
@Around: 环绕通知,在目标方法前后都会有代码执行.
-
proceedingJoinPoint.proceed(): 执行加入切面的连接点.
-
execution(* com.jrj.books.controller.*.*(..))):切点表达式,表示这个切面对该项目下的那个方法生效.
-
ProceedingJoinPoint proceedingJoinPoint: 加入切面的连接点.
3. Spring AOP详解
3.1 核心概念
3.1.1 切点(PointCut)
切点,也就是切入点,我们一般在通知类型注解的后面使用execution切点表达式来描述切点.切点就是告诉程序对那些方法的功能进行加强.
3.1.2 连接点(Join Point)
满足切点表达式规则的方法,就是连接点,也就是可以被该切面所作用的方法.连接点的数据类型是:ProceedingJoinPoint.也就是com.jrj.books.controller.包下的所有方法都是该切面的连接点.
切点与连接点之间的关系
连接点事满足切点表达式的元素,切点可以看做是一个保存了众多连接点的集合.
3.1.3 通知(Advice)
通知就是具体要做的工作,指在指定的方法中重复那些逻辑,也就是共性功能.
比如上面实现计算运行前后的时间差的业务逻辑就叫做通知.
3.1.4 切面(Aspect)
切面=切点+通知
切面既包含了逻辑的定义,也包含连接点的定义.
切面说在的类,我们一般称为切面类.
3.2 通知类型
SpringAOP中的通知类型有以下几种:
- @Around:环绕通知,表示该方法在目标方法前后都会被执行.
- @Before:前置通知,表示该方法只在目标方法之前被执行.
- @After:后置通知,表示该方法只在目标方法之后被执行,无论是否有异常发生.
- @AfterReturning:返回后通知,表示该方法只在目标方法之后被执行,方法返回了之后才会执行,有异常发生不会执行.
- @AfterThrowing:异常后通知.表示方法在发生异常之后执行.
代码演示:
@Slf4j @Component @Aspect public class AspectDemo { @Before("execution(* com.jrj.aspect.*.*(..))") public void before(){ log.info("before method"); } @After("execution(* com.jrj.aspect.*.*(..))") public void after(){ log.info("after method"); } @Around("execution(* com.jrj.aspect.*.*(..))") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before"); Object o = proceedingJoinPoint.proceed(); log.info("around after"); return o; } @AfterReturning("execution(* com.jrj.aspect.*.*(..))") public void afterReturning(){ log.info("afterReturning method"); } @AfterThrowing("execution(* com.jrj.aspect.*.*(..))") public void afterThrowing(){ log.info("afterThrowing method"); } }
[注意] 被@Around标志的方法必须有形式和返回值.其他注解标志的方法可以加上JoinPoint类型的形式参数,**用来获取原始方法的数据.
下面我们准备测试代码:其中一个正常执行,另外一个制造一些异常出来.
@RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } @RequestMapping("t2") public String t2(){ int a = 10/0; return ""; } }
下面我们来执行代码,观察后端日志:
- 正常执行的代码
在程序运行正常的情况下,@AfterThrowing注解的方法不会执行.
从上面的执行结果中,我们可以看出:@Around有前置逻辑和后置逻辑两部分,这两个逻辑再内层就是@Before和@After标识的方法,再往内层就是@AfterReturning标注的方法.
- 异常的情况
在异常发生的情况下,@Around标识的后置逻辑不会被执行.@AfterReturning标识的方法不会被执行.@AfterThrowing表示的方法会被执行.
[注意事项]
• @Around 环绕通知需要调用ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行.
• @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.
3.3 @Pointcut
上面的代码中存在一个问题,就是切点表达式大量重复,这时候,Spring提供了@Pointcut注解用来把公共的切点表达式提取出来.到时候直接使用方法名把它提取出来即可.
@Slf4j @Component @Aspect public class AspectDemo { @Pointcut("execution(* com.jrj.aspect.*.*(..))") public void pointCut(){} @Before("pointCut()") public void before(){ log.info("before method"); } @After("pointCut()") public void after(){ log.info("after method"); } @Around("pointCut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before"); Object o = proceedingJoinPoint.proceed(); log.info("around after"); return o; } @AfterReturning("pointCut()") public void afterReturning(){ log.info("afterReturning method"); } @AfterThrowing("pointCut()") public void afterThrowing(){ log.info("afterThrowing method"); } }
[注意] 当切点使用private修饰的时候,仅可以在该类中使用.当其他切面类也需要使用到该切点的时候,就需要把private改成public,并使用全限定方法名.
3.4 切面优先级@Order
当我们在一个项目中,定义了多个切面类,并且这些切面类的多个切点都匹配到了同一个目标方法,当目标方法运行的时候,这些切面类中的方法都会执行,那么这些方法执行的顺序是什么样的呢?我们通过代码来验证.
@RequestMapping("/test2") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } } @Slf4j @Component @Aspect public class AspectDemo1 { @Pointcut("execution(* com.jrj.aspect.order.*.*(..))") public void pt(){} @Before("pt()") public void before(){ log.info("AspectDemo1 before"); } @After("pt()") public void after(){ log.info("AspectDemo1 after"); } } @Slf4j @Component @Aspect public class AspectDemo2 { @Before("com.jrj.aspect.order.AspectDemo1.pt()") public void before(){ log.info("AspectDemo2 before"); } @After("com.jrj.aspect.order.AspectDemo1.pt()") public void after(){ log.info("AspectDemo2 after"); } } @Slf4j @Component @Aspect public class AspectDemo3 { @Before("com.jrj.aspect.order.AspectDemo1.pt()") public void before(){ log.info("AspectDemo3 before"); } @After("com.jrj.aspect.order.AspectDemo1.pt()") public void after(){ log.info("AspectDemo3 after"); } }
观察日志:
通过上述程序的运行结果,可以看出:
存在多个切面类对应一个方法的时候,默认按照切面类的字母序排序.Before和After成对称式分布.
- @Before:字母序靠前的先通知.
- @After:字母序靠后的先通知.
此外,我们还可以通过Spring的注解,来控制切面的执行顺序:@Order.
使用方式如下:
@Slf4j @Component @Aspect @Order(1) public class AspectDemo1 { ... } @Slf4j @Component @Aspect @Order(3) public class AspectDemo2 { ... } @Slf4j @Component @Aspect @Order(2) public class AspectDemo3 { ... }
观察日志:
我们可以得出以下结论:
- @Before:数字小的先执行
- @After:数字大的先执行
和上面一样,Before和After也是呈对称式分布.
3.5 切点表达式
上面的代码中,我们一直在使用切点表达式来描述切点,下面我们来介绍一下切点表达式.
切点表达式最常见的有以下两种方式:
- execution(...): 根据方法的签名来匹配.
- @annotation:根据注解匹配.
3.5.1 execution表达式
execution( )
其中访问限定符可以省略.
切点表达式支持通配符:
- *:匹配任何字符,只可以匹配一个元素,可以匹配返回类型,包名,类名,方法名,方法参数.
a. 包名使用* 表示任意包(⼀层包使用⼀个*)
b. 类名使用* 表示任意类
c. 返回值使用* 表示任意返回值类型
d. 方法名使用* 表示任意方法
e. 参数使用* 表示⼀个任意类型的参数
- ..:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数.
a. 使用 .. 配置包名,标识此包以及此包下的所有子包
b. 可以使用 .. 配置参数,任意个任意类型的参数
[注意] *只可以匹配一个元素,..可以匹配多个元素.
举例:
- 匹配TestController下的所有无参方法.
execution(* com.example.demo.controller.TestController.*())
- 匹配TestController下的所有方法
execution(* com.example.demo.controller.TestController.*(..))
- 匹配com.example.demo包下,子孙包下的所有类的所有方法
execution(* com.example.demo..*(..))
- 匹配所有包下面的TestController类的所有方法.
execution(* com..TestController.*(..))
3.5.2 @annotation(翻译:注解)
准备接口测试代码:
@RestController @RequestMapping("/user") public class UserController { @RequestMapping("/u1") public String user1(){ return "u1"; } @RequestMapping("/u2") public String user2(){ return "u2"; } } @RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } @RequestMapping("t2") public String t2(){ int a = 10/0; return ""; } }
如果我们要匹配多个无规则的方法呢?比如UserController中的user1方法和TestController中的t1方法.这时候使用execution显得有些麻烦.这时候我们就可以借助切点表达式的另一种方式.@annotation注解来描述一类切点.具体实现步骤如下:
- 编写自定义注解
- 使用@annotation表达式来描述切点.
- 在连接点的方法上添加自定义注解.
- 自定义注解
创建一个自定义注解类,创建的方式和创建类是一样的.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAspect {}
- @Target注解,代表的是注解可以修饰的对象,常用的有以下四种取值:
ElementType.TYPE:用于描述类、接口(包括注解类型)或enum声明
ElementType.METHOD:描述方法
ElementType.PARAMETER:描述参数
ElementType.TYPE_USE:可以标注任意类型
- @Retention注解,代表的是Annotation被保留的时间.表示的是该注解的生命周期.
RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃.
RetentionPolicy.CLASS:编译时注解.表示注解存在于源代码和字节码中,但在运行时会被丢弃.
RetentionPolicy.RUNTIME:运行时注解,表示注解存在于源代码,字节码和运行时中.
- 切面类
使用@annotation切点表达式定义切点,只对@MyAspect生效.
@Aspect @Slf4j @Component public class AspectDemo4 { @Before("@annotation(com.jrj.aspect.MyAspect)") public void before(){ log.info("before method"); } @After("@annotation(com.jrj.aspect.MyAspect))") public void after(){ log.info("after method"); } }
- 为指定方法添加自定义注解,在加上自定义注解之后,在调用接口的时候就会执行切面中的方法.
在测试代码的方法下面加上自定义注解:
@RestController @RequestMapping("/test") public class TestController { @MyAspect @RequestMapping("/t1") public String t1(){ ... } @MyAspect @RequestMapping("t2") public String t2(){ ... } } @RestController @RequestMapping("/user") public class UserController { @MyAspect @RequestMapping("/u1") public String user1(){ ... } @MyAspect @RequestMapping("/u2") public String user2(){ ... } }
观察日志:
我们发现测试的接口打印出了制定的日志.
4. Spring AOP原理
SpringAOP使用的是动态代理模式来实现的.他所涉及的设计模式是代理模式.
4.1 代理模式
代理模式,也叫委托模式
为其他对象提供⼀种代理以控制对这个对象的访问.它的作用就是通过提供⼀个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用.
举例说明代理模式:
房屋中介与房东,艺人与经纪人,老板与秘书.
代理模式的主要角色:
- Subject:业务接口类.这里定义的是代理和被代理对象要做的事情.可以抽象类或者接口(不一定有).
- RealSubject:业务实现类.具体的业务执行,也就是被代理的对象.
- Proxy:代理类.RealSubject的代理.
根据代理创建的时期,可以把代理模式分为动态代理和静态代理.
代理模式的优点:
- 可以提供权限控制,可以控制对真实对象的访问权限,以保证真实对象的安全性.
- 代理可以对真实的对象做一些功能上的增强,在真实对象的前后增加一些额外的逻辑.
- 把被代理对象与外界和这个被代理对象交互的对象进行了解耦操作.
就像我们想要对外卖方租房,我们可以找中介代理
- 首先房产中介可以防止我们遇到诈骗.
- 房产中介也可以为房东提供一些额外的服务.
- 房产中介可以对接不同的人,包括房东可能不会接受的人.
4.1.1 静态代理
静态代理:在程序运行前,代理类的.class文件就已经存在了,代理类和被代理类就一定绑定好了固定的关系.(在出租房子之前,中介和房东已经做好了相关的工作就等租户来租房子了)
- 接口定义(定义房东和中介要做的事情)
public interface HouseSubject { void rentHouse(); }
- 实现接口(房东出租房子)
public class RealHouseProxy implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } }
- 代理(中介,帮房东出租房子)
public class Proxy implements HouseSubject{ private RealHouseProxy realHouseProxy; public Proxy(RealHouseProxy realHouseProxy) { this.realHouseProxy = realHouseProxy; } @Override public void rentHouse() { System.out.println("我要代理出租房子"); realHouseProxy.rentHouse(); System.out.println("代理完成"); } }
- 使用
public static void main(String[] args) { RealHouseProxy realHouseProxy1 = new RealHouseProxy(); Proxy proxy = new Proxy(realHouseProxy1); proxy.rentHouse(); }
运行结果:
虽然静态代理也完成了代理,但是由于代码是写死的,对目标方法的增强都是手动来完成的,非常不灵活,所以我们有了动态代理.
4.1.2 动态代理
我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个代理对象的工作推迟到程序运行的时候由JVM来实现,也就是在程序运行的时候,根据需要动态创建代理.
常见的动态代理实现模式有两种方式:
- jdk动态代理
实现步骤如下:
- 首先定义一个接口及其实现类(相当于房东).
- 之后自定义一个类,实现InvocationHandler接口并重写invoke方法,在invoke方法中调用目标方法(被代理类的方法)并自定义一些处理逻辑,创建代理类的时候,代理类主要是通过实现相同的接口,并在运行时生成的代理对象.生成代理对象的时候,主要是通过调用反射API(反射机制)来生成的.
- 通过Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)方法创建代理对象.生成的代理类会实现接口,之后这些方法都会被路由到InvocationHandler.invoke()方法中.
- 在主方法中通过Proxy.newProxyInstance创建动态代理类的时候,需要传入三个参数,分别是被代理类接口的类加载器,被代理类接口的列表,方法调用处理器.这里使用的是反射的机制来传入,通过反射的方式来获得到代理类的类加载器和代理类的接口
- 使用invoke()方法来调用被代理类的方法方法,并传入被代理对象和args(数组中存放的是方法的参数)参数.这里的Invoke方法也是通过反射的方式来拿到被代理对象方法的信息的(target.getClass().getMethod()方法).
public interface HouseSubject { void rentHouse(); } public class RealHouseProxy implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } public class JDKInvocationHandler implements InvocationHandler { private Object object;//由于动态代理的代理对象不是固定的,所以这里使用Object来表示被代理的对象 public JDKInvocationHandler(Object o) { this.object = o; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是代理,开始代理"); Object object = method.invoke(this.object,args);//表示执行被代理对象中的方法 //其中invoke就代表的是执行类中的方法, //invoke中第一个参数表示的是被代理类,第二个参数代表的是执行被代理类的方法的时候需要传入的参数 System.out.println("代理结束"); return object; } public static void main(String[] args) { HouseSubject houseSubject = new RealHouseProxy(); HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(houseSubject.getClass().getClassLoader(), new Class[]{HouseSubject.class},new JDKInvocationHandler(houseSubject)); //与静态代理不同的是,静态代理是在JVM运行之前就已经写好了,但是动态代理是在JVM运行的时候动态创建了一个代理 proxy.rentHouse(); } }
[注意事项] 1. 在动态代理类中,**由于代理的对象可以是任何类型的**,所以被代理对象应该是Object类型的. 2. 在使用newInstance方法创建代理对象的时候,第一个传入的是被代理对象的加载器,第二个传入的是一个class类的数组,数组中放着被代理对象接口的class,最后一个放的是我们提前设定好的代理类的对象,这里使用的是反射的原理.
- CGLIB动态代理
jdk代理致命的问题就是他只可以代理接口(实现一个接口的类),但是CGLIB动态代理既可以代理实现接口的,又可以代理没有实现接口的,如果代理的是实现接口的类,那么机制和JDK是一样的,如果不是,那么主要是通过创建目标方法的子类和重写目标方法中的方法来创建代理对象的.
CGLIB动态代理类实现步骤:
- 引入CGLIB依赖
- 定义一个被代理类
- 自定义一个代理类,并实现MethodInterceptor 接口,实现intercept方法,Intercept方法用于调用被代理类的方法,方法和jdk代理中的invoke类似,如果被代理类实现了接口的话,那么就是通过反射来生成代理类,通过反射调用了目标方法,如果没有实现接口的话,就是通过直接继承的方式来生成代理,直接调用子类中重写的方法来调用目标方法.
- 通过Enhancer类的create()方法创建代理对象.在其中传入接口的class和之前写好的自定义CGLIB代理类的对象.这里也同样用到了反射的机制来获取到接口的信息和动态代理的信息.
cglib cglib 3.3.0
public interface HouseSubject { void rentHouse(); } public class RealHouseSubject implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } public class CGLIBInterceptor implements MethodInterceptor { private Object object; public CGLIBInterceptor(Object object) { this.object = object; } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("我是代理,开始代理"); Object o1 = methodProxy.invoke(object,objects); System.out.println("代理结束"); return o1; } } public class Main { public static void main(String[] args) { HouseSubject houseSubject = new RealHouseSubject(); HouseSubject proxy = (HouseSubject) Enhancer.create(HouseSubject.class,new CGLIBInterceptor(houseSubject)); proxy.rentHouse(); } }
4.1.3 两种代理模式有什么区别?
- 代理生成的方式不同: JDK动态代理是通过实现目标类的接口来生成代理对象的.而CGLIB是通过生成目标类的子类来生成代理对象的.
- 使用场景上: 如果目标类有接口,JDK动态代理会优先使用,CGLIB可以用但是没必要,如果目标类没有实现接口,JDK动态代理无法使用,必须使用CGLIB动态代理,SpringAOP在目标类中有实现接口的时候,默认会选择JDK动态代理,目标类无接口的时候,默认会选择CGLIB动态代理.
- 性能对比上: JDK动态代理生成代理的速度比较快,方法调用速度比较慢,内存占用比较低,而CGLIB动态代理代理生成速度比较慢,方法调用速度比较快,内存占用比较高.
在生成代理方面,之所以这样是因为JDK直接生成了接口代理类,CGLIB需要操作字节码生成子类,在方法调用速度上,之所以这样是因为JDK动态代理通过反射调用,而CGLIB动态代理直接调用重写的方法,内存占用上,之所以这样是因为CGLIB动态代理生成了子类.
- 方法拦截上,JDK主要是通过InvocationHandler.invoke()方法来拦截的,CGLIB动态代理主要是通过MethodInterceptor.intercept()方法来拦截的.
- 可扩展性上,JDK动态代理的扩展性较差,而CGLIB动态代理的可扩展性较高,这是由于JDK动态代理只能实现接口实现的类,CGLIB则都可以代理.
扩展性维度 JDK动态代理 CGLIB动态代理 接口变更适应性 ❌ 差(需同步修改接口和实现) ✅ 好(直接修改类即可) 新增方法支持 ❌ 必须通过接口扩展 ✅ 类中直接添加方法即可 对设计约束的要求 ✅ 严格(强制面向接口) ❌ 宽松(可能导致代码随意性) 技术兼容性 ✅ 所有Java环境支持 ⚠️ 需CGLIB/ASM(部分环境需适配) 4.1.4 Spring AOP什么时候会失效?
SpringAOP失效的原因要基于他的实现原理-----代理模式来解答.
- 内部方法调用: 由于代理的时候代理的是类,如果直接从类的内部进行方法调用的话会绕开中间代理.导致AOP失效
@Service public class OrderService { public void placeOrder() { validateOrder(); // 内部调用,AOP不会生效 } @Transactional public void validateOrder() { // 验证逻辑 } }
- private修饰的方法: AOP是基于代理模式实现的,如果是private修饰,如果是JDK动态代理代理的是接口,根本不考虑private的情况,如果是CGLIB动态代理,则子类无法重写private的方法.
@Aspect @Component public class MyAspect { @Before("execution(* com.example.*.*(..))") public void beforeAdvice() { System.out.println("Before method execution"); } } @Service public class MyService { void nonPublicMethod() { // 非public方法,AOP不会生效 // ... } }
- final修饰的方法或者是类: 如果是final修饰的类,无法被代理类继承,如果是final修饰的方法,则无法被代理类重写.所以两者都无法代理,和上面的原理类似.
@Service public final class FinalService { // final类无法被代理 @Transactional public final void finalMethod() { // final方法无法被代理 // ... } }
- static修饰的方法:代理对象无法覆盖静态方法,因为静态方法不可以被重写,静态方法属于类调用是编译绑定,而动态代理是运行时增强,无法干预静态方法的调用。
@Service public class UtilityService { @Cacheable("data") public static String getStaticData() { // 静态方法无法被代理 return "static data"; } }
- 低优先级切面被高级优先级切面排除:如果多个切面匹配同一个连接点,order比较靠前的切面可能异常或者是没有继续执行,导致了优先级较低的切面实失效。
5. Spring AOP的常见应用场景
5.1 日志记录
在方法执行前后记录日志,以便与对接口的监控和调试,避免了在主逻辑中通过硬编码的方式来完成日志的打印.
@Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("方法执行前: " + joinPoint.getSignature().getName()); } @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result") public void logAfterReturning(Object result) { System.out.println("方法返回结果: " + result); } }
5.2 事务管理
在Spring的事务中,即内置的@Transactional注解,底层的实现原理就包含AOP.通过AOP动态代理拦截了@Transactional方法,之后由TransactionManager控制事务的开启提交和回滚.
@Service public class UserService { @Transactional public void createUser(User user) { // 数据库操作 } }
5.3 权限校验
在方法调用之前检查用户的权限
@Aspect @Component public class SecurityAspect { @Before("@annotation(RequiresAdmin)") public void checkAdminRole(JoinPoint joinPoint) { if (!CurrentUser.isAdmin()) { throw new SecurityException("无权限访问!"); } } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiresAdmin {} @RequiresAdmin public void deleteUser(Long userId) { ... }
5.4 性能监控
通过统计某个方法的执行耗时来判断这个接口的性能,以便后面对接口进行性能优化
@Aspect @Component public class PerformanceAspect { @Around("execution(* com.example.service.*.*(..))") public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - start; System.out.println("方法执行耗时: " + duration + "ms"); return result; } }
5.5 异常处理
通过@AfterThrowing统一捕获异常,并返回错误信息:
@Aspect @Component public class ExceptionAspect { @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex") public void handleException(Throwable ex) { if (ex instanceof BusinessException) { // 发送告警邮件 EmailSender.send("admin@example.com", "业务异常: " + ex.getMessage()); } } }
- 低优先级切面被高级优先级切面排除:如果多个切面匹配同一个连接点,order比较靠前的切面可能异常或者是没有继续执行,导致了优先级较低的切面实失效。
- static修饰的方法:代理对象无法覆盖静态方法,因为静态方法不可以被重写,静态方法属于类调用是编译绑定,而动态代理是运行时增强,无法干预静态方法的调用。
- final修饰的方法或者是类: 如果是final修饰的类,无法被代理类继承,如果是final修饰的方法,则无法被代理类重写.所以两者都无法代理,和上面的原理类似.
- private修饰的方法: AOP是基于代理模式实现的,如果是private修饰,如果是JDK动态代理代理的是接口,根本不考虑private的情况,如果是CGLIB动态代理,则子类无法重写private的方法.
- 为指定方法添加自定义注解,在加上自定义注解之后,在调用接口的时候就会执行切面中的方法.
- @Target注解,代表的是注解可以修饰的对象,常用的有以下四种取值:
- 匹配所有包下面的TestController类的所有方法.
- 匹配com.example.demo包下,子孙包下的所有类的所有方法
- 匹配TestController下的所有方法
- 正常执行的代码
-
- 什么是AOP?