《自动化埋点:利用 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);
}
相关推荐
undsky2 小时前
【RuoYi-SpringBoot3-Pro】:多租户功能上手指南
spring boot·后端·mybatis
明天有专业课2 小时前
松耦合的设计模式-观察者
后端
鱼跃鹰飞2 小时前
怎么排查线上CPU100%的问题
java·jvm·后端
哈库纳2 小时前
dbVisitor 的双层适配架构
后端
我想问问天2 小时前
【从0到1大模型应用开发实战】04|RAG混合检索
后端·aigc
Andy工程师2 小时前
不要在 Bean(尤其是单例 Bean)里积累大量数据
后端
想用offer打牌2 小时前
Google Code Wiki: AI 代码知识库
后端·程序员·架构
ZoeGranger2 小时前
【Spring】使用Spring实现AOP
后端