《自动化埋点:利用 AOP 统一记录接口入参、出参及执行耗时》

前言

在定位线上问题时,最常用的操作就是查看接口的"入参是什么"以及"返回了什么"。如果手动在每个 Controller 方法中去打印日志,不仅代码臃肿,且难以维护。

通过 Spring AOP(面向切面编程) ,我们可以实现一种非侵入式的日志增强机制:在不修改任何业务代码的前提下,自动捕捉接口的请求参数、响应结果以及整个方法的执行耗时。

关键实现

性能监控 StopWatch

StopWatch 是 spring 提供的轻量级性能监控工具,使用方式如下:

java 复制代码
StopWatch stopWatch = new StopWatch("order-process");

stopWatch.start("query-db");
// 查询数据库
stopWatch.stop();

stopWatch.start("call-remote");
// 调用远程接口
stopWatch.stop();

stopWatch.start("calculate");
// 业务计算
stopWatch.stop();

log.info("cost info:\n{}", stopWatch.prettyPrint());

// 耗时
log.info("接口耗时:{}ms", stopWatch.getTotalTimeMillis());

切入点

配置切入点 @RestControllerController,对所有接口中的方法进行拦截:

java 复制代码
@Pointcut("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
public void controllerPointcut() {
    
}
    

获取 swagger 接口描述

配置上面切入点的环绕通知,对接口方法增强:

java 复制代码
    /**
     * 缓存方法描述,避免频繁反射
     */
    private final Map<Method, String> methodDescriptionCache = new ConcurrentHashMap<>();
    
    // swagger 接口全类名(用于反射)
    private static final String SWAGGER_OPERATION_CLASS = "io.swagger.v3.oas.annotations.Operation";

    /**
     * 获取方法描述(尝试从 Swagger 注解中获取)
     *
     * @param method 方法
     * @return 描述信息
     */
    private String getMethodDescription(Method method) {
        return methodDescriptionCache.computeIfAbsent(method, m -> {
            // 尝试获取 Swagger 3 (@Operation)
            String summary = getAnnotationValue(m, SWAGGER_OPERATION_CLASS, "summary");
            if (StringUtils.hasText(summary)) {
                return summary;
            }

            return "";
        });
    }

    /**
     * 反射获取注解属性值
     *
     * @param method          方法
     * @param annotationClass 注解全类名
     * @param property        属性名
     * @return 属性值
     */
    private String getAnnotationValue(Method method, String annotationClass, String property) {
        try {
            Class<?> clazz = Class.forName(annotationClass);
            if (clazz.isAnnotation()) {
                @SuppressWarnings("unchecked")
                Class<? extends Annotation> annotationType = (Class<? extends Annotation>) clazz;
                Annotation annotation = method.getAnnotation(annotationType);
                if (annotation != null) {
                    Method propertyMethod = clazz.getMethod(property);
                    Object value = propertyMethod.invoke(annotation);
                    return value != null ? value.toString() : null;
                }
            }
        } catch (Exception e) {
            // 忽略异常(类不存在或反射失败)
        }
        return null;
    }

完整实现

配置文件

java 复制代码
@Data
@ConfigurationProperties(prefix = "x-polaris.web")
public class PolarisWebProperties {

    /**
     * 请求日志配置
     */
    private Log log = new Log();

    /**
     * 请求日志配置类
     */
    @Data
    public static class Log {
        /**
         * 是否开启请求日志切面,默认 true
         */
        private boolean enabled = true;

        /**
         * 是否记录请求参数,默认 true
         */
        private boolean logRequest = true;

        /**
         * 是否记录响应结果,默认 true
         */
        private boolean logResponse = true;
    }
}

日志切面

java 复制代码
perties properties;

    /**
     * 缓存方法描述,避免频繁反射
     */
    private final Map<Method, String> methodDescriptionCache = new ConcurrentHashMap<>();

    /**
     * Swagger Operation 注解全类名
     */
    private static final String SWAGGER_OPERATION_CLASS = "io.swagger.v3.oas.annotations.Operation";

    @Pointcut("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
    public void controllerPointcut() {
    }

    @Around("controllerPointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        PolarisWebProperties.Log logConfig = properties.getLog();
        if (!logConfig.isEnabled()) {
            return point.proceed();
        }

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = null;
        HttpServletRequest request = null;

        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes();
            if (attributes != null) {
                request = attributes.getRequest();
            }

            // 获取方法描述
            String description = "";
            if (point.getSignature() instanceof MethodSignature) {
                Method method = ((MethodSignature) point.getSignature()).getMethod();
                description = getMethodDescription(method);
            }

            // 记录请求信息
            if (request != null && logConfig.isLogRequest()) {
                String method = request.getMethod();
                String uri = request.getRequestURI();
                String queryString = request.getQueryString();
                String args = Arrays.stream(point.getArgs())
                        .map(arg -> {
                            if (arg instanceof MultipartFile) {
                                return "MultipartFile:" + ((MultipartFile) arg).getOriginalFilename();
                            }
                            return String.valueOf(arg);
                        })
                        .collect(Collectors.joining(", "));

                if (StringUtils.hasText(description)) {
                    log.info("Request Start: ({}) [{}] {} , Args: {}", description, method,
                            uri + (queryString != null ? "?" + queryString : ""), args);
                } else {
                    log.info("Request Start: [{}] {}, Args: {}", method,
                            uri + (queryString != null ? "?" + queryString : ""), args);
                }
            }

            result = point.proceed();
            return result;

        } catch (Throwable e) {
            log.error("Request Error: {}", e.getMessage());
            throw e;
        } finally {
            stopWatch.stop();
            // 记录响应信息
            if (logConfig.isLogResponse()) {
                log.info("Request End: Time: {}ms, Result: {}", stopWatch.getTotalTimeMillis(), result);
            } else {
                log.info("Request End: Time: {}ms", stopWatch.getTotalTimeMillis());
            }
        }
    }

    /**
     * 获取方法描述(尝试从 Swagger 注解中获取)
     *
     * @param method 方法
     * @return 描述信息
     */
    private String getMethodDescription(Method method) {
        return methodDescriptionCache.computeIfAbsent(method, m -> {
            // 尝试获取 Swagger 3 (@Operation)
            String summary = getAnnotationValue(m, SWAGGER_OPERATION_CLASS, "summary");
            if (StringUtils.hasText(summary)) {
                return summary;
            }

            return "";
        });
    }

    /**
     * 反射获取注解属性值
     *
     * @param method          方法
     * @param annotationClass 注解全类名
     * @param property        属性名
     * @return 属性值
     */
    private String getAnnotationValue(Method method, String annotationClass, String property) {
        try {
            Class<?> clazz = Class.forName(annotationClass);
            if (clazz.isAnnotation()) {
                @SuppressWarnings("unchecked")
                Class<? extends Annotation> annotationType = (Class<? extends Annotation>) clazz;
                Annotation annotation = method.getAnnotation(annotationType);
                if (annotation != null) {
                    Method propertyMethod = clazz.getMethod(property);
                    Object value = propertyMethod.invoke(annotation);
                    return value != null ? value.toString() : null;
                }
            }
        } catch (Exception e) {
            // 忽略异常(类不存在或反射失败)
        }
        return null;
    }
}

自动配置

java 复制代码
 @Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "x-polaris.web.log", name = "enabled", havingValue = "true", matchIfMissing = true)
public RequestLogAspect requestLogAspect(PolarisWebProperties properties) {
    return new RequestLogAspect(properties);
}
相关推荐
zopple7 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001119 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本9 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji341610 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan10 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer11 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor35611 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor35611 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer12 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP13 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪