beyondsoft-log-spring3-starter 功能概览
1. 模块定位
beyondsoft-log-spring3-starter 提供统一的 操作日志、登录日志、请求日志 能力,基于注解 + AOP + Filter 自动采集关键操作行为和请求信息,通常结合 MongoDB 等存储实现日志追踪与审计。
2. 主要能力
审计日志(操作日志)
- 通过注解(如
@AuditLog)对关键业务操作进行审计记录。 - 支持记录操作类型、操作对象、前后数据变化、业务主键等信息。
- 支持基于ID的默认查询和自定义查询方法,满足复杂业务场景(如按月份、年份等条件批量更新)。
- 通过注解(如
登录日志
- 通过注解(如
@LoginLog)记录登录行为,包括用户信息、IP、地理位置、浏览器等。 - 支持登录成功/失败状态记录,并存储登录响应信息。
- 通过注解(如
请求日志
- 通过请求日志过滤器(如
RequestLogFilter)自动记录 HTTP 请求和响应摘要。 - 记录请求路径、方法、参数、响应结果概要以及耗时、TraceId 等信息。
- 通过请求日志过滤器(如
统一日志模型与配置
- 提供统一的数据模型(如
BeyondSoftLoginLog,BeyondSoftRequestLog,BeyondSoftDataAuditLog)。 - 提供配置属性类(如
BeyondSoftLogProperties)和自动配置类(如BeyondSoftLogAutoConfig),支持通过application.yml调整日志行为。
- 提供统一的数据模型(如
3. 使用约定
优先使用注解采集日志,而不是手写“插表”
- 对需要审计的业务操作,优先使用
@AuditLog注解,由框架自动采集并入库。 - 登录接口使用
@LoginLog,避免在 Controller 中手动编写登录日志写入逻辑。
- 对需要审计的业务操作,优先使用
请求日志统一通过框架 Filter 处理
- 全局请求访问日志统一由本 Starter 的 Filter 记录,不再在每个 Controller 中手工打印请求/响应日志。
日志模型与存储表结构尽量保持统一
- 业务项目如需扩展日志数据结构,建议在现有日志模型基础上扩展和兼容,而不是另起一套完全不同的日志表和模型,保持跨项目可观测性一致。
与 Web / 安全 / 异常模块协同使用
- 搭配 Web Starter:可通过统一响应和拦截器获得更丰富的请求上下文信息。
- 搭配 安全 Starter:可记录安全相关请求、加解密接口调用等行为。
- 搭配 异常 Starter:可统一记录发生异常时的请求上下文与错误信息,提升问题排查效率。
4. AuditLog 完整使用指南
4.1 注解属性完整参考
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
handleName | String | "" | 操作名称,如 "创建用户"、"批量更新发票" |
operationType | OperationType | NONE | 操作类型:INSERT、UPDATE、DELETE、QUERY、NONE(自动判断) |
serviceClass | Class<?> | Class.class | 必填:Service类,用于查询数据 |
needDefaultCompare | boolean | false | 是否需要默认的数据变更对比 |
idType | Class<?> | Integer.class | ID字段类型(Long、Integer、String等) |
primaryKey | String | "id" | 实体对象的主键字段名 |
batchIdParam | String | "ids" | 批量操作时,请求对象中ID列表的字段名 |
customQueryMethod | String | "" | 自定义查询方法名(用于非ID查询场景) |
customQueryParamField | String | "" | 自定义查询方法的参数字段名(支持字段提取) |
loginName | String | CommonConstants.USERNAME | 登录日志专用:当前登录账号的key值 |
4.2 默认ID查询用法(无需自定义方法)
场景1:单个ID操作
直接传入ID
java
@PostMapping("/delete/{id}")
@AuditLog(
handleName = "删除用户",
operationType = OperationType.DELETE,
serviceClass = UserService.class,
idType = Long.class // ID类型
)
public Result deleteUser(@PathVariable Long id) {
userService.removeById(id);
return Result.success();
}从实体对象中提取ID
java
@PostMapping("/update")
@AuditLog(
handleName = "更新用户",
operationType = OperationType.UPDATE,
serviceClass = UserService.class,
idType = Long.class,
primaryKey = "userId" // 实体中的主键字段名(如果不是"id")
)
public Result updateUser(@RequestBody User user) {
userService.updateById(user);
return Result.success();
}从请求对象中提取ID
java
public class UpdateUserRequest {
private Long userId;
private String name;
// getters and setters
}
@PostMapping("/updateUser")
@AuditLog(
handleName = "更新用户信息",
operationType = OperationType.UPDATE,
serviceClass = UserService.class,
idType = Long.class,
primaryKey = "userId" // 从request中提取userId字段作为ID
)
public Result updateUser(@RequestBody UpdateUserRequest request) {
userService.updateUser(request);
return Result.success();
}场景2:批量ID操作
直接传入ID列表
java
@PostMapping("/batchDelete")
@AuditLog(
handleName = "批量删除用户",
operationType = OperationType.DELETE,
serviceClass = UserService.class,
idType = Long.class
)
public Result batchDelete(@RequestBody List<Long> ids) {
userService.removeByIds(ids);
return Result.success();
}从请求对象中提取ID列表
java
public class BatchDeleteRequest {
private List<Long> userIds; // ID列表字段
private String reason;
// getters and setters
}
@PostMapping("/batchDeleteUsers")
@AuditLog(
handleName = "批量删除用户",
operationType = OperationType.DELETE,
serviceClass = UserService.class,
idType = Long.class,
batchIdParam = "userIds" // 指定ID列表字段名
)
public Result batchDelete(@RequestBody BatchDeleteRequest request) {
userService.removeByIds(request.getUserIds());
return Result.success();
}场景3:不同ID类型示例
String类型ID
java
@AuditLog(
handleName = "删除订单",
operationType = OperationType.DELETE,
serviceClass = OrderService.class,
idType = String.class, // String类型ID
primaryKey = "orderNo" // 字段名
)
public Result deleteOrder(@RequestBody Order order) {
orderService.removeById(order.getOrderNo());
return Result.success();
}4.3 自定义查询方法(复杂业务场景)
当操作不是基于ID,而是基于其他业务条件时,使用自定义查询方法。
配置规则总结
| 业务参数类型 | 查询方法参数类型 | customQueryParamField配置 | 说明 |
|---|---|---|---|
| 简单类型(String、Integer等) | 相同类型 | 不配置 | 直接传递 |
| 复杂对象 | 相同类型 | 不配置 | 直接传递整个对象 |
| 复杂对象 | 简单类型 | 配置字段名 | 从对象中提取字段值 |
| 复杂对象 | 简单类型 | 配置嵌套字段 | 支持 user.id 格式 |
4.4 使用示例
场景1:按月份批量更新发票数据(参数类型一致)
业务需求:传入月份字符串(如 2025-12),批量更新该月的所有发票数据。
Controller 层
java
@RestController
@RequestMapping("/api/invoice")
public class InvoiceController {
@Autowired
private InvoiceService invoiceService;
/**
* 按月份批量更新发票状态
* @param month 月份,格式:2025-12
*/
@PostMapping("/updateByMonth")
@AuditLog(
handleName = "批量更新发票",
operationType = OperationType.UPDATE,
serviceClass = InvoiceService.class,
customQueryMethod = "queryByMonth" // 指定自定义查询方法
)
public Result updateByMonth(@RequestParam String month) {
invoiceService.updateStatusByMonth(month, "PROCESSED");
return Result.success();
}
}Service 层
java
@Service
public class InvoiceService extends ServiceImpl<InvoiceMapper, Invoice> implements IService<Invoice> {
/**
* 自定义查询方法:根据月份查询发票列表
* 方法签名:Object methodName(Object arg)
*/
public List<Invoice> queryByMonth(String month) {
return this.list(
QueryWrapper.create()
.where(INVOICE.INVOICE_MONTH.eq(month))
);
}
/**
* 业务方法:批量更新发票状态
*/
public void updateStatusByMonth(String month, String status) {
this.update(
UpdateWrapper.create()
.set(INVOICE.STATUS, status)
.where(INVOICE.INVOICE_MONTH.eq(month))
);
}
}场景2:按复杂条件批量更新(参数类型一致)
请求对象
java
public class BatchUpdateRequest {
private String year;
private String category;
private String status;
// getters and setters
}Controller
java
@PostMapping("/batchUpdate")
@AuditLog(
handleName = "批量更新数据",
operationType = OperationType.UPDATE,
serviceClass = DataService.class,
customQueryMethod = "queryByCondition" // 自定义查询方法
)
public Result batchUpdate(@RequestBody BatchUpdateRequest request) {
dataService.batchUpdate(request);
return Result.success();
}Service
java
@Service
public class DataService extends ServiceImpl<DataMapper, Data> implements IService<Data> {
/**
* 自定义查询:根据复杂条件查询
*/
public List<Data> queryByCondition(BatchUpdateRequest request) {
return this.list(
QueryWrapper.create()
.where(DATA.YEAR.eq(request.getYear()))
.and(DATA.CATEGORY.eq(request.getCategory()))
.and(DATA.STATUS.eq(request.getStatus()))
);
}
public void batchUpdate(BatchUpdateRequest request) {
// 批量更新逻辑
}
}场景3:参数类型不一致(需要字段提取)
这是最常见的场景:业务方法接收一个复杂请求对象,但查询方法只需要其中的某个字段。
请求对象
java
public class ChangePositionRequest {
private Long userId;
private String newPositionGroup;
private LocalDate effectiveDate;
// getters and setters
}Controller
java
@PostMapping("/changePosition")
@AuditLog(
handleName = "换岗位",
operationType = OperationType.UPDATE,
serviceClass = SysUserPositionGroupService.class,
customQueryMethod = "getDataByGroup",
customQueryParamField = "newPositionGroup" // 从request中提取newPositionGroup字段
)
public boolean changePosition(@RequestBody ChangePositionRequest request) {
return sysUserPositionGroupService.changePosition(
request.getUserId(),
request.getNewPositionGroup(),
request.getEffectiveDate()
);
}Service
java
@Service
public class SysUserPositionGroupService extends ServiceImpl<...> implements IService<...> {
/**
* 自定义查询:根据岗位组查询数据
* 注意:参数类型是String,不是ChangePositionRequest
*/
public List<SysUserPositionGroupEntity> getDataByGroup(String positionGroup) {
return this.list(
QueryWrapper.create()
.where(SysUserPositionGroupEntity::getPositionGroup)
.eq(positionGroup)
);
}
public boolean changePosition(Long userId, String newPositionGroup, LocalDate effectiveDate) {
// 业务逻辑
}
}工作流程:
- Controller 接收
ChangePositionRequest对象 - 框架从 request 中提取
newPositionGroup字段(值为 String) - 将提取的字段值传递给
getDataByGroup(String)方法 - 查询到旧数据用于审计日志
场景4:嵌套字段提取
支持嵌套字段提取,使用 . 分隔符:
java
@AuditLog(
customQueryMethod = "queryByUserId",
customQueryParamField = "user.id" // 支持嵌套字段
)
public Result doSomething(@RequestBody ComplexRequest request) {
// request.user.id 会被提取并传递给查询方法
}4.5 注意事项
- 方法签名要求:自定义查询方法必须是单参数方法,签名为
Object methodName(Object arg) - 参数传递规则:
- 未配置
customQueryParamField:整个业务方法的第一个参数会传递给查询方法 - 已配置
customQueryParamField:从业务参数中提取指定字段值后传递
- 未配置
- 字段提取:
- 支持简单字段:
"fieldName" - 支持嵌套字段:
"user.id"或"request.data.name" - 字段不存在会抛出异常
- 支持简单字段:
- 返回值:可以返回单个对象或对象列表(
List) - 兼容性:如果未指定
customQueryMethod,将使用默认的 ID 查询逻辑 - 错误处理:如果自定义方法不存在、参数类型不匹配或字段提取失败,会抛出详细的异常信息
4.6 配置对比
| 场景 | customQueryMethod | customQueryParamField | 说明 |
|---|---|---|---|
| 参数类型完全一致 | "queryByMonth" | 不配置(空字符串) | 直接传递整个参数对象 |
| 需要提取单个字段 | "getDataByGroup" | "newPositionGroup" | 从参数对象中提取指定字段 |
| 需要提取嵌套字段 | "queryByUserId" | "user.id" | 支持多层嵌套提取 |
4.7 对比:传统方式 vs 自定义查询
| 特性 | 传统ID查询 | 自定义查询方法 |
|---|---|---|
| 适用场景 | 单个/批量 ID 操作 | 任意复杂条件查询 |
| 配置方式 | primaryKey, batchIdParam | customQueryMethod |
| 查询灵活性 | 仅支持 ID | 支持任意查询条件 |
| 实现复杂度 | 低(框架自动处理) | 中(需实现查询方法) |
| 性能 | 高(索引查询) | 取决于查询条件 |
4.8 最佳实践
- 优先使用默认 ID 查询:如果业务操作基于 ID,使用默认方式即可(见 4.2 章节)
- 复杂场景使用自定义查询:当操作基于其他条件时,使用
customQueryMethod(见 4.3-4.4 章节) - 查询方法命名规范:建议使用
queryBy前缀,如queryByMonth、queryByCondition - 字段提取灵活运用:
- 参数类型一致时,不配置
customQueryParamField - 需要提取字段时,使用
customQueryParamField - 支持嵌套字段提取,使用
.分隔符
- 参数类型一致时,不配置
- 性能考虑:自定义查询方法中应添加适当的索引,避免全表扫描
- 日志监控:框架会记录自定义方法的调用日志,便于排查问题
4.9 完整配置示例速查表
ID查询配置
java
// 单个ID - 直接传入
@AuditLog(serviceClass = UserService.class, idType = Long.class)
public Result delete(@PathVariable Long id) { ... }
// 单个ID - 从对象提取
@AuditLog(serviceClass = UserService.class, idType = Long.class, primaryKey = "userId")
public Result update(@RequestBody User user) { ... }
// 批量ID - 直接传入列表
@AuditLog(serviceClass = UserService.class, idType = Long.class)
public Result batchDelete(@RequestBody List<Long> ids) { ... }
// 批量ID - 从对象提取
@AuditLog(serviceClass = UserService.class, idType = Long.class, batchIdParam = "userIds")
public Result batchDelete(@RequestBody BatchRequest request) { ... }自定义查询配置
java
// 参数类型一致 - 直接传递
@AuditLog(serviceClass = InvoiceService.class, customQueryMethod = "queryByMonth")
public Result update(@RequestParam String month) { ... }
// 参数类型不一致 - 字段提取
@AuditLog(
serviceClass = UserService.class,
customQueryMethod = "getDataByGroup",
customQueryParamField = "positionGroup"
)
public Result change(@RequestBody ChangeRequest request) { ... }
// 嵌套字段提取
@AuditLog(
serviceClass = OrderService.class,
customQueryMethod = "queryByUserId",
customQueryParamField = "user.id"
)
public Result process(@RequestBody ComplexRequest request) { ... }4.10 常见问题FAQ
Q1: 什么时候用默认ID查询,什么时候用自定义查询?
- A: 如果操作基于ID(单个或批量),用默认ID查询;如果基于其他业务条件(月份、状态等),用自定义查询。
Q2: primaryKey 和 batchIdParam 有什么区别?
- A:
primaryKey用于单个对象场景,指定对象中的主键字段名;batchIdParam用于批量场景,指定对象中ID列表的字段名。
Q3: customQueryParamField 什么时候需要配置?
- A: 当自定义查询方法的参数类型与业务方法参数类型不一致时需要配置,用于从业务参数中提取特定字段。
Q4: 自定义查询方法可以有多个参数吗?
- A: 当前设计为单参数方法。如果需要多个参数,建议封装成一个对象或使用Map。
Q5: 支持哪些ID类型?
- A: 支持所有实现了
Serializable接口的类型,常见的有 Long、Integer、String 等。
Q6: 如果查询方法不存在会怎样?
- A: 会抛出
IllegalArgumentException,异常信息会明确指出缺少的方法名和期望的参数类型。