Skip to content

BeyondSoft Log Spring3 Starter 开发文档

1. 模块概述

BeyondSoft Log Spring3 Starter 是一个企业级日志记录模块,为 Spring Boot 应用提供完整的日志解决方案。该模块支持请求日志、登录日志和审计日志的记录,具备自动配置、数据脱敏、灵活查询等特性。

1.1 核心功能

  • 请求日志记录: 自动记录所有 HTTP 请求和响应
  • 登录日志记录: 记录用户登录登出操作
  • 审计日志记录: 记录数据变更操作,支持新旧数据对比
  • 数据脱敏: 支持手机号、邮箱、身份证号等敏感信息脱敏
  • 灵活查询: 提供统一的日志查询接口
  • 多存储支持: 支持文件和 MongoDB 存储

1.2 支持的日志类型

  • LOGIN_LOG: 登录日志
  • REQUEST_LOG: 请求日志
  • AUDIT_LOG: 审计日志

2. 项目结构

beyondsoft-log-spring3-starter/
├── src/main/java/com/beyondsoft/log/
│   ├── annotation/          # 日志注解
│   ├── aspect/              # 日志切面
│   ├── config/              # 自动配置类
│   ├── controller/          # 日志查询接口
│   ├── filter/              # 请求日志过滤器
│   ├── model/               # 日志数据模型
│   ├── parser/              # 审计日志解析器
│   ├── serialize/           # 脱敏序列化器
│   ├── service/             # 日志服务接口
│   └── utils/               # 工具类
├── docs/                    # 文档
└── pom.xml                  # Maven 依赖配置

3. 重要说明

3.1 @AuditLog 注解使用关键点

  1. serviceClass 是必填项(INSERT 操作除外)

    • UPDATE/DELETE 操作必须指定 serviceClass
    • serviceClass 必须实现 MyBatis-Flex 的 IService 接口
    • 框架通过 serviceClass 调用 getById() 或 listByIds() 查询数据
  2. 默认查询机制

    • 框架会从方法参数中提取 primaryKey 字段的值作为ID
    • 调用 serviceClass.getById(id) 查询单个对象
    • 调用 serviceClass.listByIds(ids) 查询批量对象
  3. 自定义查询方法

    • 通过 customQueryMethod 指定 Service 中的自定义方法
    • 自定义方法必须接收一个参数
    • 默认传入整个 request 对象
    • 可通过 customQueryParamField 从 request 中提取特定字段
  4. 操作类型说明

    • INSERT:直接记录方法参数为 newObject,不查询 oldObject
    • UPDATE:执行前查询 oldObject,执行后查询 newObject
    • DELETE:执行前查询 oldObject,不查询 newObject
    • QUERY:不建议使用,纯查询操作建议使用 INSERT 类型
  5. primaryKey 的作用

    • 从方法参数中提取此字段的值作为ID传给查询方法
    • 从查询结果中提取此字段的值作为 businessId
    • 默认值为 "id"

3.2 工作流程

  1. @Before 阶段:

    • 初始化日志对象,记录操作人、IP、时间等信息
    • 调用 AuditParser.getOldResult() 查询旧数据
  2. 方法执行:

    • 执行实际的业务操作
  3. @After 阶段:

    • 调用 AuditParser.getNewResult() 查询新数据
    • 如果 needDefaultCompare=true,生成字段对比结果
    • 调用 BeyondSoftLogService.saveBatch() 保存日志

3.3 注意事项

  1. Service 必须实现 IService 接口
  2. INSERT 操作不需要 serviceClass
  3. needDefaultCompare 只对 UPDATE 操作有效
  4. 批量操作使用 batchIdParam 指定ID列表字段
  5. 日志保存到 MongoDB,需要配置 MongoDB 连接

4. 核心组件详解

3.1 日志注解

3.1.1 @AuditLog - 审计日志注解

使用说明:

  • 标记在方法上,用于记录审计日志
  • 支持多种参数组合,满足不同业务场景
  • 可自定义操作名称、类型、主键字段等

注解参数说明:

参数类型默认值说明
handleNameString""具体业务操作名称
operationTypeOperationTypeNONE操作类型(INSERT/UPDATE/DELETE/QUERY)
serviceClassClassClass.class查询数据库所调用的class文件
needDefaultComparebooleanfalse是否需要默认的改动比较
idTypeClassInteger.classID的类型
primaryKeyString"id"主键字段名
batchIdParamString"ids"批量id名称
customQueryMethodString""自定义查询方法名称
customQueryParamFieldString""自定义查询方法的参数字段名
loginNameStringUSERNAME登录账号的key值

完整示例代码 - 展示Controller层使用@AuditLog注解的各种组合场景:

java
@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // ==================== 新增操作 ====================
    
    /**
     * 场景1:简单新增
     * 适用场景:新增数据,不需要查询旧数据
     */
    @PostMapping("/add")
    @AuditLog(handleName = "添加用户", operationType = OperationType.INSERT)
    public Result<Void> addUser(@RequestBody UserCreateRequest request) {
        userService.addUser(request);
        return Result.success();
    }
    
    // ==================== 更新操作 ====================
    
    /**
     * 场景2:根据ID更新(必须指定serviceClass)
     * 适用场景:根据主键ID更新单条记录
     * 说明:
     * - serviceClass 是必填项,用于调用 service.getById() 查询旧数据
     * - 框架通过 primaryKey + idType 从请求对象中提取ID
     * - 旧数据通过 serviceClass.getById() 查询,新数据在方法执行后再次查询
     */
    @PutMapping("/update")
    @AuditLog(handleName = "更新用户", operationType = OperationType.UPDATE,
             serviceClass = UserService.class,
             primaryKey = "userId", idType = Long.class)
    public Result<Void> updateUser(@RequestBody UserUpdateRequest request) {
        userService.updateUser(request);
        return Result.success();
    }
    
    /**
     * 场景3:更新并自动对比字段
     * 适用场景:需要详细记录哪些字段发生了变化
     * 审计日志会显示:字段名[旧值 → 新值]
     * 说明:
     * - needDefaultCompare = true 开启字段变更对比
     * - serviceClass 必须指定,用于查询旧数据和新数据
     * - 该参数仅对 UPDATE 操作有效
     */
    @PutMapping("/updateWithCompare")
    @AuditLog(handleName = "修改用户信息", operationType = OperationType.UPDATE,
             serviceClass = UserService.class,
             primaryKey = "userId", idType = Long.class,
             needDefaultCompare = true)
    public Result<Void> updateUserWithCompare(@RequestBody UserUpdateRequest request) {
        userService.updateUserWithCompare(request);
        return Result.success();
    }
    
    /**
     * 场景4:批量更新 - 使用自定义查询方法
     * 适用场景:需要批量更新满足特定条件的数据,如禁用所有男性用户
     * 说明:
     * - 使用 customQueryMethod 查询满足条件的数据列表作为 oldObject
     * - 批量更新后,框架会再次调用该方法获取更新后的数据作为 newObject
     * - customQueryParamField 指定从request中提取gender字段传给查询方法
     * - primaryKey 用于从查询结果中提取每个对象的businessId字段
     */
    @PutMapping("/disableByGender")
    @AuditLog(handleName = "批量禁用用户", operationType = OperationType.UPDATE,
             serviceClass = UserService.class, customQueryMethod = "getByGender",
             primaryKey = "id",
             customQueryParamField = "gender")
    public Result<Void> disableUsersByGender(@RequestBody GenderDisableRequest request) {
        userService.disableUsersByGender(request.getGender());
        return Result.success();
    }

        
    // ==================== 删除操作 ====================
    
    /**
     * 场景5:删除记录(必须指定serviceClass)
     * 适用场景:需要追踪被删除的数据详情
     * 说明:
     * - serviceClass 是必填项,用于删除前查询旧数据
     * - 框架通过 primaryKey + idType 从请求中提取ID
     * - 旧数据通过 serviceClass.getById() 查询
     */
    @DeleteMapping("/delete")
    @AuditLog(handleName = "删除用户", operationType = OperationType.DELETE,
             serviceClass = UserService.class,
             primaryKey = "userId", idType = Long.class)
    public Result<Void> deleteUser(@RequestBody DeleteUserRequest request) {
        userService.deleteUser(request);
        return Result.success();
    }
    
    /**
     * 场景8:批量删除用户
     * 适用场景:批量删除多个用户记录
     * 说明:
     * - batchIdParam 指定从request中提取ID列表的字段名
     * - 框架会调用 serviceClass.listByIds() 批量查询旧数据
     * - primaryKey 用于从查询结果中提取每个对象的businessId
     */
    @DeleteMapping("/batchDelete")
    @AuditLog(handleName = "批量删除用户", operationType = OperationType.DELETE,
             serviceClass = UserService.class,
             primaryKey = "id",
             batchIdParam = "userIds",
             idType = Long.class)
    public Result<Void> batchDeleteUsers(@RequestBody BatchDeleteRequest request) {
        userService.batchDeleteUsers(request);
        return Result.success();
    }
    
    /**
     * 场景9:记录重要查询操作(INSERT类型)
     * 适用场景:追踪敏感数据的查询记录,如导出操作
     * 说明:
     * - 对于纯查询操作,建议使用 INSERT 类型,只记录查询结果到 newObject
     * - 可选择使用自定义查询方法或直接在方法内查询
     * - primaryKey 用于从查询结果中提取businessId
     */
    @GetMapping("/export")
    @AuditLog(handleName = "导出用户数据", operationType = OperationType.INSERT,
             serviceClass = UserService.class, 
             customQueryMethod = "queryUsers",
             primaryKey = "id")
    public Result<List<User>> exportUsers(UserQueryRequest request) {
        List<User> users = userService.exportUsers(request);
        return Result.success(users);
    }
}

// ==================== Service层代码 ====================

@Service
public class UserService implements IService<User> {
    
    @Autowired
    private UserMapper userMapper;
    
    // ==================== 业务方法 ====================
    
    public void addUser(UserCreateRequest request) {
        User user = new User();
        BeanUtils.copyProperties(request, user);
        user.setCreateTime(LocalDateTime.now());
        user.setCreateBy(getCurrentUserId());
        userMapper.insert(user);
    }
    
    public void updateUser(UserUpdateRequest request) {
        User user = getById(request.getUserId());
        BeanUtils.copyProperties(request, user);
        user.setUpdateTime(LocalDateTime.now());
        user.setUpdateBy(getCurrentUserId());
        updateById(user);
    }
    
    public void updateUserWithCompare(UserUpdateRequest request) {
        User user = getById(request.getUserId());
        BeanUtils.copyProperties(request, user);
        user.setUpdateTime(LocalDateTime.now());
        user.setUpdateBy(getCurrentUserId());
        updateById(user);
    }
    
    public void deleteUser(DeleteUserRequest request) {
        removeById(request.getUserId());
    }
    
    public void batchDeleteUsers(BatchDeleteRequest request) {
        removeByIds(request.getUserIds());
    }
    
    public void disableUsersByGender(String gender) {
        List<User> users = getByGender(gender);
        for (User user : users) {
            user.setStatus(0); // 0-禁用
            user.setUpdateTime(LocalDateTime.now());
            updateById(user);
        }
    }
    
    public List<User> exportUsers(UserQueryRequest request) {
        return queryUsers(request);
    }
    
    // ==================== 自定义查询方法(用于@AuditLog) ====================
    
    /**
     * 根据性别查询用户列表
     * 用于场景4:批量更新条件查询
     */
    public List<User> getByGender(String gender) {
        return userMapper.selectList(new LambdaQueryWrapper<User>()
            .eq(User::getGender, gender));
    }
    
    /**
     * 复杂条件查询用户
     * 用于场景9:导出等需要复杂查询的场景
     */
    public List<User> queryUsers(UserQueryRequest request) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        if (request.getDepartmentId() != null) {
            wrapper.eq(User::getDepartmentId, request.getDepartmentId());
        }
        if (request.getStatus() != null) {
            wrapper.eq(User::getStatus, request.getStatus());
        }
        return userMapper.selectList(wrapper);
    }
}

3.1.2 @LoginLog - 登录日志注解

使用说明:

  • 标记在登录/登出方法上
  • 自动记录登录账号、IP、浏览器等信息

示例代码:

java
@RestController
public class AuthController {
    
    @LoginLog(operationType = OperationType.LOGIN)
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest request) {
        // 登录逻辑
        return Result.success();
    }
    
    @LoginLog(operationType = OperationType.LOGOUT)
    @PostMapping("/logout")
    public Result logout() {
        // 登出逻辑
        return Result.success();
    }
}

3.2 日志切面

3.2.1 AuditLogAspect - 审计日志切面

核心功能:

  • 拦截带有 @AuditLog 注解的方法
  • 自动提取方法参数中的新旧数据
  • 生成详细的变更对比信息
  • 支持批量操作的日志记录

工作流程:

  1. 方法执行前(@Before):记录方法入参,查询旧数据
  2. 方法执行:执行实际的业务操作
  3. 方法执行后(@After):查询新数据,生成对比结果
  4. 日志保存:调用 BeyondSoftLogService.saveBatch() 保存到 MongoDB

3.2.2 LoginLogAspect - 登录日志切面

核心功能:

  • 拦截带有 @LoginLog 注解的方法
  • 自动获取用户登录信息
  • 记录登录时间、IP地址、浏览器信息
  • 支持登录失败记录

3.3 请求日志过滤器

3.3.1 RequestLogFilter - 请求日志过滤器

核心功能:

  • 自动记录所有 HTTP 请求和响应
  • 支持 multipart/form-data 请求解析
  • 文件下载记录功能
  • 响应内容长度限制和脱敏
  • 支持排除特定 URL 模式

配置特性:

  • 可配置最大响应长度(默认1024字符)
  • 支持 URL 模式匹配
  • 可配置是否记录响应体
  • 支持认证/未认证资源路径配置

过滤规则:

  • 跳过日志查询接口 /api/log/*
  • 跳过 SSE 请求
  • 支持 multipart 请求处理
  • 响应内容截断保护

3.4 日志数据模型

3.4.1 BeyondSoftRequestLog - 请求日志模型

java
@Document
public class BeyondSoftRequestLog implements Serializable {
   @Serial
   private static final long serialVersionUID = 5460589354989806672L;

   private String ip;                 // IP地址
   private String location;           // 地理位置
   private String requestType;        // 请求类型(GET/POST等)
   private String url;                // 请求URL
   private String requestParams;      // 请求参数
   private String contentType;        // 内容类型
   private String operator;           // 操作人
   private String userAgent;          // User-Agent
   private LocalDateTime startTime;   // 开始时间
   private LocalDateTime endTime;     // 结束时间
   private Long spendTime;            // 耗时(毫秒)
   private Integer status;            // HTTP状态码
   private String response;           // 响应体(完整内容)

   @Transient
   private String responseLog;        // 响应日志(截断后的内容,仅用于控制台输出)
   private String error;              // 错误信息
   private String traceId;            // 追踪ID
   private LocalDateTime requestTime; // 请求时间

   @Transient
   private List<LocalDateTime> duration; // 持续时间列表
}

3.4.2 BeyondSoftLoginLog - 登录日志模型

java
@Document
public class BeyondSoftLoginLog implements Serializable {
    @Serial
    private static final long serialVersionUID = 7469996603969901543L;

    private String username;           // 用户名
    private String ip;                 // IP地址
    private String location;           // 地理位置
    private String platform;           // 平台信息
    private String browser;            // 浏览器
    private String os;                 // 操作系统
    private String status = "登录成功"; // 状态(默认:登录成功)
    private String response;           // 响应信息
    private LocalDateTime requestTime; // 请求时间

    @Transient
    private List<LocalDateTime> duration; // 持续时间列表
}

3.4.3 BeyondSoftDataAuditLog - 审计日志模型

java
@Document
public class BeyondSoftDataAuditLog implements Serializable {
    
    @Serial
    private static final long serialVersionUID = 3058996912357426755L;

    private String operator;
    private LocalDateTime modifyDate;
    private OperationType operation;
    private String handleName;
    private String modifyContent;
    private String modifierIp;
    private String modifierLocation;
    private Object businessId;
    private String businessDataStatus;
    private Object oldObject;
    private Object newObject;
    private String response;
    private LocalDateTime requestTime;

    @Transient
    private List<LocalDateTime> duration;
}

3.5 脱敏序列化器

3.5.1 DesensitizeJsonSerializer - 注解驱动的脱敏序列化器

核心功能:

  • 继承 Jackson 的 JsonSerializer 和 ContextualSerializer
  • 基于 @Desensitize 注解实现实体类字段脱敏
  • 通过 DesensitizeStrategy 枚举定义脱敏策略

使用示例:

java
public class User {
    @Desensitize(strategy = DesensitizeStrategy.PHONE)
    private String phone;
    
    @Desensitize(strategy = DesensitizeStrategy.EMAIL)
    private String email;
    
    @Desensitize(strategy = DesensitizeStrategy.ID_CARD)
    private String idCard;
}

3.5.2 DesensitizationUtil - 配置文件驱动的日志脱敏工具

核心功能:

  • 基于 logback-desensitize.yml 配置文件实现日志脱敏
  • 通过正则表达式自动匹配日志中的敏感字段
  • 支持单字段规则(pattern)和多字段规则(patterns)
  • 支持自定义脱敏规则和内置脱敏规则

配置文件位置:

  • 配置文件名: logback-desensitize.yml
  • 默认路径: 引用方项目的 src/main/resources/ 目录下

配置文件格式:

yaml
log-desensitize:
  # 是否开启脱敏(必须设置为 true)
  open: true
  
  # 是否忽略key的大小写(默认 true)
  ignore: true
  
  # 单字段规则配置
  pattern:
    password: password
    phone: 3,7
  
  # 多字段规则配置(推荐)
  patterns:
    # 密码类敏感信息 - 完全脱敏
    - key: password,passwd,pwd,userPassword
      custom:
        defaultRegex: password
        position: password
    
    # 手机号 - 保留前3位和后4位
    - key: phone,mobile,telephone
      custom:
        defaultRegex: phone
        position: 3,7
    
    # 身份证号 - 保留前6位和后4位
    - key: idCard,id_card,identityCard
      custom:
        defaultRegex: identity
        position: 6,14
    
    # 邮箱 - 保留@前3位和@后全部
    - key: email,mailbox,mail
      custom:
        defaultRegex: email
        position: "@>3,10"
    
    # 自定义规则 - Token类
    - key: token,accessToken,authorization
      custom:
        defaultRegex: other
        position: 3,8

内置脱敏规则:

  • phone: 手机号验证规则 (^1[0-9]{10}$)
  • email: 邮箱验证规则 (^[\w-]+@[\w-]+(.[\w-]+)+$)
  • identity: 身份证验证规则 ((^\d{18}$)|(^\d{15}$))
  • password: 完全脱敏为 ******
  • other: 其他自定义规则

脱敏位置配置说明:

  • 3,7: 保留前3位和后4位 (下标: 0-2 和 7-end)
  • @>3,10: 在@符号后保留前3位和后10位
  • @<3,10: 在@符号前保留指定位置
  • password: 完全脱敏

工作原理:

  1. YmlUtils 加载并解析 logback-desensitize.yml 配置文件
  2. DesensitizationAppender 在 Logback Appender 层拦截日志事件
  3. DesensitizationUtil.customChange() 使用正则表达式匹配敏感字段
  4. 根据配置的脱敏规则对匹配到的值进行脱敏处理
  5. 将脱敏后的日志输出到控制台或文件

3.6 日志服务接口

3.6.1 BeyondSoftLogService - 日志服务接口

核心方法:

方法名参数返回值说明
saveRequestLogBeyondSoftRequestLogvoid保存请求日志
saveLoginLogBeyondSoftLoginLogvoid保存登录日志
saveDataAuditLogBeyondSoftDataAuditLogvoid保存审计日志
findRequestLogsRequestLogQueryList<BeyondSoftRequestLog>查询请求日志
findLoginLogsLoginLogQueryList<BeyondSoftLoginLog>查询登录日志
findDataAuditLogsDataAuditLogQueryList<BeyondSoftDataAuditLog>查询审计日志

3.7 日志查询控制器

3.7.1 BeyondSoftLogQueryController - 统一日志查询接口

核心功能:

  • 提供统一的日志查询REST接口
  • 通过 doc 参数区分不同类型的日志
  • 支持灵活的条件查询和分页
  • 基础路径可通过配置自定义

API接口:

请求方法路径说明
GET$统一日志查询接口

查询参数:

参数名类型必填说明
docDocumentEnums日志类型: LOGIN_LOGREQUEST_LOGAUDIT_LOG
pageNumInteger页码,默认值: 1
pageSizeInteger每页条数,默认值: 10
其他参数String动态查询条件,根据日志类型不同而不同

支持的日志类型 (DocumentEnums):

枚举值queryNamedocumentName对应实体类
LOGIN_LOGloginLogBeyondSoftLoginLogBeyondSoftLoginLog
REQUEST_LOGrequestLogBeyondSoftRequestLogBeyondSoftRequestLog
AUDIT_LOGauditLogBeyondSoftDataAuditLogBeyondSoftDataAuditLog

配置项:

yaml
beyond-soft:
  web:
    log-uri: /api/log  # 日志查询接口基础路径,默认: /api/log

使用示例:

bash
# 查询登录日志列表
curl "http://localhost:8080/api/log?doc=LOGIN_LOG&pageNum=1&pageSize=10"

# 查询请求日志列表(带条件)
curl "http://localhost:8080/api/log?doc=REQUEST_LOG&pageNum=1&pageSize=20&loginName=admin"

# 查询审计日志列表
curl "http://localhost:8080/api/log?doc=AUDIT_LOG&pageNum=1&pageSize=10&handleName=更新用户"

# 自定义基础路径查询
# 配置: beyond-soft.web.log-uri=/system/logs
curl "http://localhost:8080/system/logs?doc=LOGIN_LOG&pageNum=1&pageSize=10"

返回结果:

json

{
  "total": 100,
  "pageNum": 1,
  "pageSize": 10,
  "pages": 10,
  "records": [
    {
      // 日志记录字段,根据doc类型不同而不同
    }
  ]
}

4. 核心功能详解

4.1 请求日志记录

核心流程:

  1. RequestLogFilter 拦截所有请求(继承 OncePerRequestFilter)
  2. 使用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 包装请求和响应
  3. 记录请求信息:IP、地理位置、请求方法、URL、请求参数、User-Agent
  4. 执行过滤器链,处理实际请求
  5. 记录响应信息:响应状态、响应体、耗时
  6. 特殊处理:
    • 文件下载:记录文件名和文件大小,不记录响应体内容
    • 认证路径:匹配 authResourcePath 或 unAuthResourcePath 的请求不记录响应体
  7. 同步保存请求日志到 MongoDB(通过 BeyondSoftLogService.save())
  8. copyBodyToResponse() 复制响应体给客户端

过滤规则:

  • 跳过日志查询接口:logUri 配置的路径
  • 跳过 SSE(Server-Sent Events)请求
  • 支持 multipart/form-data 请求处理
  • 响应内容截断保护(MAX_LOG_RESPONSE_LENGTH = 1024)

配置选项:

  • logTypes: 日志存储类型(如 "mongodb")
  • authResourcePath: 认证资源路径模式
  • unAuthResourcePath: 非认证资源路径模式
  • logUri: 日志查询接口路径

4.2 登录日志记录

核心流程:

  1. LoginAspect 拦截 @LoginLog 注解方法
  2. @Before 阶段:
    • 从 HttpServletRequest 获取 IP 地址和地理位置
    • 解析 User-Agent 获取浏览器、操作系统、平台信息
    • 根据 operationType 获取用户名:
      • LOGIN:从请求参数或请求体中提取(根据 Content-Type)
      • LOGOUT:从 StpUtil.getExtra() 获取
    • 创建 BeyondSoftLoginLog 对象
  3. @After 阶段:
    • 设置 requestTime 为当前时间
    • 同步保存登录日志到 MongoDB(通过 BeyondSoftLogService.save())
  4. @AfterThrowing 阶段:
    • 设置 status 为失败状态

注解参数:

  • operationType: 操作类型(OperationType.LOGIN / OperationType.LOGOUT)
  • value: 用户名字段名,默认 "username"

4.3 审计日志记录

核心流程:

  1. AuditLogAspect 拦截 @AuditLog 注解方法
  2. 解析注解参数,获取操作类型、主键等配置
  3. 方法执行前:通过 AuditParser.getOldResult() 查询修改前的旧数据
    • INSERT操作:直接返回方法参数作为 newObject
    • UPDATE/DELETE操作:调用 serviceClass 的查询方法获取 oldObject
  4. 执行目标方法
  5. 方法执行后:通过 AuditParser.getNewResult() 查询修改后的新数据
    • UPDATE操作:再次调用查询方法获取 newObject
  6. 如果 needDefaultCompare=true,生成差异对比内容
  7. 调用 BeyondSoftLogService.saveBatch() 保存审计日志到 MongoDB

数据对比逻辑:

  • 对比 oldObject 和 newObject 的差异
  • 生成格式化的变更描述:字段名[旧值 → 新值]
  • 支持嵌套对象的深度对比
  • 自动忽略 null 值字段

重要说明:

  • serviceClass 是必填项(INSERT操作除外)
  • serviceClass 必须实现 MyBatis-Flex 的 IService 接口
  • 默认查询方法使用 getById() 或 listByIds()
  • customQueryMethod 可以指定任意自定义查询方法

配置选项:

  • log-audit-enabled: 是否启用审计日志
  • audit-log-collection: 审计日志集合名
  • need-default-compare: 是否需要默认的字段变更对比
  • custom-query-method: 自定义查询方法名

4.4 日志存储

存储策略:

  • MongoDB:默认存储方式,支持高性能查询
  • 文件存储:可选的备用存储方式
  • 双写模式:同时写入 MongoDB 和文件

MongoDB 集合:

  • beyond_soft_request_log: 请求日志集合
  • beyond_soft_login_log: 登录日志集合
  • beyond_soft_data_audit_log: 审计日志集合

索引配置:

  • createTime 索引:支持时间范围查询
  • loginName 索引:支持按用户名查询
  • handleName 索引:支持按操作名称查询
  • 复合索引:支持多条件组合查询

4.5 日志查询

查询能力:

  • 按时间范围查询
  • 按用户查询
  • 按操作类型查询
  • 按业务类型查询
  • 全文搜索
  • 分页查询

返回结果:

  • 请求日志:请求参数、响应内容、耗时
  • 登录日志:登录IP、设备信息
  • 审计日志:变更前后的数据对比

4.6 日志脱敏

脱敏策略:

  • 手机号:138****8888
  • 邮箱:te***@example.com
  • 身份证号:110101****1234
  • 银行卡号:6222 **** **** 8888
  • 姓名:张*

配置方式:

yaml
beyondsoft:
  log:
    desensitization:
      enabled: true
      types:
        - phone
        - email
        - idCard

5. 配置项说明

5.1 核心配置

配置项类型默认值说明
beyond-soft.log.enabledbooleantrue是否启用日志模块
beyond-soft.log.showSqlbooleantrue是否显示SQL日志
beyond-soft.log.showRespbooleantrue是否显示响应日志
beyond-soft.log.logTypesList<String>["file"]日志存储类型(file, mongodb)

5.2 MongoDB 配置

当 logTypes 包含 "mongodb" 时需要配置 MongoDB 连接

配置项类型默认值说明
spring.data.mongodb.hostStringlocalhostMongoDB主机地址
spring.data.mongodb.portint27017MongoDB端口
spring.data.mongodb.databaseStringtest数据库名称
spring.data.mongodb.usernameString用户名
spring.data.mongodb.passwordString密码

5.3 配置示例

yaml
beyond-soft:
  log:
    enabled: true
    showSql: true
    showResp: true
    logTypes: ["file", "mongodb"]  # 同时保存到文件和MongoDB

# MongoDB 连接配置(当使用MongoDB存储时)
spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      database: beyondsoft_logs
      username: admin
      password: admin123

6. 高级用法

6.1 自定义查询方法

在某些复杂场景下,需要使用 Service 中的自定义查询方法:

java
@Service
public class UserService implements IService<User> {
    
    // 自定义查询方法,包含关联数据
    public User getUserWithRoles(Long userId) {
        User user = getById(userId);
        if (user != null) {
            user.setRoles(roleMapper.selectRolesByUserId(userId));
        }
        return user;
    }
}

@RestController
public class UserController {
    
    @PutMapping("/updateWithRoles")
    @AuditLog(handleName = "更新用户", operationType = OperationType.UPDATE,
             serviceClass = UserService.class, 
             customQueryMethod = "getUserWithRoles",
             primaryKey = "userId", idType = Long.class)
    public Result<Void> updateUserWithRoles(@RequestBody UserUpdateRequest request) {
        userService.updateUserWithRoles(request);
        return Result.success();
    }
}

6.2 批量操作日志

批量操作场景下,使用 batchIdParam 参数指定ID列表字段:

java
@DeleteMapping("/batchDelete")
@AuditLog(handleName = "批量删除用户", operationType = OperationType.DELETE,
         serviceClass = UserService.class,
         batchIdParam = "userIds",  // 指定request中ID列表字段名
         primaryKey = "id",
         idType = Long.class)
public Result<Void> batchDeleteUsers(@RequestBody BatchDeleteRequest request) {
    userService.batchDeleteUsers(request.getUserIds());
    return Result.success();
}

@Service
public class UserService implements IService<User> {
    
    public void batchDeleteUsers(List<Long> userIds) {
        // 批量删除
        removeByIds(userIds);
    }
}

6.3 开启字段变更对比

当需要记录详细的字段变更信息时:

java
@PutMapping("/update")
@AuditLog(handleName = "更新用户", operationType = OperationType.UPDATE,
         serviceClass = UserService.class,
         primaryKey = "userId", idType = Long.class,
         needDefaultCompare = true)  // 开启字段对比
public Result<Void> updateUser(@RequestBody UserUpdateRequest request) {
    userService.updateUser(request);
    return Result.success();
}

审计日志会在 modifyContent 字段记录详细的变更信息,格式为:字段名[旧值 → 新值]

7. 常见问题

7.1 审计日志没有记录旧数据

问题描述: 使用 @AuditLog 注解后,审计日志中 oldObject 为空

可能原因:

  1. 没有配置 serviceClass(UPDATE/DELETE 操作必须配置)
  2. serviceClass 没有实现 MyBatis-Flex 的 IService 接口
  3. 自定义查询方法返回 null
  4. 主键字段名不匹配

解决方案:

java
// 方案1:使用默认查询逻辑(必须指定serviceClass)
@AuditLog(handleName = "更新用户", operationType = OperationType.UPDATE,
         serviceClass = UserService.class,  // 必须指定
         primaryKey = "userId", idType = Long.class)
public Result<Void> updateUser(@RequestBody UserUpdateRequest request) {
    // 框架会自动调用 UserService.getById() 查询旧数据
    return Result.success();
}

// 方案2:使用自定义查询方法
@AuditLog(handleName = "更新用户", operationType = OperationType.UPDATE,
         serviceClass = UserService.class, 
         customQueryMethod = "getByUserCode",
         primaryKey = "userCode",
         customQueryParamField = "userCode")
public Result<Void> updateUser(@RequestBody UserUpdateRequest request) {
    // 框架会调用 UserService.getByUserCode(request.getUserCode()) 查询旧数据
    return Result.success();
}

// 确保 Service 实现了 IService 接口
@Service
public class UserService implements IService<User> {
    // ...
}

7.2 字段对比结果不正确

问题描述: needDefaultCompare = true 时,字段对比结果与预期不符

可能原因:

  1. serviceClass 没有指定
  2. 查询方法返回的数据不一致
  3. 数据被其他事务修改
  4. 字段类型不一致

解决方案:

java
// 确保指定 serviceClass
@AuditLog(handleName = "更新用户", operationType = OperationType.UPDATE,
         serviceClass = UserService.class,  // 必须指定
         primaryKey = "userId", idType = Long.class,
         needDefaultCompare = true)
public Result<Void> updateUser(@RequestBody UserUpdateRequest request) {
    userService.updateUser(request);
    return Result.success();
}

// Service 层确保查询方法返回正确的数据
@Service
public class UserService implements IService<User> {
    
    public void updateUser(UserUpdateRequest request) {
        User user = getById(request.getUserId());
        BeanUtils.copyProperties(request, user);
        updateById(user);
    }
}

7.3 批量操作性能问题

问题描述: 批量操作时审计日志记录耗时较长

可能原因:

  1. 批量查询数据量大
  2. MongoDB 连接池配置不当
  3. 开启了 needDefaultCompare 导致大量对比计算

解决方案:

java
// 1. 优化批量查询,确保 Service 实现了 IService 接口
@Service
public class UserService implements IService<User> {
    
    // MyBatis-Flex 的 IService 接口已经提供了高效的 listByIds 方法
    // 框架会自动调用此方法
}

// 2. 批量操作可以关闭 needDefaultCompare 减少对比计算
@DeleteMapping("/batchDelete")
@AuditLog(handleName = "批量删除用户", operationType = OperationType.DELETE,
         serviceClass = UserService.class,
         batchIdParam = "userIds",
         primaryKey = "id",
         idType = Long.class,
         needDefaultCompare = false)  // 关闭对比
yaml
# 3. 调整 MongoDB 连接池
spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/beyondsoft_logs
      auto-index-creation: true

7.4 脱敏规则不生效

问题描述: 配置了脱敏规则,但日志中数据未脱敏

可能原因:

  1. 没有使用 @JsonSerialize 注解
  2. 脱敏类型配置错误
  3. 序列化器未被注册

解决方案:

java
// 1. 在实体类字段上添加注解
public class User {
    @JsonSerialize(using = DesensitizationSerializer.class, 
                   type = DesensitizationType.PHONE)
    private String phone;
    
    @JsonSerialize(using = DesensitizationSerializer.class,
                   type = DesensitizationType.EMAIL)
    private String email;
}

// 2. 配置脱敏类型
public enum DesensitizationType {
    PHONE,    // 手机号
    EMAIL,    // 邮箱
    ID_CARD,  // 身份证号
    BANK_CARD,// 银行卡号
    NAME      // 姓名
}

// 3. 确保序列化器被正确注册
@Configuration
public class BeyondSoftLogAutoConfiguration {
    
    @Bean
    public Module desensitizationModule() {
        SimpleModule module = new SimpleModule();
        module.addSerializer(String.class, new DesensitizationSerializer());
        return module;
    }
}

7.5 日志查询超时

问题描述: 查询大量历史日志时出现超时

可能原因:

  1. MongoDB 没有创建索引
  2. 查询条件没有命中索引
  3. 数据量过大导致查询缓慢

解决方案:

java
// 1. 确保创建了必要的索引
@Document(collection = "beyond_soft_data_audit_log")
public class BeyondSoftDataAuditLog {
    @Indexed
    private Long createTime;
    
    @Indexed
    private String loginName;
    
    @Indexed
    private String handleName;
    
    @CompoundIndex(name = "time_user_handle", 
                   def = "{'createTime': -1, 'loginName': 1}")
    private CompositeIndex;
}

// 2. 优化查询条件
public List<BeyondSoftDataAuditLog> findAuditLogs(DataAuditLogQuery query) {
    Query mongoQuery = new Query();
    
    // 按时间范围查询(使用索引)
    if (query.getStartTime() != null && query.getEndTime() != null) {
        mongoQuery.addCriteria(Criteria.where("createTime")
            .gte(query.getStartTime())
            .lte(query.getEndTime()));
    }
    
    // 按用户名查询(使用索引)
    if (StringUtils.isNotBlank(query.getLoginName())) {
        mongoQuery.addCriteria(Criteria.where("loginName").is(query.getLoginName()));
    }
    
    // 使用分页查询
    mongoQuery.with(PageRequest.of(query.getPageNum(), query.getPageSize()));
    mongoQuery.with(Sort.by(Sort.Direction.DESC, "createTime"));
    
    return mongoTemplate.find(mongoQuery, BeyondSoftDataAuditLog.class);
}

8. 性能优化建议

8.1 异步处理

  • 使用 @EnableAsync 启用异步支持
  • 配置线程池大小
  • 使用异步保存日志
java
@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean("logExecutor")
    public Executor logExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Log-Async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

8.2 批量写入

  • 使用 MongoDB 批量插入 API
  • 配置批量写入阈值
  • 定期刷新日志
java
@Service
public class AuditLogServiceImpl implements BeyondSoftLogService {
    
    private static final int BATCH_SIZE = 100;
    
    @Override
    public void saveDataAuditLog(BeyondSoftDataAuditLog log) {
        auditLogBuffer.add(log);
        if (auditLogBuffer.size() >= BATCH_SIZE) {
            flushLogs();
        }
    }
    
    @Async("logExecutor")
    public void flushLogs() {
        if (!auditLogBuffer.isEmpty()) {
            mongoTemplate.insert(auditLogBuffer, BeyondSoftDataAuditLog.class);
            auditLogBuffer.clear();
        }
    }
}

8.3 索引优化

  • 根据查询频率创建索引
  • 使用复合索引优化多条件查询
  • 定期分析索引使用情况
java
@Document(collection = "beyond_soft_data_audit_log")
public class BeyondSoftDataAuditLog {
    @Id
    private String id;
    
    @Indexed
    private Long createTime;
    
    @Indexed
    private String loginName;
    
    @Indexed
    private String handleName;
    
    @Indexed
    private OperationType operationType;
    
    @CompoundIndex(name = "idx_time_operation", 
                   def = "{'createTime': -1, 'operationType': 1}")
    private CompositeIndex timeOperation;
}

8.4 数据归档

  • 定期归档历史日志
  • 使用 TTL 自动过期
  • 分表存储大量数据
java
@Configuration
public class MongoConfig {
    
    @Bean
    public MongoTemplate mongoTemplate() {
        MongoDatabaseFactory factory = new SimpleMongoClientDatabaseFactory(
            mongoClient, "beyondsoft_logs");
        return new MongoTemplate(factory);
    }
}

// 配置 TTL 索引自动过期(30天后删除)
@Document(collection = "beyond_soft_request_log")
public class BeyondSoftRequestLog {
    @Id
    private String id;
    
    @Indexed(expireAfter = "30d")
    private Long createTime;
}

9. 版本日志

1.0.0 (2024-01-01)

  • 初始版本发布
  • 支持请求日志、登录日志、审计日志
  • 支持 MongoDB 存储
  • 支持数据脱敏
  • 支持异步保存
  • 支持批量操作
  • 支持字段对比
  • 支持自定义查询方法

1.1.0 (2024-02-01)

  • 新增文件存储支持
  • 新增多租户支持
  • 优化性能
  • 修复已知问题
  • 完善文档

1.2.0 (2024-03-01)

  • 新增日志查询 API
  • 新增差异对比 API
  • 新增自定义脱敏规则
  • 优化批量操作性能
  • 修复并发问题
  • 完善单元测试

Copyright © 2025-present | 网站备案号:豫ICP备19038229号-1