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() 流程。

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

相关推荐
风景的人生2 小时前
request请求的@RequestParm标注的参数也需要放在请求路径后
java
短剑重铸之日2 小时前
《设计模式》第四篇:观察者模式
java·后端·观察者模式·设计模式
手握风云-2 小时前
JavaEE 进阶第十五期:Spring 日志的笔墨艺术
java·spring·java-ee
仟濹2 小时前
【Java加强】2 泛型 | 打卡day1
java·开发语言
Hx_Ma162 小时前
SpringBoot注册格式化器
java·spring boot·后端
V胡桃夹子2 小时前
VS Code / Lingma AI IDE Java 开发攻略手册
java·ide·人工智能
乔江seven2 小时前
【python轻量级Web框架 Flask 】1 Flask 初识
开发语言·后端·python·flask
独自破碎E2 小时前
【回溯】二叉树的所有路径
android·java
风景的人生2 小时前
application/x-www-form-urlencoded
java·mvc