0


Java程序员必会的Spring AOP在实际项目中的应用

很久没有用过Java的AOP,最近接触到了一个需求,恰好可以用AOP的思想来实现,就此总结一下。

AOP简介

AOP,即Aspect Oriented Programming,直接翻译过来的意思是“面向侧面的程序设计”,更常见的说法是“面向切面编程”,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。

关于AOP的概念这里不做过多介绍,我们只需要知道其中有三个非常重要的概念:

aspect(切面)、pointcut(切入点)、advice(通知)。

我们分别来看看这三者是什么意思。

pointcut(切入点)

也就是具体拦截的某个业务点。

分为 execution(路径表达式)和 annotation 方式,被这两种方式修饰的代码将会被切面拦截处理。

切点和连接点有什么区别?

  1. Joint point

:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。

advice(通知)

切面当中的处理方式,声明通知方法在业务层的执行位置,类型如下:

@Before:前置通知,在方法执行之前执行
@After:后置通知,在方法执行之后执行
@AfterRunning:返回通知,在方法返回结果后执行
@AfterThrowing:异常通知,在方法执行异常后执行
@Around:环绕通知,可以替代上述通知方法,并且可以控制方法是否执行以及何时执行

Spring AOP中的通知方法,用到了动态代理,动态代理主要有两种方式:JDK动态代理和CGLIB动态代理,关于动态代理的详细说明,可以参考这里,以及实现原理。

aspect(切面)

即 pointcut+ advice ,和拦截器(HandlerInterceptorAdapter)的作用并没有太大的区别,只是两者的作用域不同,相对而言,切面的作用域更灵活一些。两者之间的区别可以参考这里。

AOP作用

如果说MVC的三层架构是Java的纵向编程思想,那AOP可以说是横向编程思想,能够让我们在不影响软件原有功能上,横向拓展新功能。


AOP组成

常见的用法有:

  • 性能监控:在方法调用前后记录调用时间,方法执行太长或超时报警
  • 缓存代理:缓存某方法的返回值,下次执行该方法时,直接从缓存里获取
  • 软件破解:使用AOP修改软件的验证类的判断逻辑
  • 记录日志:在方法执行前后记录系统日志
  • 工作流系统:工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务
  • 权限验证:方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉

AOP的实际应用

说了那么多概念,下面让我们来看一个真实的应用场景:

为了更好地追踪项目中每个接口在执行时的状态,现在要求在原有接口基础上进行操作日志的记录,日志内容包括包括执行时间,执行人等。

有两种实现方式;

① 写一个公共方法,每个接口里都调用该方法,优点是易于实现,缺点是高耦合;

② 使用AOP框架,在不影响原有接口的基础上,进行日志记录,低耦合;

我们使用第二种方法,来实现该功能:

一、使用自定义注解,定义pointCut

我们在项目中自定义一个Log注解,并在Log注解中定义一个属性userName,作为切点:

  1. package com.czq.aop.log;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. @Retention(RetentionPolicy.RUNTIME)
  5. public @interface Log {
  6. String userName() default "";
  7. }

并且在项目中定义一个操作日志类:

  1. package com.czq.aop.bean;
  2. import com.fasterxml.jackson.annotation.JsonProperty;
  3. import lombok.Data;
  4. import lombok.ToString;
  5. import java.util.Date;
  6. /**
  7. * 操作日志记录表 oper_log
  8. *
  9. * @author czq
  10. */
  11. @Data
  12. @ToString(onlyExplicitlyIncluded = true)
  13. public class SysOperLog
  14. {
  15. private static final long serialVersionUID = 1L;
  16. /** 操作人员 */
  17. @JsonProperty("操作人员")
  18. @ToString.Include
  19. private String operator;
  20. /** 操作地点 */
  21. @JsonProperty("操作地点")
  22. @ToString.Include
  23. private String operLocation;
  24. /** 请求参数 */
  25. @JsonProperty("请求参数")
  26. @ToString.Include
  27. private String operParam;
  28. /** 返回参数 */
  29. @JsonProperty("返回参数")
  30. @ToString.Include
  31. private String jsonResult;
  32. /** 错误消息 */
  33. @JsonProperty("错误消息")
  34. @ToString.Include
  35. private String errorMsg;
  36. /** 操作时间 */
  37. @JsonProperty("操作时间")
  38. @ToString.Include
  39. private Date operTime;
  40. }

二、实现advice(通知),用切面类来拦截处理被注解的方法并获取注解中的内容

  1. package com.czq.aop.aspect.log;
  2. import com.czq.aop.bean.SysOperLog;
  3. import com.czq.aop.log.Log;
  4. import com.fasterxml.jackson.core.JsonProcessingException;
  5. import com.fasterxml.jackson.databind.ObjectMapper;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.aspectj.lang.ProceedingJoinPoint;
  8. import org.aspectj.lang.annotation.Around;
  9. import org.aspectj.lang.annotation.Aspect;
  10. import org.aspectj.lang.annotation.Pointcut;
  11. import org.aspectj.lang.reflect.MethodSignature;
  12. import org.springframework.stereotype.Component;
  13. import org.springframework.util.CollectionUtils;
  14. import java.lang.reflect.Method;
  15. import java.util.Arrays;
  16. import java.util.Date;
  17. import java.util.List;
  18. import java.util.Map;
  19. import java.util.Objects;
  20. import java.util.stream.Collectors;
  21. /**
  22. * @Author: czq
  23. * @CreateTime: 2022-10-12 16:25
  24. * @Description: 写得不好,瞎写
  25. */
  26. @Aspect
  27. @Component
  28. @Slf4j
  29. public class LogAspect {
  30. /*
  31. * 切点,也可以不写切点,直接将自定义注解注释在通知方法的注解里
  32. * */
  33. @Pointcut("@annotation(com.czq.aop.log.Log)")
  34. public void pointCut() {
  35. }
  36. /*
  37. * 如果不写切点,直接在通知方法的注解里写自定义注解,也可以获取到自定义注解
  38. *
  39. * 写法如下:
  40. * @Around("@annotation(sysLog)")
  41. public Object execute(ProceedingJoinPoint joinPoint,Log sysLog) throws JsonProcessingException {
  42. //以如下方式获取自定义注解的值
  43. sysOperLog.setOperator(sysLog.userName());
  44. }
  45. *
  46. * */
  47. @Around("pointCut()")
  48. public Object execute(ProceedingJoinPoint joinPoint) throws JsonProcessingException {
  49. SysOperLog sysOperLog = getOperLog(joinPoint);
  50. try {
  51. // 执行完成后再打印日志
  52. Object proceed = joinPoint.proceed();
  53. sysOperLog.setJsonResult(proceed.toString());
  54. log.info(new ObjectMapper().writeValueAsString(sysOperLog));
  55. return proceed;
  56. } catch (Throwable e) {
  57. sysOperLog.setErrorMsg(e.getMessage());
  58. if (e instanceof NullPointerException) {
  59. sysOperLog.setErrorMsg("java.lang.NullPointerException");
  60. }
  61. // 执行异常时打印日志
  62. log.error(new ObjectMapper().writeValueAsString(sysOperLog));
  63. throw new RuntimeException(e);
  64. }
  65. }
  66. private SysOperLog getOperLog(ProceedingJoinPoint joinPoint) {
  67. SysOperLog sysOperLog = new SysOperLog();
  68. sysOperLog.setOperTime(new Date());
  69. // just demo to get current user and os
  70. Map<String, String> map = System.getenv();
  71. // 获取注解中的用户名
  72. // 如果直接将自定义注解写在通知方法里,则获取自定义注解的值如下
  73. // sysOperLog.setOperator(sysLog.userName());
  74. MethodSignature sign = (MethodSignature)joinPoint.getSignature();
  75. Method method = sign.getMethod();
  76. Log annotation = method.getAnnotation(Log.class);
  77. sysOperLog.setOperator(annotation.userName());
  78. // 获取操作系统名
  79. String os = map.get("OS");
  80. sysOperLog.setOperLocation(os);
  81. // 获取请求参数
  82. Object[] args = joinPoint.getArgs();
  83. List<Object> collect = Arrays.stream(args).filter(Objects::nonNull).collect(Collectors.toList());
  84. if (!CollectionUtils.isEmpty(collect)) {
  85. sysOperLog.setOperParam(Arrays.toString(args));
  86. }
  87. return sysOperLog;
  88. }
  89. }

三、在原有controller方法上加上注解:

  1. package com.czq.aop.controller;
  2. import com.czq.aop.log.Log;
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. /**
  6. * @Author: czq
  7. * @CreateTime: 2022-10-12 11:21
  8. * @Description: 测试用
  9. */
  10. @RestController
  11. public class AopController {
  12. @RequestMapping("/test")
  13. @Log(userName = "czq")
  14. public String test( Long id) {
  15. return id.toString();
  16. }
  17. }

四、请求该方法,查看操作日志:

① 请求正常时:

  1. GET http://localhost:8080/test?id=110

查看请求结果:

② 请求出错时:

  1. GET http://localhost:8080/test?110

查看请求结果:

参考文章

Java:由浅入深揭开 AOP 实现原理 - 知乎概述:最近在开发中遇到了一个刚好可以用AOP实现的例子,就顺便研究了AOP的实现原理,把学习到的东西进行一个总结。文章中用到的编程语言为kotlin,需要的可以在IDEA中直接转为java。 这篇文章将会按照如下目录展…https://zhuanlan.zhihu.com/p/93135819

CGLIB(Code Generation Library)详解_danchu的博客-CSDN博客什么是CGLIBCGLIB是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。Hibernate作为一个比较受欢迎的ORM框架,同样使用CGLIB来代理单端(多对一和一对一)关联(延迟提取集合使用的另一种机制)。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib为什么https://blog.csdn.net/danchu/article/details/70238002

Java AOP的底层实现原理 - 健人雄 - 博客园Java AOP的底层实现原理 一、什么是AOP 1、AOP:Aspect Oriented Programming(面向切面编程),OOP是面向对象编程,AOP是在OOP基础之上的一种更高级的设计思https://www.cnblogs.com/tian874540961/p/10812124.html

标签: java spring aop

本文转载自: https://blog.csdn.net/eclipse1024/article/details/127266980
版权归原作者 codu4u1314 所有, 如有侵权,请联系我们删除。

“Java程序员必会的Spring AOP在实际项目中的应用”的评论:

还没有评论