基于注解的业务日志记录框架mzt-biz-log及其多场景实践
引言
在企业级应用开发中,操作日志是确保系统可追溯性、满足合规性要求和支持安全审计的关键组成部分。传统的日志记录方式往往与业务逻辑代码高度耦合,导致维护成本高昂且扩展性差。
mzt-biz-log 是一个专为Spring Boot应用设计的、基于注解的通用业务操作日志组件。它以非侵入的方式,智能地记录"谁在何时、何地、对什么对象做了什么操作"的详细信息。通过简单的注解配置,开发者可以轻松地为Java应用集成一套全面的操作日志解决方案,尤其适用于用户管理、订单跟踪和权限审计等核心业务场景。
为何选择mzt-biz-log?
mzt-biz-log 凭借其独特优势,成为构建高效、可靠操作日志系统的理想选择:
- 零侵入性设计:只需在业务方法上添加注解,无需修改现有业务逻辑代码,保持代码的整洁与核心业务的专注。
- 强大的SpEL表达式支持:利用Spring Expression Language (SpEL) 的能力,灵活构建日志消息模板,可直接引用方法参数、返回值以及异常信息等上下文数据。
- 智能对象差异对比:内置对象字段对比功能,能自动识别并记录实体对象前后状态的变化,生成清晰的差异日志,便于追踪数据变更历史。
- 多租户支持:内置多租户机制,能够轻松实现不同业务单元或租户之间的日志隔离与管理。
- 高度可扩展性:支持自定义函数扩展,例如将ID转换为对应的名称、将状态码解析为易懂的文字描述,进一步丰富日志信息。
用户管理场景:精细化操作追踪
在用户管理系统中,对管理员操作用户信息的每一次修改进行记录至关重要。mzt-biz-log 通过 @LogRecord 注解,能够轻松捕获这些关键操作。
用户实体配置示例
以下 SystemUser 实体展示了如何通过注解配置需要记录的字段,以及如何忽略特定字段或自定义字段显示方式:
import lombok.Data;
import com.mzt.logapi.starter.annotation.DiffLogAllFields;
import com.mzt.logapi.starter.annotation.DiffLogField;
import com.mzt.logapi.starter.annotation.DiffLogIgnore;
@Data
@DiffLogAllFields // 自动记录所有字段的变化
public class SystemUser {
private Long id;
private String userName; // 用户名
@DiffLogIgnore // 忽略电子邮件字段的日志记录
private String email;
@DiffLogField(name = "用户状态", function = "USER_STATUS_PARSE") // 自定义显示名称和转换函数
private Integer status; // 状态码(例如:1-激活,0-禁用)
private UserAddress address; // 嵌套对象,支持自动对比
}
@Data
class UserAddress { // 嵌套地址类
private String city;
private String street;
}
用户信息更新日志记录
当管理员修改用户信息时,可在方法上添加 @LogRecord 注解,系统将自动生成详细的日志:
import com.mzt.logapi.starter.annotation.LogRecord;
import org.springframework.stereotype.Service;
@Service
public class SystemUserService {
// 假设 userRepository 是一个数据访问层接口
private final UserRepository userRepository;
public SystemUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@LogRecord(
success = "操作员{{#initiator}}更新了用户{USER{#updatedUser.id}}的信息:{_DIFF{#previousUser, #updatedUser}}",
fail = "操作员{{#initiator}}更新用户{USER{#updatedUser.id}}失败,错误:{{#_errorMsg}}",
type = "SYSTEM_USER_MANAGEMENT",
bizNo = "{{#updatedUser.id}}",
extra = "{{#updatedUser.toString()}}"
)
public boolean modifyUserDetails(SystemUser previousUser, SystemUser updatedUser, String initiator) {
// 核心业务逻辑:执行用户数据的更新操作
return userRepository.update(updatedUser);
}
}
当上述方法被调用,例如管理员王五修改了用户张三的信息时,日志系统可能输出如下信息:
"操作员王五更新了用户张三(ID:1002)的信息:【用户名】从【张三】修改为【李四】;【用户状态】从【激活】修改为【禁用】;【地址.城市】从【北京】修改为【上海】"
订单跟踪场景:全生命周期监控
在电商等业务系统中,订单操作频繁且流程复杂。mzt-biz-log 提供了全面的订单操作跟踪能力,覆盖订单的创建、状态变更等关键环节。
订单创建日志
以下示例展示了如何在订单创建方法中记录详细的操作日志:
import com.mzt.logapi.starter.annotation.LogRecord;
import java.math.BigDecimal; // 引入BigDecimal
@Service
public class SalesOrderService {
private final SalesOrderRepository salesOrderRepository; // 假设数据访问接口
public SalesOrderService(SalesOrderRepository salesOrderRepository) {
this.salesOrderRepository = salesOrderRepository;
}
@LogRecord(
success = "客户{{#salesOrder.customerName}}创建了订单编号为{{#salesOrder.orderId}}的新订单,包含商品:{{#salesOrder.productName}},总金额:{{#salesOrder.totalAmount}}元。",
fail = "客户{{#salesOrder.customerName}}创建订单失败:{{#_errorMsg}}",
type = "ORDER_CREATION",
bizNo = "{{#salesOrder.orderId}}"
)
public boolean placeNewOrder(SalesOrder salesOrder) {
// 订单创建的核心业务逻辑,保存订单数据
return salesOrderRepository.save(salesOrder);
}
}
// 假设 SalesOrder 实体定义如下
@Data
class SalesOrder {
private String orderId;
private String customerName;
private String productName;
private BigDecimal totalAmount;
private String status;
}
订单状态变更追踪
针对订单状态的流转,mzt-biz-log 允许通过 subType 实现不同角色或视图的日志记录,提供差异化的信息展示:
import com.mzt.logapi.starter.annotation.LogRecord;
import com.mzt.logapi.starter.enums.LogRecordType; // 假设此枚举存在
@Service
public class SalesOrderLifecycleService {
private final SalesOrderRepository salesOrderRepository;
public SalesOrderLifecycleService(SalesOrderRepository salesOrderRepository) {
this.salesOrderRepository = salesOrderRepository;
}
// 支持多个 @LogRecord 注解,记录不同维度的日志
@LogRecord(
subType = "ADMIN_VIEW", // 面向管理员的日志视图
success = "订单【{{#orderId}}】状态由【{{#oldStatus}}】变更为【{{#newStatus}}】,操作员:{{#operator}}。",
type = LogRecordType.ORDER,
bizNo = "{{#orderId}}"
)
@LogRecord(
subType = "CUSTOMER_VIEW", // 面向用户的日志视图
success = "您的订单【{{#orderId}}】已更新,当前状态为【{{#newStatus}}】。",
type = LogRecordType.ORDER,
bizNo = "{{#orderId}}"
)
public boolean updateOrderStatus(String orderId, String oldStatus, String newStatus, String operator) {
// 订单状态更新的核心逻辑
return salesOrderRepository.updateStatus(orderId, newStatus);
}
}
通过子类型(subType)功能,可以实现多维度、精细化的订单操作链路追踪:
- 管理员视图:记录完整的操作人、新旧状态等详细信息。
- 用户视图:提供用户友好的简化提示信息,告知状态变更。
- 系统视图(可选):记录更多技术细节,便于系统运维和问题排查。
权限审计场景:安全操作监控
在权限管理或任何涉及敏感操作的系统中,对每一次权限变更和重要操作进行详细记录,是满足安全合规性要求的基础。mzt-biz-log 能够与Shiro、Spring Security等安全框架无缝集成。
登录操作审计
记录用户登录行为,包括成功与失败,以及相关的IP地址等信息:
import com.mzt.logapi.starter.annotation.LogRecord;
import org.springframework.stereotype.Service;
@Service
public class AuthenticationService {
private final SecurityService securityService; // 假设认证服务接口
public AuthenticationService(SecurityService securityService) {
this.securityService = securityService;
}
@LogRecord(
success = "账户{{#accountName}}成功登录系统,IP地址为:{{#clientIp}}。",
fail = "账户{{#accountName}}登录失败,原因:{{#_errorMsg}}。",
type = "AUTH_LOGIN_AUDIT",
bizNo = "{{#accountName}}"
)
public String userLoginAttempt(String accountName, String password, String clientIp) {
// 执行用户登录认证逻辑
return securityService.authenticate(accountName, password);
}
}
权限变更审计
记录管理员为用户分配角色的操作,确保权限变更可追溯:
import com.mzt.logapi.starter.annotation.LogRecord;
import java.util.List;
@Service
public class AuthorizationManagementService {
private final AuthorizationService authorizationService; // 假设授权服务接口
public AuthorizationManagementService(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
@LogRecord(
success = "管理员{{#adminUser}}为用户{USER{#targetUserId}}分配了角色:{ROLE{#assignedRoleIds}}。",
type = "PERMISSION_ASSIGNMENT",
bizNo = "{{#targetUserId}}"
)
public boolean configureUserRoles(Long targetUserId, List<Long> assignedRoleIds, String adminUser) {
// 核心逻辑:执行角色分配操作
return authorizationService.assignRoles(targetUserId, assignedRoleIds);
}
}
敏感操作监控
对于涉及数据修改、配置更改等敏感操作,可以配置更详细的日志记录,包括操作内容和相关参数,甚至堆栈信息:
import com.mzt.logapi.starter.annotation.LogRecord;
import java.util.Map;
@Service
public class ConfidentialOperationService {
private final ConfidentialDataService confidentialService; // 假设敏感数据服务
public ConfidentialOperationService(ConfidentialDataService confidentialService) {
this.confidentialService = confidentialService;
}
@LogRecord(
success = "【敏感操作】操作员{{#initiator}}执行了"{{#action}}",涉及参数:{{#details}}。",
type = "SENSITIVE_DATA_OPERATION",
bizNo = "{{#requestId}}", // 业务编号可绑定请求ID
extra = "{{#exceptionTrace}}" // 可选:记录异常堆栈
)
public void executeConfidentialOperation(String action, Map<String, Object> details, String initiator) {
// 敏感数据操作的核心逻辑
confidentialService.process(action, details);
}
}
高级功能与应用技巧
1. 自定义函数扩展
mzt-biz-log 允许开发者实现 IParseFunction 接口来自定义数据转换逻辑,以满足业务特定的日志显示需求。
import com.mzt.logapi.starter.function.IParseFunction;
import org.springframework.stereotype.Component;
@Component
public class UserStatusParseFunction implements IParseFunction {
@Override
public String functionName() {
return "USER_STATUS_PARSE"; // 定义函数名称,在日志模板中使用
}
@Override
public String apply(Object value) {
if (value == null) return "未知";
switch ((Integer) value) { // 假设状态码为整数
case 1: return "激活";
case 0: return "禁用";
case 2: return "锁定";
default: return "未知状态";
}
}
}
在日志模板中,可以通过 {{USER_STATUS_PARSE{#status}}} 的方式调用此自定义函数。
2. 条件日志记录
使用 condition 属性可以指定仅在特定条件满足时才记录日志,避免生成不必要的日志信息。
import com.mzt.logapi.starter.annotation.LogRecord;
import org.springframework.stereotype.Service;
@Service
public class OrderStateUpdateService {
private final SalesOrderRepository salesOrderRepository; // 假设数据访问接口
public OrderStateUpdateService(SalesOrderRepository salesOrderRepository) {
this.salesOrderRepository = salesOrderRepository;
}
@LogRecord(
success = "订单【{{#orderIdentifier}}】状态已从【{{#prevStatus}}】更新为【{{#currentStatus}}】。",
condition = "{{#prevStatus != #currentStatus}}", // 只有当状态发生变化时才记录日志
type = "ORDER_STATUS_UPDATE",
bizNo = "{{#orderIdentifier}}"
)
public boolean updateOrderState(String orderIdentifier, String prevStatus, String currentStatus) {
return salesOrderRepository.updateState(orderIdentifier, currentStatus);
}
}
3. 跨方法变量传递
通过 LogRecordContext 工具类,可以在不同方法之间传递变量,使得子方法也能访问父方法上下文中的数据,丰富日志内容。
import com.mzt.logapi.starter.annotation.LogRecord;
import com.mzt.logapi.starter.support.LogRecordContext;
import org.springframework.stereotype.Service;
@Service
public class UserCreditAndOrderProcessingService {
private final SalesOrderService salesOrderService; // 假设订单处理服务
private final CreditService creditService; // 假设积分服务
public UserCreditAndOrderProcessingService(SalesOrderService salesOrderService, CreditService creditService) {
this.salesOrderService = salesOrderService;
this.creditService = creditService;
}
@LogRecord(
success = "成功更新了用户{{#systemUser.userName}}的会员积分。",
type = "USER_CREDIT_UPDATE",
bizNo = "{{#systemUser.id}}"
)
public boolean adjustUserCreditAndProcessOrder(SystemUser systemUser, SalesOrder salesOrder) {
// 将 systemUser 对象放入全局变量,供后续方法访问
LogRecordContext.putGlobalVariable("systemUser", systemUser);
// 调用子方法处理订单,子方法可以从 LogRecordContext 中获取 systemUser
salesOrderService.processOrder(salesOrder);
// 更新用户积分的业务逻辑
return creditService.updateCredit(systemUser);
}
}
性能优化建议
为了在高并发场景下保证系统性能,建议采取以下优化策略:
1. 合理配置日志级别与差异模板
在 application.yml 中配置,开启差异日志并自定义模板,使日志内容更具可读性且控制输出粒度:
mzt:
bizlog:
diff-log:
enabled: true # 开启差异日志功能
template: "【__fieldName】由【__sourceValue】变更为【__targetValue】" # 自定义差异日志显示模板
2. 选择性记录字段
- 使用
@DiffLogAllFields自动标记所有字段参与差异对比。 - 通过
@DiffLogIgnore忽略敏感或不必要的字段,减少日志量。 - 利用
@DiffLogField自定义字段的显示名称和转换函数,提高日志的可读性。
3. 异步日志处理
对于高并发的应用,强烈建议将日志的存储操作异步化,避免阻塞主业务流程。这可以通过实现 ILogRecordService 接口并结合Spring的 @Async 注解实现。
import com.mzt.logapi.starter.service.ILogRecordService;
import com.mzt.logapi.starter.domain.LogRecord;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsynchronousLogPersistenceService implements ILogRecordService {
// 假设 LogRecordRepository 用于将日志持久化到数据库或消息队列
private final LogRecordRepository logRecordRepository;
public AsynchronousLogPersistenceService(LogRecordRepository logRecordRepository) {
this.logRecordRepository = logRecordRepository;
}
@Async // 确保在Spring Boot启动类上添加 @EnableAsync 注解
@Override
public void record(LogRecord logRecord) {
// 在独立的线程中执行日志的持久化操作
// 可以保存到关系型数据库、NoSQL数据库(如MongoDB)、Elasticsearch或发送到Kafka/RabbitMQ
logRecordRepository.save(logRecord);
System.out.println("异步记录日志:" + logRecord.getLogContent());
}
}
最佳实践总结
用户管理场景
- 用户信息变更:利用对象对比功能,详细记录用户属性的修改前后对比。
- 权限分配与回收:记录操作管理员、受影响用户以及具体的权限或角色变动。
- 登录与登出:记录登录时间、IP地址、设备信息、成功或失败原因。
订单跟踪场景
- 状态流转:记录订单从创建到完成的每一个状态变更节点。
- 金额调整:对订单金额的任何变动(如优惠、退款)进行详细记录。
- 物流信息:追踪物流状态更新,包括时间、操作人及新的物流状态。
权限审计场景
- 操作溯源:确保所有敏感操作都能追溯到具体的执行人员和时间。
- 敏感数据操作:对核心数据(如用户密码、财务数据)的增删改查进行最高级别的日志记录。
- 定期审计报告:利用日志数据生成定期审计报告,满足合规性要求。
快速开始指南
1. 添加Maven依赖
在您的Spring Boot项目 pom.xml 文件中添加 mzt-biz-log 的SDK依赖:
<dependency>
<groupId>io.github.mouzt</groupId>
<artifactId>bizlog-sdk</artifactId>
<version>3.0.7</version> <!-- 请使用最新稳定版本 -->
</dependency>
2. 启用日志记录模块
在您的Spring Boot主启动类上添加 @EnableLogRecord 注解以启用日志功能,并配置您的租户ID:
import com.mzt.logapi.starter.annotation.EnableLogRecord;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync; // 如果使用异步日志,需添加此注解
@SpringBootApplication
@EnableLogRecord(tenant = "your-app-tenant-id") // 配置您的应用租户ID
@EnableAsync // 如果使用异步日志处理,请启用异步功能
public class ApplicationStartup {
public static void main(String[] args) {
SpringApplication.run(ApplicationStartup.class, args);
}
}
3. 实现操作员获取服务
实现 IOperatorGetService 接口,提供获取当前操作员信息的方法。通常,这会从Spring Security上下文、HTTP Session或JWT Token中获取。
import com.mzt.logapi.starter.domain.OperatorDO;
import com.mzt.logapi.starter.service.IOperatorGetService;
import org.springframework.stereotype.Service;
@Service
public class CurrentOperatorInfoService implements IOperatorGetService {
@Override
public OperatorDO getUser() {
// 实际应用中,您需要从当前系统的安全上下文中获取操作员信息
// 例如,从 Spring Security 的 SecurityContextHolder.getContext().getAuthentication() 中获取
// 或从自定义的用户会话管理中获取
//
// 示例:
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
// UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// return new OperatorDO(userDetails.getUsername(), userDetails.getUsername());
// }
// 简化示例,请替换为实际的获取逻辑
return new OperatorDO("anonymous", "匿名用户");
}
}