引言
如上图所示,产品的新需求,需要将操作人在系统中具体编辑操作的变更内容记录下来。
按正常思路来说,无非就是将修改前后的对象字段逐个比较,再拼接为详细的操作描述记录到操作日志表中。如果是一个模块的需求,单独写个方法即可,但各个模块都有这部分需求,在各个模块中单独写就显得冗余了,功能代码与日志记录代码混在一起,代码可读性更差了。
因此,项目中引入了新的日志组件,统一实现团队项目中各模块记录操作日志的需求,将日志记录代码与功能代码分离,提升代码的可读性。
概述
在一个系统中,日志主要分为系统日志和操作日志。
系统日志主要是为开发排查问题提供依据。操作日志主要是对某条数据进行新增或者修改操作后进行记录,操作日志要求可读性强,因为它是给用户看的,比如订单的物流信息,用户需求知道在什么时候发生了什么事情。
本篇博客介绍的组件解决的就是该问题:「谁」在「什么时间」对「什么」做了「什么事」
快速开始
1. Maven依赖
<dependency>
<groupId>io.github.mouzt</groupId>
<artifactId>bizlog-sdk</artifactId>
<version>3.0.3<version>
</dependency>
2. SpringBoot入口启用日志组件,添加@EnableLogRecord注解
tenant是代表租户的标识,一般一个服务或者一个业务下的多个服务都写死一个 tenant 就可以。示例如下:
@SpringBootApplication
@EnableTransactionManagement
@EnableLogRecord(tenant = "com.mzt.test")
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
3. 普通日志记录
下面给一个示例,在新增方法上加上@LogRecord注解,补上对应的参数,代码如下:
@LogRecord(
operator = "{{#currentUser}}",
fail = "创建订单失败,失败原因:「{{#_errorMsg}}」",
subType = "MANAGER_VIEW",
extra = "{{#order.toString()}}",
success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,测试变量 {{#innerOrder.productName}}",
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;
}
参数说明:
- SpEL 表达式:其中用双大括号包围起来的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL表达式。Spring中支持的它都支持的。比如调用静态方法,三目表达式
- type:是拼接在 bizNo 上作为 log 的一个标识。避免 bizNo 都为整数 ID 的时候和其他的业务中的 ID 重复。比如订单 ID、用户 ID 等,type可以是订单或者用户
- bizNo:就是业务的 ID,比如订单ID,我们查询的时候可以根据 bizNo 查询和它相关的操作日志
- subType:日志子类型,主要是便于对日志做分类,实现不同角色的人看到不同的日志
- operator:操作人
- success:方法调用成功后把 success 记录在日志的内容中
- extra:支持记录操作的详情或者额外信息
- fail:如果抛出异常则记录fail的日志,其中的 #_errorMsg 是取的方法抛出异常后的异常的 errorMessage
此时会打印操作日志 “张三下了一个订单,购买商品「XXX」,测试变量「内部变量测试」”
4. 保存日志于存储介质中
实现ILogRecordService接口即可,在record方法中保存日志记录,查询日志可根据自身业务需求实现。
@Service
public class DbLogRecordServiceImpl implements ILogRecordService {
@Resource
private LogRecordMapper logRecordMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(LogRecord logRecord) {
log.info("【logRecord】log={}", logRecord);
LogRecordPO logRecordPO = LogRecordPO.toPo(logRecord);
logRecordMapper.insert(logRecordPO);
}
@Override
public List<LogRecord> queryLog(String bizKey, Collection<String> types) {
return Lists.newArrayList();
}
@Override
public PageDO<LogRecord> queryLogByBizNo(String bizNo, Collection<String> types, PageRequestDO pageRequestDO) {
return logRecordMapper.selectByBizNoAndCategory(bizNo, types, pageRequestDO);
}
}
通过上面简单的几步就能实现记录新增操作的日志,不用在业务代码中写一些繁琐的日志代码。下面再简单介绍一些组件的其他使用特性和示例。
其他使用
1. 自定义函数
在日志记录中,可能会存在字段值是ID,如订单号,而看到一堆数字并不知道对应的内容(订单名等)这日志记录的可读性很差。因此可以使用自定义函数,可在函数中实现通过ID查询到对应内容显示。
使用上只需要实现框架里面的IParseFunction的接口,实现两个方法:
1)functionName() 方法就返回注解上面的函数名;
2)apply()函数参数是 "{ORDER{#orderId}}"中SpEL解析的#orderId的值,这里是一个数字1223110,接下来只需要在实现的类中把 ID 转换为可读懂的字符串就可以了, 一般为了方便排查问题需要把名称和ID都展示出来,例如:"订单名称(ID)"的形式。
示例代码:
//使用了自定义函数,主要是在 {{#orderId}} 的大括号中间加了 functionName
@LogRecord(success = "更新了订单{ORDER{#orderId}},更新内容为...",
type = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}",
extra = "{{#order.toString()}}")
public boolean update(Long orderId, Order order) {
return false;
}
// 还需要加上函数的实现
@Slf4j
@Component
public class OrderParseFunction implements IParseFunction {
@Override
public boolean executeBefore() {
return true;
}
@Override
public String functionName() {
return "ORDER";
}
@Override
public String apply(Object value) {
log.info("@@@@@@@@");
if (StringUtils.isEmpty(value)) {
return "";
}
log.info("###########,{}", value);
Order order = new Order();
order.setProductName("xxxx");
return order.getProductName().concat("(").concat(value.toString()).concat(")");
}
}
2. 使用方法参数之外的变量
可以在方法中通过 LogRecordContext.putVariable(variableName, Object) 的方法添加变量,第一个对象为变量名称,后面为变量的对象
3. diff特性
用于快速比较并记录两个对象之间不同的字段内容。
1)使用@DiffLogField标注字段含义
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
@DiffLogField(name = "订单ID", function = "ORDER")
private Long orderId;
@DiffLogField(name = "订单号")
private String orderNo;
private String purchaseName;
private String productName;
@DiffLogField(name = "创建时间")
private LocalDateTime createTime;
@DiffLogField(name = "创建人")
private UserDO creator;
@DiffLogField(name = "更新人")
private UserDO updater;
@DiffLogField(name = "列表项", function = "ORDER")
private List<String> items;
@DiffLogField(name = "拓展信息", function = "extInfo")
private String[] extInfo;
@Data
public static class UserDO {
@DiffLogField(name = "用户ID")
private Long userId;
@DiffLogField(name = "用户姓名")
private String userName;
}
}
name:是生成的 DIFF 文案中 Field 的中文, function: 自定义函数,例如可以把用户ID映射成用户姓名。
2)在更新方法上,使用日志注解
@LogRecord(success = "更新订单。{_DIFF{#oldOrder, #newOrder}}",
type = LogRecordType.ORDER, bizNo = "{{#newOrder.orderNo}}",
extra = "{{#newOrder.toString()}}")
public boolean diff(Order oldOrder, Order newOrder) {
//....
return false;
}
4. 日志记录与业务逻辑一起回滚
默认日志记录错误不影响业务的流程,若希望日志记录过程如果出现异常,让业务逻辑也一起回滚,在 @EnableLogRecord 中 joinTransaction 属性设置为 true, 另外 @EnableTransactionManagement order 属性设置为0 (让事务的优先级在@EnableLogRecord之前)
@EnableLogRecord(tenant = "com.mzt.test", joinTransaction = true)
@EnableTransactionManagement(order = 0)
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
总结
通过本篇博客对bizlog的简单介绍与使用教程,相信你一定感受到了该组件的魅力所在。如果你的项目中正好有同样的需求,而你还是在业务逻辑代码中实现,不妨试试该日志框架,简单好用。