Spring-AOP

AOP概念

AOP(面向切面编程):能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块间的耦合度。Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。

实际开发场景

通用日志审计

需求分析

在企业级后端开发中,需要对所有对外提供的API接口进行监控和审计。如果不在AOP中统一处理,就需要在每一个方法中重复写log.info("request start...") 和 log.info("request end...") 。

考虑到这样一个需求,所以编写了RequestLogAspect这样一个类,其核心目的如下:

  • 全链路审计 :记录谁(User)、什么时间(Time)、调了什么接口(URL)、传了什么参数(Args)、返回了什么结果(Response)。
  • 性能监控 :计算每个接口的 执行耗时 (Spend Time),帮助定位慢接口。
  • 代码解耦 :将"写日志"这个非业务逻辑从 Controller 中剥离出来,保持业务代码的纯净。

代码分析

java 复制代码
/**
 * 对controller层的所有public方法进行日志记录
 * 切面编程
 */
//切面注解
@Aspect
@Component
public class RequestLogAspect {

    // 保存每次请求的参数在其线程中
     static final ThreadLocal<String> REQUEST_PARAMS = new ThreadLocal<>();
    protected Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 开始时钟
     * jdk的本地线程类
     */
    private ThreadLocal<Long> startNanoTime = new ThreadLocal<>();
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 拦截请求方法
     * execution 配置拦截路径
     */
    @Pointcut(value = "execution(public * com.*.*.controller..*.*(..))")
    public void requestLog() {

    }

    /**
     * 请求拦截之前处理方法
     * 方法请求前执行
     */
    @Before(value = "requestLog()")
    public void doBefore(JoinPoint joinPoint) {
        long start = System.nanoTime();
        this.startNanoTime.set(start);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }
        //tomcat中servlet的http
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        StringBuilder sb = new StringBuilder();
        sb.append(Constants.LOG_TAG).append("URL: ").append(request.getRequestURL().toString());
        sb.append(Constants.LOG_TAG).append("URL: ").append(request.getRequestURI().toString());
        sb.append(Constants.LOG_TAG).append("request method: ").append(request.getMethod());
        sb.append(Constants.LOG_TAG).append("request userId: ").append(request.getAttribute("userId"));
        sb.append(Constants.LOG_TAG).append("remote address: ").append(request.getRemoteAddr());
        sb.append(Constants.LOG_TAG).append("remote port: ").append(request.getRemotePort());
        sb.append(Constants.LOG_TAG).append("contentType: ").append(request.getContentType());

        Signature signature = joinPoint.getSignature();
        sb.append(Constants.LOG_TAG).append("CLASS_METHOD: ").append(signature.getDeclaringTypeName()).append(".").append(signature.getName());
        Object[] args = joinPoint.getArgs();
        String params = Arrays.toString(args);
        for (Object arg : args) {
            if (arg instanceof HttpServletRequest) {
                StringBuilder buf = new StringBuilder();
                request.getParameterMap().forEach((k, v) -> {
                    buf.append(k).append("=").append(Arrays.toString(v)).append(", ");
                });
                params += ", getParameterMap: " + buf.toString();
                break;
            }
        }
        REQUEST_PARAMS.set(params);
        sb.append(Constants.LOG_TAG).append("args: ").append(params);
        logger.info("nanoTime = " + start + sb.toString());
    }

    /**
     * 请求拦截后处理方法
     * 方法正常退出后执行
     *
     */
    @AfterReturning(value = "requestLog()", returning = "result")
    public void doAfterReturning(Object result) throws JsonProcessingException {
        Long start = this.startNanoTime.get();
        long spend_time = (System.nanoTime() - start) / 1_000_000;//秒
        String json = objectMapper.writeValueAsString(result);
        if (json.length() < 10_000) {
            String sb = Constants.LOG_TAG + "response json format: " + json
                    + Constants.LOG_TAG + "spend time: " + spend_time + " ms\n\n";
            logger.info("nanoTime = " + start + sb);
        } else {
            String sb = Constants.LOG_TAG + "response big json, length = : " + json.length()
                    + Constants.LOG_TAG + "spend time: " + spend_time + " ms\n\n";
            logger.info("nanoTime = " + start + sb);
        }
    }

}
1. 核心注解与定义
java 复制代码
@Aspect     // 声明这是一个切面类
@Component  // 交给 Spring 容器管理
public class RequestLogAspect {
    // ThreadLocal 确保多线程环境下的时间计算互不干扰
    private ThreadLocal<Long> startNanoTime = new ThreadLocal<>();
}
2. 定义切入点
java 复制代码
@Pointcut(value = "execution(public * com.*.*.controller..*.*(..))")
public void requestLog() {}
  • 这行代码定义了 拦截规则 :拦截 com...controller 包及其子包下,所有类的所有 public 方法。这意味着只要是 Controller 层的接口,都会自动触发这个切面。
3. 前置通知 (Before Advice) ------ 请求进来时做什么

请求进来时,记录开始时间,获取http请求对象,拼接并打印请求的详情,获取并打印Java方法参数

java 复制代码
@Before(value = "requestLog()")
public void doBefore(JoinPoint joinPoint) {
    // 1. 记录开始时间 (纳秒级)
    long start = System.nanoTime();
    this.startNanoTime.set(start);

    // 2. 获取当前的 HttpServletRequest 对象
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

    // 3. 拼接并打印请求详情
    StringBuilder sb = new StringBuilder();
    sb.append("URL: ").append(request.getRequestURL()) // 请求地址
      .append("method: ").append(request.getMethod())  // GET/POST
      .append("userId: ").append(request.getAttribute("userId")) // 操作人
      .append("remote address: ").append(request.getRemoteAddr()); // 来源IP

    // 4. 获取并打印 Java 方法参数
    Object[] args = joinPoint.getArgs();
    String params = Arrays.toString(args);
    logger.info("nanoTime = " + start + sb.toString());
}
4. 后置通知 (AfterReturning Advice) ------ 请求结束时做什么

请求结束的时候,获取当前时间,记录耗时,序列化响应结果,根据响应体的大小判断是否完全输出

java 复制代码
@AfterReturning(value = "requestLog()", returning = "result")
public void doAfterReturning(Object result) throws JsonProcessingException {
    // 1. 取出开始时间,计算耗时
    Long start = this.startNanoTime.get();
    long spend_time = (System.nanoTime() - start) / 1_000_000; // 换算成毫秒(ms)

    // 2. 序列化响应结果
    String json = objectMapper.writeValueAsString(result);

    // 3. 智能日志记录 (防止大 JSON 刷屏)
    if (json.length() < 10_000) {
        // 如果响应体小于 10k,打印完整内容
        logger.info("response json: " + json + " spend time: " + spend_time + " ms");
    } else {
        // 如果响应体太大,只打印长度,不打印内容 (保护日志磁盘空间)
        logger.info("response big json, length: " + json.length() + " spend time: " + spend_time + " ms");
    }
}
5. 总结

这个类是一个标准的*"日志拦截器"。它像一个安检门,所有进出 Controller 的请求都必须经过它。

  • 进门时 ( @Before ) :它给你拍张照,记下你是谁、几点来的、带了什么东西。
  • 出门时 ( @AfterReturning ) :它算一下你在里面待了多久,带走了什么东西(如果东西太大,它就只记个重量)。

使用场景

这个代码写好之后,自动生效,也就是所谓的零倾入性,不需要在 Controller 里写一行关于日志的代码,只要项目启动,Spring 容器(IOC)就会自动把这个切面织入到所有的 Controller 中。

那这里为什么可以做到自动生效呢?主要是这三个注解:

  • @Component :告诉 Spring,"把我作为一个 Bean 管理起来",这样它才能被扫描到。
  • @Aspect :告诉 Spring,"我不是一个普通的 Bean,我是一个切面(Interceptor)"。
  • @Pointcut :定义了规则,"我要拦截哪些方法"。
java 复制代码
@Pointcut(value = "execution(public * com.*.*.controller..*.*(..))")

这个表达式的意思是:只要是在 com...controller 包下的所有 public 方法,Spring 都会在运行时动态创建一个 代理对象 。当你调用 DialogController.saveDialogHistory() 时,实际上调用的是代理对象的 doBefore() -> saveDialogHistory() -> doAfterReturning() 流程。

当启动项目后,调用接口,控制台日志就会输出显示如下

相关推荐
sjmaysee5 分钟前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat
渔阳节度使1 小时前
SpringAI实时监控+观测性
后端·python·flask
Victor3561 小时前
MongoDB(42)如何使用$project阶段?
后端
Victor3561 小时前
MongoDB(43)什么是嵌入式文档?
后端
1.14(java)1 小时前
Spring-boot快速上手
java·开发语言·javaee
Darkdreams2 小时前
SpringBoot项目集成ONLYOFFICE
java·spring boot·后端
bropro2 小时前
【Spring Boot】Spring AOP中的环绕通知
spring boot·后端·spring
lhbian2 小时前
【Spring Cloud Alibaba】基于Spring Boot 3.x 搭建教程
java·spring boot·后端
luom01022 小时前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud
IT_陈寒2 小时前
Redis性能提升3倍的5个冷门技巧,90%开发者都不知道!
前端·人工智能·后端