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_LOG、REQUEST_LOG、AUDIT_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