一个小小的注解,帮你搞定 SpringBoot 操作日志

6. 日志文案调整

==========

对于更新等方法,方法的参数上大部分都是订单 ID、或者产品 ID 等,

比如下面的例子:日志记录的 success 内容是:“更新了订单 {{#orderId}}, 更新内容为…”,这种对于运营或者产品来说难以理解,所以引入了自定义函数的功能。

使用方法是在原来的变量的两个大括号之间加一个函数名称 例如 “{ORDER{#orderId}}” 其中 ORDER 是一个函数名称。只有一个函数名称是不够的, 需要添加这个函数的定义和实现。可以看下面例子

自定义的函数需要实现框架里面的 IParseFunction 的接口,需要实现两个方法:

  • functionName() 方法就返回注解上面的函数名;

  • apply() 函数参数是 “{ORDER{#orderId}}” 中 SpEL 解析的 #orderId 的值,这里是一个数字 1223110,接下来只需要在实现的类中把 ID 转换为可读懂的字符串就可以了,

这里有个问题:加了自定义函数后,框架怎么能调用到呢?

// 没有使用自定义函数

@LogRecordAnnotation(success = “更新了订单{{#orderId}},更新内容为…”,

prefix = LogRecordType.ORDER, bizNo = “{{#order.orderNo}}”,

detail = “{{#order.toString()}}”)

public boolean update(Long orderId, Order order) {

return false;

}

//使用了自定义函数,主要是在 {{#orderId}} 的大括号中间加了 functionName

@LogRecordAnnotation(success = “更新了订单ORDER{#orderId}},更新内容为…”,

prefix = LogRecordType.ORDER, bizNo = “{{#order.orderNo}}”,

detail = “{{#order.toString()}}”)

public boolean update(Long orderId, Order order) {

return false;

}

// 还需要加上函数的实现

@Component

public class OrderParseFunction implements IParseFunction {

@Resource

@Lazy //为了避免类加载顺序的问题 最好为Lazy,没有问题也可以不加

private OrderQueryService orderQueryService;

@Override

public String functionName() {

// 函数名称为 ORDER

return “ORDER”;

}

@Override

//这里的 value 可以吧 Order 的JSON对象地传递过来,然后反解析拼接一个定制的操作日志内容

public String apply(String value) {

if(StringUtils.isEmpty(value)){

return value;

}

Order order =

orderQueryService.queryOrder(Long.parseLong(value));

//把订单产品名称加上便于理解,加上 ID 便于查问题

return order.getProductName().concat(“(”).concat(value).concat(“)”);

}

}

7. 日志文案调整 使用 SpEL 三目表达式

========================

@LogRecordAnnotation(prefix =

LogRecordTypeConstant.CUSTOM_ATTRIBUTE, bizNo = “{{#businessLineId}}”,

success = “{{#disable ? ‘停用’ : ‘启用’}}了自定义属性{ATTRIBUTE{#attributeId}}”)

public CustomAttributeVO disableAttribute(Long businessLineId, Long attributeId, boolean disable) {

return xxx;

}

8. 日志文案调整 模版中使用方法参数之外的变量

=========================

可以在方法中通过

LogRecordContext.putVariable(variableName, Object) 的方法添加变量,第一个对象为变量名称,后面为变量的对象,

@Override

@LogRecordAnnotation(

success = “{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,测试变量「{{#innerOrder.productName}}」,下单结果:{{#_ret}}”,

prefix = 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;

}

9. 函数中使用 LogRecordContext 的变量

==============================

使用

LogRecordContext.putVariable(variableName, Object) 添加的变量除了可以在注解的 SpEL 表达式上使用,还可以在自定义函数中使用, 这种方式比较复杂,下面例子中示意了列表的变化,比如从 [A,B,C] 改到 [B,D] 那么日志显示:「删除了 A,增加了 D」

@LogRecord(success = “{DIFF_LIST{‘文档地址’}}”, bizNo = “{{#id}}”, prefix = REQUIREMENT)

public void updateRequirementDocLink(String currentMisId, Long id, List docLinks) {

RequirementDO requirementDO = getRequirementDOById(id);

LogRecordContext.putVariable(“oldList”, requirementDO.getDocLinks());

LogRecordContext.putVariable(“newList”, docLinks);

requirementModule.updateById(“docLinks”, RequirementUpdateDO.builder()

.id(id)

.docLinks(docLinks)

.updater(currentMisId)

.updateTime(new Date())

.build());

}

@Component

public class DiffListParseFunction implements IParseFunction {

@Override

public String functionName() {

return “DIFF_LIST”;

}

@SuppressWarnings(“unchecked”)

@Override

public String apply(String value) {

if (StringUtils.isBlank(value)) {

return value;

}

List oldList = (List)

LogRecordContext.getVariable(“oldList”);

List newList = (List)

LogRecordContext.getVariable(“newList”);

oldList = oldList == null ? Lists.newArrayList() : oldList;

newList = newList == null ? Lists.newArrayList() : newList;

Set deletedSets = Sets.difference(Sets.newHashSet(oldList), Sets.newHashSet(newList));

Set addSets = Sets.difference(Sets.newHashSet(newList), Sets.newHashSet(oldList));

StringBuilder stringBuilder = new StringBuilder();

if (

CollectionUtils.isNotEmpty(addSets)) {

stringBuilder.append(“新增了 ”).append(value).append(“:”);

for (String item : addSets) {

stringBuilder.append(item).append(“,”);

}

}

if (

CollectionUtils.isNotEmpty(deletedSets)) {

stringBuilder.append(“删除了 ”).append(value).append(“:”);

for (String item : deletedSets) {

stringBuilder.append(item).append(“,”);

}

}

return StringUtils.isBlank(stringBuilder) ? null : stringBuilder.substring(0, stringBuilder.length() - 1);

}

}

框架的扩展点

======

  • 重写 OperatorGetServiceImpl 通过上下文获取用户的扩展,例子如下

@Service

public class DefaultOperatorGetServiceImpl implements****IOperatorGetService {

@Override

public Operator getUser() {

return Optional.ofNullable(UserUtils.getUser())

.map(a -> new Operator(a.getName(), a.getLogin()))

.orElseThrow(()->new IllegalArgumentException(“user is null”));

}

}

  • ILogRecordService 保存 / 查询日志的例子, 使用者可以根据数据量保存到合适的存储介质上,比如保存在数据库 / 或者 ES。自己实现保存和删除就可以了

也可以只实现查询的接口,毕竟已经保存在业务的存储上了,查询业务可以自己实现,不走 ILogRecordService 这个接口,毕竟产品经理会提一些千奇百怪的查询需求。

@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 queryLog(String bizKey, Collection types) {

return Lists.newArrayList();

}

@Override

public PageDO queryLogByBizNo(String bizNo, Collection types, PageRequestDO pageRequestDO) {

return

logRecordMapper.selectByBizNoAndCategory(bizNo, types, pageRequestDO);

}

}

  • IParseFunction 自定义转换函数的接口,可以实现 IParseFunction 实现对 LogRecord 注解中使用的函数扩展

@Component

public class UserParseFunction implements IParseFunction {

private final Splitter splitter = Splitter.on(“,”).trimResults();

@Resource

@Lazy
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

ActiveMQ消息中间件面试专题

  • 什么是ActiveMQ?
  • ActiveMQ服务器宕机怎么办?
  • 丢消息怎么办?
  • 持久化消息非常慢怎么办?
  • 消息的不均匀消费怎么办?
  • 死信队列怎么办?
  • ActiveMQ中的消息重发时间间隔和重发次数吗?

ActiveMQ消息中间件面试专题解析拓展:

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


redis面试专题及答案

  • 支持一致性哈希的客户端有哪些?
  • Redis与其他key-value存储有什么不同?
  • Redis的内存占用情况怎么样?
  • 都有哪些办法可以降低Redis的内存使用情况呢?
  • 查看Redis使用情况及状态信息用什么命令?
  • Redis的内存用完了会发生什么?
  • Redis是单线程的,如何提高多核CPU的利用率?

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


Spring面试专题及答案

  • 谈谈你对 Spring 的理解
  • Spring 有哪些优点?
  • Spring 中的设计模式
  • 怎样开启注解装配以及常用注解
  • 简单介绍下 Spring bean 的生命周期

Spring面试答案解析拓展

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


高并发多线程面试专题

  • 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
  • Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
  • Java 中 wait 和 sleep 方法有什么区别?
  • 如何在 Java 中实现一个阻塞队列?
  • 如何在 Java 中编写代码解决生产者消费者问题?
  • 写一段死锁代码。你在 Java 中如何解决死锁?

高并发多线程面试解析与拓展

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


jvm面试专题与解析

  • JVM 由哪些部分组成?
  • JVM 内存划分?
  • Java 的内存模型?
  • 引用的分类?
  • GC什么时候开始?

JVM面试专题解析与拓展!

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。

  • Java 中 wait 和 sleep 方法有什么区别?
  • 如何在 Java 中实现一个阻塞队列?
  • 如何在 Java 中编写代码解决生产者消费者问题?
  • 写一段死锁代码。你在 Java 中如何解决死锁?

高并发多线程面试解析与拓展

[外链图片转存中…(img-EpSWjO7R-1711866483982)]


jvm面试专题与解析

  • JVM 由哪些部分组成?
  • JVM 内存划分?
  • Java 的内存模型?
  • 引用的分类?
  • GC什么时候开始?

JVM面试专题解析与拓展!

[外链图片转存中…(img-JkDcZxQh-1711866483982)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值