基于springmvc拓展机制的高性能日志审计框架的设计与实现

高性能日志审计框架设计与实现

在现代微服务架构中,审计日志是保障系统安全性、可追溯性和合规性的关键组件。本文将详细介绍我们团队设计并实现的一个高性能、零侵入式Spring Boot日志审计框架,该框架利用Spring MVC扩展机制和事件驱动模型,实现了高效的请求日志收集与处理。

源码地址:https://gitee.com/pengkaiyan/audit-log

一、设计背景与目标

随着业务复杂度增加,传统的AOP切面日志记录方式逐渐暴露出性能瓶颈问题。为了构建一个既满足功能需求又具备良好性能的日志审计系统,我们制定了如下设计目标:

  1. 零侵入性:通过注解方式启用,不对现有业务代码造成任何修改负担
  2. 高性能:采用Spring MVC底层扩展机制替代传统AOP,降低性能损耗
  3. 灵活性:支持全局配置和方法级细粒度配置
  4. 安全性:提供敏感字段脱敏处理能力
  5. 异步处理:基于事件驱动模型实现日志异步处理,不影响主业务流程性能

二、核心技术选型

2.1 Spring MVC扩展机制

相比传统的AOP实现方式,我们选择了Spring MVC的扩展机制作为基础架构。主要原因包括:

  • 直接嵌入请求处理流程,避免额外的代理层开销
  • 更精确地控制日志收集时机和内容
  • 减少不必要的方法拦截,提高整体性能

2.2 事件驱动模型

采用Spring的事件驱动模型处理日志数据,具有以下优势:

  • 解耦日志收集与处理逻辑
  • 支持异步处理,提升系统响应速度
  • 易于扩展多种日志处理方式(数据库存储、消息队列等)

三、架构设计详解

3.1 整体架构图

客户端请求 DispatcherServlet AuditLogRequestMappingHandlerAdapter AuditLogServletInvocableHandlerMethod 业务方法执行 收集日志信息 发布AuditLogEvent AuditLogListener 异步处理日志

关于mvc的请求流程请参考笔者的另一篇文章:https://blog.csdn.net/pky86676022/article/details/128363499

3.2 核心组件剖析

3.2.1 注解系统

框架提供了两个核心注解:

  1. @EnableAuditLog:用于开启审计日志功能(这个做法符合springboot的可插拔组件设计,例如开启异步支持也是加一个类似的注解在启动类上)
  2. @AuditLog:用于标记需要审计的具体方法
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AuditLogConfiguration.class)
@Documented
public @interface EnableAuditLog {
    /**
     * 是否开启此功能,默认开启
     */
    boolean enable() default true;

    /**
     * 需要扫描的包路径
     */
    String[] scanBasePackages() default {};
}
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
    /**
     * 操作名称
     */
    @AliasFor("name")
    String value() default "";

    /**
     * 操作名称
     */
    @AliasFor("value")
    String name() default "";

    /**
     * 是否记录请求头
     */
    boolean logHeaders() default true;

    /**
     * 是否记录请求参数
     */
    boolean logRequestParams() default true;

    /**
     * 是否记录响应参数
     */
    boolean logResponseParams() default true;

    /**
     * 排除敏感信息,防止审计时记录到用户敏感数据
     */
    String[] sensitiveParams() default {"authorization", "password", "secret", "token"};
}
3.2.2 配置中心

AuditLogConfiguration是框架的核心配置类,负责初始化所有必要组件:

  1. 扫描指定包路径下的Controller方法,识别带有@AuditLog注解的方法
  2. 构建方法与配置映射关系表并注册为Spring Bean
  3. 注册自定义的WebMvcRegistrations实现
java 复制代码
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
                                    BeanNameGenerator importBeanNameGenerator) {
    // 获取EnableAuditLog注解属性
    Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableAuditLog.class.getName(), true);
    // 如果关闭了日志审计功能,则直接返回
    if (annotationAttributes == null || !(boolean) annotationAttributes.get("enable")) {
        logger.debug("未启用审计日志功能");
        return;
    }

    // 获取包的扫描路径
    String[] basePackages = (String[]) annotationAttributes.get("scanBasePackages");
    if (basePackages == null || basePackages.length == 0) {
        // 如果没有指定包路径,则使用默认的包路径,使用当前注解位置所在的包及其子包
        basePackages = new String[]{
            ClassUtils.getPackageName(importingClassMetadata.getClassName())
        };
        logger.warn("审计日志未指定包路径,将使用当前注解位置所在的包及其子包" + basePackages[0]);
    }

    // 扫描包路径下的类,找出加了@AuditLog注解的controller方法
    Map<Method, MethodAuditLogBO> auditLogMethods = scanAuditLogAnnotation(basePackages);

    // 将auditLogMethods注册为bean
    BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(LinkedHashMap.class);
    beanDefinitionBuilder.addConstructorArgValue(auditLogMethods);
    registry.registerBeanDefinition("auditLogMethods", beanDefinitionBuilder.getBeanDefinition());

    // 注册审计日志核心bean
    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
        .genericBeanDefinition(AuditLogWebMvcRegistrations.class)
        .getBeanDefinition();
    registry.registerBeanDefinition("auditLogWebMvcRegistrations", beanDefinition);
}
3.2.3 Web MVC扩展

框架通过自定义WebMvcRegistrations实现替换默认的RequestMappingHandlerAdapter:

java 复制代码
public class AuditLogWebMvcRegistrations implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
        return new AuditLogRequestMappingHandlerAdapter();
    }
}

其中,AuditLogRequestMappingHandlerAdapter重写了createInvocableHandlerMethod方法,返回我们自定义的处理方法包装器:

java 复制代码
public class AuditLogRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
    @Override
    protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
        return new AuditLogServletInvocableHandlerMethod(handlerMethod, getWebApplicationContext());
    }
}
3.2.4 核心执行逻辑

AuditLogServletInvocableHandlerMethod是整个框架最核心的部分,它重写了doInvoke方法,在业务方法执行前后收集日志信息:

java 复制代码
@Override
protected Object doInvoke(Object... args) throws Exception {
    if (!auditLogMethods.containsKey(getMethod())) {
        // 如果当前的请求不在 auditLogMethods集合中,说明不需要做日志审计,则直接调用原方法
        return super.doInvoke(args);
    }

    MethodAuditLogBO methodAuditLog = auditLogMethods.get(getMethod());
    return invokeAndLog(methodAuditLog, args);
}

invokeAndLog方法中,我们收集完整的请求和响应信息,并通过Spring事件机制发布:

java 复制代码
private Object invokeAndLog(MethodAuditLogBO methodLog, Object[] args) throws Exception {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    long startTime = System.nanoTime(); // 记录开始时间
    Object returnValue = null;
    Exception exception = null;
    try {
        returnValue = super.doInvoke(args);
    } catch (Exception e) {
        exception = e;
        throw e; // 重新抛出异常,不影响原方法的行为
    } finally {
        String userId = request.getHeader("userId"); // 记录用户id
        String clientIp = request.getRemoteAddr(); // 这里简单记录最后一跳的IP地址,实际生产中,这里应该记录用户访问时的IP地址
        String requestType = request.getMethod(); // 请求类型
        String operationName = methodLog.getName(); // 操作名称
        String requestUri = request.getRequestURI(); // 请求URI
        String exceptionInfo = exception != null ? exception.getMessage() : "";

        // 获取 HttpServletResponse 对象并获取实际的状态码
//      HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
        int responseCode = exception != null ? 500 : 200;

        // 执行时长, 单位毫秒
        long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
        String requestHeaders = methodLog.isLogHeaders() ? getFilteredHeaders(request, methodLog.getSensitiveParams()) : "";
        String requestParams = methodLog.isLogRequestParams() ? Stream.of(args).map(Object::toString).collect(Collectors.joining(", ")) : "";
        String responseParams = methodLog.isLogResponseParams() ? (returnValue != null ? returnValue.toString() : "") : "";

        // 发布审计日志事件
        context.publishEvent(new AuditLogEvent(new AuditLogDO(121212L, userId, operationName, clientIp, requestUri, requestType, requestParams,
            requestHeaders, responseParams, responseCode, duration, exceptionInfo, new Date())));
    }

    return returnValue;
}
3.2.5 事件处理系统

通过Spring的事件机制,我们将日志收集与处理分离,确保主业务流程不受影响:

java 复制代码
@Component
public class AuditLogListener implements ApplicationListener<AuditLogEvent> {
    private static final Logger log = LoggerFactory.getLogger(AuditLogListener.class);

    @Override
    @Async
    public void onApplicationEvent(AuditLogEvent event) {
        // 可以在这里处理审计日志事件,例如保存到数据库或发送到消息队列
        // 这里只是简单地记录日志
        log.info("模拟打印日志审计事件: {}", event.getAuditLogDO());
    }
}

这里用了异步事件支持,可以尝试自己设置线程池,笔者的源代码中有注释掉的线程池,值得注意的是自定义线程池如果在极高并发下不应该丢弃任务,毕竟日志审计是不能丢失数据的(等保要求)!

我这里模拟打印到日志到控制台,如果是正式的生产环境,笔者认为最好是发送到消息队列,然后消费者处理具体的审计日志,甚至结合云原生K8S之类的还能做到敏感事件推送,比如说检测的不合理的时间深更半夜有用户下载导出了敏感数据报表,那么可以在消费者端做告警处理,给主管发送短信提醒之类的(k8s一般就有告警系统)

四、性能优化措施

4.1 避免AOP代理开销

通过Spring MVC扩展机制而非AOP实现,避免了代理对象创建和方法拦截的性能开销。

4.2 异步事件处理

采用@Async注解实现日志异步处理,确保主业务流程不受日志处理性能影响。

4.3 精确的方法匹配

只对明确标注@AuditLog注解的方法进行日志处理,避免全量方法拦截带来的性能损耗。

4.4 线程池优化

框架支持配置专用的线程池处理日志事件,避免与其他异步任务争抢资源。

五、使用示例

5.1 在主启动类上启用审计日志

java 复制代码
@SpringBootApplication
@EnableAuditLog(scanBasePackages = {"com.example.controller"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

5.2 方法级配置

java 复制代码
@RestController
@RequestMapping("/api")
public class UserController {

    @AuditLog(
        name = "查询用户列表",
        logHeaders = true,
        logRequestParams = true,
        logResponseParams = true
    )
    @GetMapping("/users")
    public ResponseEntity<List<User>> getUsers() {
        // 业务逻辑
        return ResponseEntity.ok(userService.findAll());
    }
}

六、测试

使用wrk命令在本地idea环境模拟测试接口

bash 复制代码
wrk -c 1000  -t 12 -d 30s http://172.18.0.1:8083/user/queryUserInfo3?userName=iiiii



测试结果显示了相当不错的性能表现:
并发连接数:1000
线程数:12
测试时长:30秒
总请求数:546,328
平均延迟:57.00ms
QPS(每秒请求数):18,152.28
这个结果表明您的高性能日志审计框架在高并发场景下仍然保持良好的性能表现。

如下是idea自带的性能工具分析:

七、总结

本框架通过创新性地运用Spring MVC扩展机制和事件驱动模型,成功实现了高性能、零侵入的日志审计解决方案。相比传统的AOP实现方式,该方案具有更低的性能损耗和更好的可控性,适用于对性能要求较高的企业级应用。

从我们的性能测试结果可以看出,即便在1000并发连接的高压环境下,框架仍能保持约18,000 QPS的处理能力,平均响应延迟仅为57ms。这充分证明了我们选择Spring MVC扩展机制而非AOP的正确性。

源码已经开源,托管在Gitee上:https://gitee.com/pengkaiyan/audit-log,欢迎访问、使用和贡献代码。

未来,我们可以进一步完善该框架,比如:

  1. 提供更多日志存储选项(数据库、消息队列等)
  2. 增强敏感信息识别和脱敏能力
  3. 添加日志查询和分析接口
  4. 支持分布式环境下的链路追踪
    笔者的源码欢迎大家一起修改,我的邮箱是pkyit@qq.com 欢迎交流,我们一起进步!
    通过不断迭代优化,相信这个框架能够为更多的项目提供稳定可靠的日志审计服务。
相关推荐
乐迪信息2 小时前
乐迪信息:AI摄像机识别煤矿出入井车辆数量异常检测
大数据·运维·人工智能·物联网·安全
qq_12498707532 小时前
基于springboot框架的小型饮料销售管理系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·spring·毕业设计
拾忆,想起2 小时前
设计模式:软件开发的可复用武功秘籍
开发语言·python·算法·微服务·设计模式·性能优化·服务发现
百度安全2 小时前
百度办公网安全秘诀分享——兼顾安全与效率
安全·网络安全·dubbo·办公安全
想用offer打牌2 小时前
数据库大事务有什么危害(面试版)
数据库·后端·架构
Jaising6662 小时前
Spring 错误使用事务导致数据可见性问题分析
数据库·spring boot
踏浪无痕3 小时前
别再只会用 Feign!手写一个 Mini RPC 框架搞懂 Spring Cloud 底层原理
后端·面试·架构
NMBG223 小时前
外卖综合项目
java·前端·spring boot
小徐Chao努力3 小时前
Spring AI Alibaba A2A 使用指南
java·人工智能·spring boot·spring·spring cloud·agent·a2a