前言:
为什么要用biz-log,接口需要记录谁在什么时间做了什么事,并在日志模块进行展示,每个接口的接口描述不一样。需要接口写不同描述。
提前了解:SpEL(Spring Expression Language)语句
注意:该文档只是记录作者使用过程中用到的!!!
biz-log基本使用
-
导包(最新3.0.6)
<!-- https://mvnrepository.com/artifact/io.github.mouzt/bizlog-sdk --> <dependency> <groupId>io.github.mouzt</groupId> <artifactId>bizlog-sdk</artifactId> <version>3.0.6</version> </dependency> -
启动类添加 @EnableLogRecord注解
tenant可以作为系统的标识。
@EnableLogRecord(tenant = "XXX系统")
-
注解@LogRecord 简单使用
接口上添加@LogRecord注解 (源码里面截取)
@LogRecord(success = "更新了订单{{#newOrder}}", type = LogRecordType.ORDER, bizNo = "{{#newOrder.orderNo}}", extra = "{{#newOrder.toString()}}") boolean diff2(Order newOrder);
注解字段解释
success:执行成功后后的日志模版
type: 类型
bizNo:业务标识
extra:补充说明,额外信息
如果方法执行成功,日志系统会根据 success 属性的值来记录日志。
在记录日志时,日志系统会解析字符串模板,并替换其中的占位符为实际的值。{{#newOrder}} 可能会被替换为 newOrder 对象的某个属性或其字符串表示形式,{{#newOrder.orderNo}} 会被替换为 newOrder 对象的 orderNo 属性值。
日志系统会记录一条包含实际值的日志消息。
看完这部分其实就可以简单使用了。接下来就是存储 以及自定义复杂日志
1. 日志存储
a. 添加 LogRecordPO,LogRecordDao
LogRecordPO字段包含源码里面的LogRecord类(com.mzt.logapi.beans) 添加部分业务字段
@Data
@NoArgsConstructor
@TableName(value = "tab_log_record")
@Accessors(chain = true)
public class LogRecordPO {
/**
* id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 租户
*/
private String tenant;
/**
* 保存的操作日志的类型,比如:订单类型、商品类型
*
* @since 2.0.0 从 prefix 修改为了type
*/
@NotBlank(message = "type required")
@Length(max = 200, message = "type max length is 200")
private String type;
/**
* 日志的子类型,比如订单的C端日志,和订单的B端日志,type都是订单类型,但是子类型不一样
* @since 2.0.0 从 category 修改为 subtype
*/
private String subType;
/**
* 日志绑定的业务标识
*/
@NotBlank(message = "bizNo required")
@Length(max = 200, message = "bizNo max length is 200")
private String bizNo;
/**
* 操作人
*/
@NotBlank(message = "operator required")
@Length(max = 63, message = "operator max length 63")
private String operator;
private String ip;
private String orgId;
private String orgName;
/**
* 日志内容
*/
@NotBlank(message = "opAction required")
@Length(max = 511, message = "operator max length 511")
private String action;
/**
* 记录是否是操作失败的日志
*/
private boolean fail;
/**
* 日志的创建时间
*/
private LocalDateTime createTime;
/**
* 日志的额外信息
*
* @since 2.0.0 从detail 修改为extra
*/
private String extra;
}
@Mapper
public interface LogRecordDao extends BaseMapper<LogRecordPO> {
}
b.实现ILogRecordService 重写record,获取到LogRecord 并入库
//ILogRecordService源码
public interface ILogRecordService {
void record(LogRecord logRecord);
List<LogRecord> queryLog(String bizNo, String type);
List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType);
}
@Service
@RequiredArgsConstructor
public class LogRecordServiceImpl implements ILogRecordService{
private final LogRecordConvert logRecordConvert;
private final LogRecordDao logRecordDao;
@Override
public void record(LogRecord logRecord) {
LogRecordPO po = logRecordConvert.convertToPo(logRecord);
//需要处理的业务字段
logRecordDao.insert(po);
}
@Override
public List<LogRecord> queryLog(String bizNo, String type) {
return null;
}
@Override
public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
return null;
}
}
c.指定操作人 实现IOperatorGetService
LogRecord中有operator 用来标识操作人,可以实现IOperatorGetService
@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {
@Override
public Operator getUser() {
//获取用户
UserBean userInfo = UserThreadLocal.getUserBean();
Operator operatorDO = new Operator();
operatorDO.setOperatorId(userInfo.getUsername());
return operatorDO;
}
}
2.复杂日志
a.三元表达式
@LogRecord(
fail = "统计模块,{{#timeRangeParam.receiverType==0? '测试' : '测试1'}}完成率导出失败。",
success = "统计模块,{{#timeRangeParam.receiverType==0? '测试' : '测试1'}}完成率导出。共{{#timeRangeParam.size}}条"
type = "统计模块",
subType = "{{#timeRangeParam.receiverType==0? '测试' : '测试1'}}完成率导出",
bizNo = "",
extra = "入参:{{#timeRangeParam.toJson()}}")
List<StatisticsExportRt> finishStateExport(TimeRangeParam timeRangeParam);
type: 用来标识模块
subType: 模块某个接口
extra: 入参信息 TimeRangeParam中写了一个toJson
所有SpEL语句 获取到的TimeRangeParam 里面的参数 都是整个方法执行完后的。
假如:TimeRangeParam 里面 有一个size ,最开始赋值为0. 方法内部 使size=10,则
成功的描述里面 共10条,而不是0条。
b.模版中使用方法参数之外的变量
接口:导出用户信息
日志描述:用户模块,导出用户信息从2024年X月X日到2024年X月X日,共X条
时间一般为LocalDateTime形式 与产品所需2024年X月X日格式不同。
解决思路:
第一种:size也可以加到param里面查出来之后 set一下。这样的话 我们需要什么字段就需要添加到param中 如果需要记录日志的接口过多,改动了很大
第二种:使用 LogRecordContext.
putVariable 添加变量。
代码如下:
添加InnerLogInfoDTO类
@Data
@NoArgsConstructor
public class InnerLogInfoDTO {
/**
* 数量
*/
private Integer size;
/**
* 开始时间
*/
private String startTimeStr;
/**
* 结束时间
*/
private String endTimeStr;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
}
添加 @LogRecord注解 部分使用innerLogInfoDTO
@LogRecord(
fail = "用户模块,导出失败。",
success = "用户模块,导出用户信息," +
"{{#innerLogInfoDTO.startTimeStr != null && #innerLogInfoDTO.endTimeStr != null ? #innerLogInfoDTO.startTimeStr +'到'+#innerLogInfoDTO.endTimeStr+',' :''}}" +
"共{{#innerLogInfoDTO.size}}条",
type = "用户模块",
subType = "用户信息导出",
bizNo = "",
extra = "入参:{{#param.toJson()}}")
File exportUserInfo(Param param) throws Exception;
exportUserInfo接口添加如下代码
@Override
public File exportUserInfo(Param param) throws Exception {
//处理日志描述
InnerLogInfoDTO innerLogInfoDTO=new InnerLogInfoDTO();
innerLogInfoDTO.setSize(size);
if(Objects.nonNull(commandParam.getStartTime())){
innerLogInfoDTO.setStartTime(commandParam.getStartTime());
}
if(Objects.nonNull(commandParam.getEndTime())){
innerLogInfoDTO.setEndTime(commandParam.getStartTime());
}
innerLogInfoManagement.handleInnerLogInfo(innerLogInfoDTO);
}
public void handleInnerLogInfo(InnerLogInfoDTO innerLogInfoDTO) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
if(Objects.nonNull(innerLogInfoDTO.getStartTime())){
String startTime = innerLogInfoDTO.getStartTime().format(formatter);
innerLogInfoDTO.setStartTimeStr(startTime);
}
if(Objects.nonNull(innerLogInfoDTO.getEndTime())){
String endTime = innerLogInfoDTO.getEndTime().format(formatter);
innerLogInfoDTO.setEndTimeStr(endTime);
}
//使用LogRecordContext.putVariable添加变量
LogRecordContext.putVariable("innerLogInfoDTO", innerLogInfoDTO);
}
通过 LogRecordContext.putVariable(variableName, Object) 方法添加变量,第一个对象为变量名称,后面为变量的对象, 使用
SpEL
使用这个变量了
@LogRecord 里面{{#innerLogInfoDTO.size}} 中的innerLogInfoDTO为putVariable的variableName。
跨方法使用
通过LogRecordContext.putGlobalVariable(variableName, Object) 放入上下文中,此优先级为最低,若方法上下文中存在相同的变量,则会覆盖
@Override
@LogRecord(
success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,测试变量「{{#innerOrder.productName}}」,下单结果:{{#_ret}}",
type = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order) {
log.info("【创建订单】orderNo={}", order.getOrderNo());
// db insert order
Order order1 = new Order();
order1.setProductName("内部变量测试");
LogRecordContext.putVariable("innerOrder", order1);
return true;
}
------------2025.06更新------------
6. 需求:记录部分接口的变更记录,变更字段更新前的值,以及更新后的值
实现:接口添加注解@LogRecord
@LogRecord(
//标识操作模块
type = LogRecordConstants.OPINION_GATHER_MODULE,
//标识操作类型
subType = LogRecordConstants.UPDATE_OPINION_INFO,
//自定函数
extra = "{opinionInfoUpdate{#oldInfo.toString()}}",
success = LogRecordConstants.UPDATE_OPINION_INFO,
bizNo = "{{#opinionLevelParam.opinionId}}")
@Override
public void update(LevelParam param){
//获取详情
DetailRt rt=getInfo(param.getId());
//旧数据放入日志上下文,方便后续对比更新后的数据信息
LogRecordContext.putVariable("oldInfo", rt);
//业务操作 更新。。。
}
a. 自定义注解ForUpdate(标识需要记录的变更字段)
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ForUpdate { String fieldName() default ""; String fieldComment() default "";}
b. 对应实体类字段添加注解@ForUpdate
filedName,主要是为了在后面入库存储,变更前,变更后字段。方便前展示
@ApiModelProperty("级别") @ForUpdate(fieldName = "level") private Integer level;
c. 自定义函数opinionInfoUpdate
第一步中的@LogRecord 中有一个参数为
extra = "{update{#oldInfo.toString()}}" 其中update 是定义的一个函数标识符。
OpinionInfoUpdateParseFunction类
@Componentpublic class OpinionInfoUpdateParseFunction implements IParseFunction { @Override //false 标识该函数目标方法执行之后运行 public boolean executeBefore() { return false; } @Override public String functionName() { //定义函数标识符,函数标识符必须唯一 return "opinionInfoUpdate"; } @Override public String apply(Object value) { //从日志上下文获取oldInfo ReportRt oldInfo = (ReportRt) LogRecordContext.getVariable("oldInfo"); Long id = oldInfo.getId(); //业务处理获取更新后的数据信息 ReportRt newInfo=getUpdateInfo(id); //将需要的字段放入context LogRecordContext.putVariable(LogRecordConstants.TITLE, newInfo.getTitle()); //ChangeFiledUtil 获取更新后的字段信息; String changedFields = ChangeFiledUtil.getChangedFields(oldInfo, newInfo); LogRecordContext.putVariable("changedFields", changedFields); LogRecordContext.putVariable(LogRecordConstants.DETAIL, changedFields); return changedFields; }}
ChangeFiledUtil获取更新后的字段信息
public class ChangeFiledUtil { public static final Logger logger = Logger.getLogger(ChangeFiledUtil.class); /** * 获取字段改变记录 * * @param beforeBean 更改前bean * @param afterBean 更改后bean * @param <T> 泛型 * @return String */ public static <T> String getChangedFields(T beforeBean, T afterBean) { Field[] fields = beforeBean.getClass().getDeclaredFields(); JSONArray arr = new JSONArray(); for (Field field : fields) { field.setAccessible(true); if (field.isAnnotationPresent(ForUpdate.class)) { try { Object beforeValue = field.get(beforeBean); Object afterValue = field.get(afterBean); if (!Objects.equals(beforeValue, afterValue)) { //获取字段名称 JSONObject jsonObject = new JSONObject(); jsonObject.set("name", field.getAnnotation(ForUpdate.class).fieldName()); jsonObject.set("beforeValue", beforeValue); jsonObject.set("afterValue", afterValue); arr.add(jsonObject); } } catch (Exception e) { logger.error("操作失败"); } } } if (arr.isEmpty()) { return null; } return JSONUtil.toJsonStr(arr); }}
定义实体类ChangeFileRt
@Datapublic class ChangeFileRt { private String name; private String beforeValue; private String afterValue;}
d. 日志信息入库处理
LogRecordServiceImpl 操作日志持久化
public class LogRecordServiceImpl implements ILogRecordService { /** * 操作日志记录方法抛出异常情况下, 事务建议使用REQUIRES_NEW,重新创建事务,否则会随着主任务回滚. * * @param logRecord */ @Override @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void record(LogRecord logRecord) { //从上下文获取相关字段 String title = (String) LogRecordContext.getVariable(LogRecordConstants.TITLE); String detailValue = (String) LogRecordContext.getVariable(LogRecordConstants.DETAIL); String changedFields = (String) LogRecordContext.getVariable(LogRecordConstants.CHANGEDFIELDS); //未修改字段,不记录日志 if (StringUtils.isBlank(changedFields)){ return; } List<ChangeFileRt> changeFileRts = JSON.parseArray(changedFields, ChangeFileRt.class); JSONObject beforeJson=new JSONObject(); JSONObject afterJson=new JSONObject(); changeFileRts.forEach(changeFileRt -> { String name = changeFileRt.getName(); String beforeValue = changeFileRt.getBeforeValue(); String afterValue = changeFileRt.getAfterValue(); beforeJson.set(name,beforeValue); afterJson.set(name,afterValue); }); String beforeValue = beforeJson.toString(); String afterValue = afterJson.toString(); String operator = UserService.currentUserName(); String type = logRecord.getType(); String subType = logRecord.getSubType(); //获取IP地址 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); String ipAddress = IpUtil.getIpAddress(request); // 业务保存日志 } @Override public List<LogRecord> queryLog(String bizNo, String type) { return Lists.newArrayList(); } @Override public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) { return Lists.newArrayList(); }}