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 注解使用关键点
serviceClass 是必填项(INSERT 操作除外)
- UPDATE/DELETE 操作必须指定 serviceClass
- serviceClass 必须实现 MyBatis-Flex 的 IService 接口
- 框架通过 serviceClass 调用 getById() 或 listByIds() 查询数据
默认查询机制
- 框架会从方法参数中提取 primaryKey 字段的值作为ID
- 调用 serviceClass.getById(id) 查询单个对象
- 调用 serviceClass.listByIds(ids) 查询批量对象
自定义查询方法
- 通过 customQueryMethod 指定 Service 中的自定义方法
- 自定义方法必须接收一个参数
- 默认传入整个 request 对象
- 可通过 customQueryParamField 从 request 中提取特定字段
操作类型说明
- INSERT:直接记录方法参数为 newObject,不查询 oldObject
- UPDATE:执行前查询 oldObject,执行后查询 newObject
- DELETE:执行前查询 oldObject,不查询 newObject
- QUERY:不建议使用,纯查询操作建议使用 INSERT 类型
primaryKey 的作用
- 从方法参数中提取此字段的值作为ID传给查询方法
- 从查询结果中提取此字段的值作为 businessId
- 默认值为 "id"
3.2 工作流程
@Before 阶段:
- 初始化日志对象,记录操作人、IP、时间等信息
- 调用 AuditParser.getOldResult() 查询旧数据
方法执行:
- 执行实际的业务操作
@After 阶段:
- 调用 AuditParser.getNewResult() 查询新数据
- 如果 needDefaultCompare=true,生成字段对比结果
- 调用 BeyondSoftLogService.saveBatch() 保存日志
3.3 注意事项
- Service 必须实现 IService 接口
- INSERT 操作不需要 serviceClass
- needDefaultCompare 只对 UPDATE 操作有效
- 批量操作使用 batchIdParam 指定ID列表字段
- 日志保存到 MongoDB,需要配置 MongoDB 连接
4. 核心组件详解
3.1 日志注解
3.1.1 @AuditLog - 审计日志注解
使用说明:
- 标记在方法上,用于记录审计日志
- 支持多种参数组合,满足不同业务场景
- 可自定义操作名称、类型、主键字段等
注解参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| handleName | String | "" | 具体业务操作名称 |
| operationType | OperationType | NONE | 操作类型(INSERT/UPDATE/DELETE/QUERY) |
| serviceClass | Class | Class.class | 查询数据库所调用的class文件 |
| needDefaultCompare | boolean | false | 是否需要默认的改动比较 |
| idType | Class | Integer.class | ID的类型 |
| primaryKey | String | "id" | 主键字段名 |
| batchIdParam | String | "ids" | 批量id名称 |
| customQueryMethod | String | "" | 自定义查询方法名称 |
| customQueryParamField | String | "" | 自定义查询方法的参数字段名 |
| loginName | String | USERNAME | 登录账号的key值 |
完整示例代码 - 展示Controller层使用@AuditLog注解的各种组合场景:
@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、浏览器等信息
示例代码:
@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注解的方法 - 自动提取方法参数中的新旧数据
- 生成详细的变更对比信息
- 支持批量操作的日志记录
工作流程:
- 方法执行前(@Before):记录方法入参,查询旧数据
- 方法执行:执行实际的业务操作
- 方法执行后(@After):查询新数据,生成对比结果
- 日志保存:调用 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 - 请求日志模型
@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 - 登录日志模型
@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 - 审计日志模型
@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枚举定义脱敏策略
使用示例:
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/目录下
配置文件格式:
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: 完全脱敏
工作原理:
YmlUtils加载并解析logback-desensitize.yml配置文件DesensitizationAppender在 Logback Appender 层拦截日志事件DesensitizationUtil.customChange()使用正则表达式匹配敏感字段- 根据配置的脱敏规则对匹配到的值进行脱敏处理
- 将脱敏后的日志输出到控制台或文件
3.6 日志服务接口
3.6.1 BeyondSoftLogService - 日志服务接口
核心方法:
| 方法名 | 参数 | 返回值 | 说明 |
|---|---|---|---|
| saveRequestLog | BeyondSoftRequestLog | void | 保存请求日志 |
| saveLoginLog | BeyondSoftLoginLog | void | 保存登录日志 |
| saveDataAuditLog | BeyondSoftDataAuditLog | void | 保存审计日志 |
| findRequestLogs | RequestLogQuery | List<BeyondSoftRequestLog> | 查询请求日志 |
| findLoginLogs | LoginLogQuery | List<BeyondSoftLoginLog> | 查询登录日志 |
| findDataAuditLogs | DataAuditLogQuery | List<BeyondSoftDataAuditLog> | 查询审计日志 |
3.7 日志查询控制器
3.7.1 BeyondSoftLogQueryController - 统一日志查询接口
核心功能:
- 提供统一的日志查询REST接口
- 通过
doc参数区分不同类型的日志 - 支持灵活的条件查询和分页
- 基础路径可通过配置自定义
API接口:
| 请求方法 | 路径 | 说明 |
|---|---|---|
| GET | $ | 统一日志查询接口 |
查询参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| doc | DocumentEnums | 是 | 日志类型: LOGIN_LOG、REQUEST_LOG、AUDIT_LOG |
| pageNum | Integer | 否 | 页码,默认值: 1 |
| pageSize | Integer | 否 | 每页条数,默认值: 10 |
| 其他参数 | String | 否 | 动态查询条件,根据日志类型不同而不同 |
支持的日志类型 (DocumentEnums):
| 枚举值 | queryName | documentName | 对应实体类 |
|---|---|---|---|
| LOGIN_LOG | loginLog | BeyondSoftLoginLog | BeyondSoftLoginLog |
| REQUEST_LOG | requestLog | BeyondSoftRequestLog | BeyondSoftRequestLog |
| AUDIT_LOG | auditLog | BeyondSoftDataAuditLog | BeyondSoftDataAuditLog |
配置项:
beyond-soft:
web:
log-uri: /api/log # 日志查询接口基础路径,默认: /api/log使用示例:
# 查询登录日志列表
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"返回结果:
{
"total": 100,
"pageNum": 1,
"pageSize": 10,
"pages": 10,
"records": [
{
// 日志记录字段,根据doc类型不同而不同
}
]
}4. 核心功能详解
4.1 请求日志记录
核心流程:
- RequestLogFilter 拦截所有请求(继承 OncePerRequestFilter)
- 使用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 包装请求和响应
- 记录请求信息:IP、地理位置、请求方法、URL、请求参数、User-Agent
- 执行过滤器链,处理实际请求
- 记录响应信息:响应状态、响应体、耗时
- 特殊处理:
- 文件下载:记录文件名和文件大小,不记录响应体内容
- 认证路径:匹配 authResourcePath 或 unAuthResourcePath 的请求不记录响应体
- 同步保存请求日志到 MongoDB(通过 BeyondSoftLogService.save())
- copyBodyToResponse() 复制响应体给客户端
过滤规则:
- 跳过日志查询接口:logUri 配置的路径
- 跳过 SSE(Server-Sent Events)请求
- 支持 multipart/form-data 请求处理
- 响应内容截断保护(MAX_LOG_RESPONSE_LENGTH = 1024)
配置选项:
logTypes: 日志存储类型(如 "mongodb")authResourcePath: 认证资源路径模式unAuthResourcePath: 非认证资源路径模式logUri: 日志查询接口路径
4.2 登录日志记录
核心流程:
- LoginAspect 拦截 @LoginLog 注解方法
- @Before 阶段:
- 从 HttpServletRequest 获取 IP 地址和地理位置
- 解析 User-Agent 获取浏览器、操作系统、平台信息
- 根据 operationType 获取用户名:
- LOGIN:从请求参数或请求体中提取(根据 Content-Type)
- LOGOUT:从 StpUtil.getExtra() 获取
- 创建 BeyondSoftLoginLog 对象
- @After 阶段:
- 设置 requestTime 为当前时间
- 同步保存登录日志到 MongoDB(通过 BeyondSoftLogService.save())
- @AfterThrowing 阶段:
- 设置 status 为失败状态
注解参数:
operationType: 操作类型(OperationType.LOGIN / OperationType.LOGOUT)value: 用户名字段名,默认 "username"
4.3 审计日志记录
核心流程:
- AuditLogAspect 拦截 @AuditLog 注解方法
- 解析注解参数,获取操作类型、主键等配置
- 方法执行前:通过 AuditParser.getOldResult() 查询修改前的旧数据
- INSERT操作:直接返回方法参数作为 newObject
- UPDATE/DELETE操作:调用 serviceClass 的查询方法获取 oldObject
- 执行目标方法
- 方法执行后:通过 AuditParser.getNewResult() 查询修改后的新数据
- UPDATE操作:再次调用查询方法获取 newObject
- 如果 needDefaultCompare=true,生成差异对比内容
- 调用 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
- 姓名:张*
配置方式:
beyondsoft:
log:
desensitization:
enabled: true
types:
- phone
- email
- idCard5. 配置项说明
5.1 核心配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| beyond-soft.log.enabled | boolean | true | 是否启用日志模块 |
| beyond-soft.log.showSql | boolean | true | 是否显示SQL日志 |
| beyond-soft.log.showResp | boolean | true | 是否显示响应日志 |
| beyond-soft.log.logTypes | List<String> | ["file"] | 日志存储类型(file, mongodb) |
5.2 MongoDB 配置
当 logTypes 包含 "mongodb" 时需要配置 MongoDB 连接
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| spring.data.mongodb.host | String | localhost | MongoDB主机地址 |
| spring.data.mongodb.port | int | 27017 | MongoDB端口 |
| spring.data.mongodb.database | String | test | 数据库名称 |
| spring.data.mongodb.username | String | 用户名 | |
| spring.data.mongodb.password | String | 密码 |
5.3 配置示例
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: admin1236. 高级用法
6.1 自定义查询方法
在某些复杂场景下,需要使用 Service 中的自定义查询方法:
@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列表字段:
@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 开启字段变更对比
当需要记录详细的字段变更信息时:
@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 为空
可能原因:
- 没有配置 serviceClass(UPDATE/DELETE 操作必须配置)
- serviceClass 没有实现 MyBatis-Flex 的 IService 接口
- 自定义查询方法返回 null
- 主键字段名不匹配
解决方案:
// 方案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 时,字段对比结果与预期不符
可能原因:
- serviceClass 没有指定
- 查询方法返回的数据不一致
- 数据被其他事务修改
- 字段类型不一致
解决方案:
// 确保指定 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 批量操作性能问题
问题描述: 批量操作时审计日志记录耗时较长
可能原因:
- 批量查询数据量大
- MongoDB 连接池配置不当
- 开启了 needDefaultCompare 导致大量对比计算
解决方案:
// 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) // 关闭对比# 3. 调整 MongoDB 连接池
spring:
data:
mongodb:
uri: mongodb://localhost:27017/beyondsoft_logs
auto-index-creation: true7.4 脱敏规则不生效
问题描述: 配置了脱敏规则,但日志中数据未脱敏
可能原因:
- 没有使用 @JsonSerialize 注解
- 脱敏类型配置错误
- 序列化器未被注册
解决方案:
// 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 日志查询超时
问题描述: 查询大量历史日志时出现超时
可能原因:
- MongoDB 没有创建索引
- 查询条件没有命中索引
- 数据量过大导致查询缓慢
解决方案:
// 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启用异步支持 - 配置线程池大小
- 使用异步保存日志
@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
- 配置批量写入阈值
- 定期刷新日志
@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 索引优化
- 根据查询频率创建索引
- 使用复合索引优化多条件查询
- 定期分析索引使用情况
@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 自动过期
- 分表存储大量数据
@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
- 新增自定义脱敏规则
- 优化批量操作性能
- 修复并发问题
- 完善单元测试