《自动化埋点:利用 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);
}
相关推荐
掘金者阿豪25 分钟前
关系数据库迁移的“暗礁”:金仓数据库如何规避数据完整性与一致性风险
后端
ServBay42 分钟前
一个下午,一台电脑,终结你 90% 的 Symfony 重复劳动
后端·php·symfony
sino爱学习1 小时前
高性能线程池实践:Dubbo EagerThreadPool 设计与应用
java·后端
颜酱1 小时前
从二叉树到衍生结构:5种高频树结构原理+解析
javascript·后端·算法
掘金者阿豪1 小时前
UUID的隐形成本:一个让数据库“慢下来”的陷阱
后端
用户084465256371 小时前
Docker 部署 MongoDB Atlas 到服务端
后端
Anita_Sun2 小时前
一看就懂的 Haskell 教程 - 类型推断机制
后端·haskell
Anita_Sun2 小时前
一看就懂的 Haskell 教程 - 类型签名
后端·haskell
七八星天2 小时前
C#代码设计与设计模式
后端
砍材农夫3 小时前
threadlocal
后端