三. Advice 通知演示
刚刚已经演示过了 @Before 前置通知, 就不在赘述了
1. @After 注解
利用 @After 后置通知实现一个统一的日志处理功能
- 建立切面
@Aspect @Component public class LogAOP { }
- 创建切点
@Aspect @Component public class LogAOP { // 创建切点 @Pointcut("execution(* com.example.demo.controller.LogController.* (..))") public void doPointcut() { // 切点只是为了配置规则, 并非具体实现. 因此为空方法 } }
- 创建后置通知
@Aspect @Component public class LogAOP { // 创建切点 @Pointcut("execution(* com.example.demo.controller.LogController.* (..))") public void doPointcut() { // 切点只是为了配置规则, 并非具体实现. 因此为空方法 } // 创建后置通知 @After("doPointcut()") public void doAfter() { System.out.println("记录日志结束 "); } }
- 建立连接点
@RestController public class LogController { // 创建日志对象 public static final Logger log = LoggerFactory.getLogger(LogController.class); @RequestMapping("/user/log") public String longLog() { System.out.println("执行登陆功能"); log.info("记录操作日志 : 用户成功登陆"); return "Spring AOP"; } }
访问路由方法模拟执行用户登陆
可以看到, 当执行登陆后, 记录下操作日志, 此时该连接点方法执行结束, 执行后置通知 " 日志记录结束 "
2. @AfterReturning 注解
还是刚刚的登陆操作, 当执行的是 @AfterReturning 返回通知时, 预期在登陆操作结束后执行
- 建立切面
@Aspect @Component public class UserAOP { }
- 创建切点
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut() { // 切点只是为了配置规则, 并非具体实现. 因此为空方法 }
- 建立返回通知
@AfterReturning("doPointcut()") public void doAfterReturning() { System.out.println("执行返回通知 "); }
- 创建连接点
@RequestMapping("user/login") public String login() { System.out.println("执行登陆操作 "); return "Spring AOP"; }
执行登陆方法
3. @AfterThrowing 注解
异常通知, 当执行匹配的连接点的方法遇到异常结束后返回通知
还是刚刚的切面里, 同样的切点, 建立异常通知, 执行登陆方法
@AfterThrowing("doPointcut()") public void doAfterThrowing() { System.out.println("执行异常通知 "); }
可以看到抛异常了, 并且控制台也在异常之后打印了异常通知
4. @Around 注解
环绕通知, 被通知的方法本身被通知包裹着, 也就是执行环绕通知在被通知的方法执行之前会发一次通知, 在被执行方法执行结束后也会执行一次通知.
还是在刚刚的切面和切点, 建立环绕通知
// 添加事件本身 // ProceedingJoinPoint 表示正在执行的方法或表达式的连接点 // 配置环绕通知 @Around("doPointcut()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("开始执行环绕通知" ); long startTime = System.currentTimeMillis(); // 因为是将方法包围起来执行, 因此只能在方法里调用本身 Object object = joinPoint.proceed(); // 调用连接点的方法 System.out.println("结束执行环绕通知" ); long endTime = System.currentTimeMillis(); System.out.println("时间差为 : " + (endTime - startTime)); return object; // 计算时间差后并获取秒数毫秒级 }
可以看到, 环绕通知里面有 joinPoint, 前面说到它是连接点的意思. 这里不难明白, 切面需要通过连接点来确定何时调用环绕通知和如何调用它们. 因此这里传入的必须是连接点. 而前面的前置、后置通知等切面只需要在正确匹配的连接点之前或者之后通知就可以了.
建立连接点
@RequestMapping("/user/count") public String fun1() { System.out.println("执行了 count 方法"); int count = 0; for(int i = 0; i < 1000000000; i++) { count++; } return "统计方法"; }
通过环绕通知, 我们就可以统计某个方法的具体执行时间了, 从而作为该方法是否需要优化的重要依据之一.
5. 环绕和前置后置通知同时执行
你可能发现了, 环绕通知其实就是一个前置通知搭配一个后置通知. 那么, 既然前置后置都有了, 为什么还需要环绕通知呢 ? 他们放在一起执行会报错呢 ? 还是有什么联系呢 ?
public class UserAOP { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void doPointcut() { // 切点只是为了配置规则, 并非具体实现. 因此为空方法 } // 配置前置通知 @Before("pointcut()") // 里面填写针对那个切点的通知, 可以有多个切点 public void doBefore() { System.out.println("执行 before 前置通知 : 登陆检验" ); } @After("doPointcut()") public void doAfter() { System.out.println("执行 After 后置通知 : 登陆检验结束"); } // 添加事件本身 // ProceedingJoinPoint 表示正在执行的方法或表达式的连接点 // 配置环绕通知 @Around("doPointcut()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("开始执行环绕通知" ); long startTime = System.currentTimeMillis(); // 因为是将方法包围起来执行, 因此只能在方法里调用本身 Object object = joinPoint.proceed(); // 调用连接点的方法 System.out.println("结束执行环绕通知" ); long endTime = System.currentTimeMillis(); System.out.println("时间差为 : " + (endTime - startTime)); return object; // 计算时间差后并获取秒数毫秒级 } }
可以看到, 前置和后置通知总是在之间环绕通知的. 这是为什么 ?
由于环绕通知需要在目标方法执行之前和之后分别执行, 以便正确的控制目标方法的执行
具体来说, 如果放在前置和后置执行之间, 那么当目标方法执行时, 环绕通知也会被调用. 通俗一点理解就是执行方法时的前置通知时间是极短的, 如果在这执行前置通知还需要执行环绕通知有可能会导致重复执行, 从而导致代码重复和性能损失.
6. 总结
Spring AOP 除了上面的这几样简单的统一功能处理外, 还有很多功能可以实现, 只要它符合统一集中处理的思想, 也就是符合 AOP 的思想. 就可以用 Spring AOP 来实现.
四. Spring AOP 原理分析
Spring AOP 倒地是怎么执行的呢 ? 它为什么就知道那些连接点是我们匹配的, 那些是我们不需要的呢 ?
Spring AOP 是构建在动态代理基础上的. 因此 Spring 对 AOP 的支持局限于方法级别的拦截.
我们上面所写的 Spring AOP 它都是原生的. 而在 JDK 中早已替我们封装了
- JDK 动态代理
通过反射的机制, 在运行的时候生成一个代理对象, 并将所有的方法调用转发给代理对象. 代理对象实现了 InvocationHandler 接口,并重写了 invoke() 方法, 该方法会在代理对象被调用的时候执行.
- CGLIB 动态代理
和 JDK 动态代理类似, 也是通过代理对象来实现方法调用的转发. 但 CGLIB 动态代理比 JDK 动态代理更加的灵活, 因为它可以处理继承关系中私有的方法中的私有方法.
无论是那种方式, 在 Spring AOP 都可以使用动态代理的方式来实现切面逻辑