《自动化埋点:利用 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);
}
相关推荐
qq_297574675 小时前
【实战教程】SpringBoot 集成阿里云短信服务实现验证码发送
spring boot·后端·阿里云
韩立学长7 小时前
【开题答辩实录分享】以《智能大学宿舍管理系统的设计与实现》为例进行选题答辩实录分享
数据库·spring boot·后端
编码者卢布9 小时前
【Azure Storage Account】Azure Table Storage 跨区批量迁移方案
后端·python·flask
她说..12 小时前
策略模式+工厂模式实现审批流(面试问答版)
java·后端·spring·面试·springboot·策略模式·javaee
梦梦代码精12 小时前
开源、免费、可商用:BuildingAI一站式体验报告
开发语言·前端·数据结构·人工智能·后端·开源·知识图谱
李慕婉学姐14 小时前
【开题答辩过程】以《基于Spring Boot的疗养院理疗管理系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·spring boot·后端
tb_first14 小时前
SSM速通2
java·javascript·后端
一路向北⁢14 小时前
Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(一)
java·spring boot·后端·sse·通信
风象南14 小时前
JFR:Spring Boot 应用的性能诊断利器
java·spring boot·后端
爱吃山竹的大肚肚14 小时前
微服务间通过Feign传输文件,处理MultipartFile类型
java·spring boot·后端·spring cloud·微服务