此组件解决的问题是:

「谁」在「什么时间」对「什么」做了「什么事」

本组件目前针对 Spring-boot 做了 Autoconfig,如果是 SpringMVC,也可自己在 xml 初始化 bean

使用方式

基本使用

maven依赖添加SDK依赖

io.github.mouztbizlog-sdk1.0.1

SpringBoot入口打开开关,添加 @EnableLogRecord 注解

tenant是代表租户的标识,一般一个服务或者一个业务下的多个服务都写死一个 tenant 就可以

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableTransactionManagement
@EnableLogRecord(tenant = "com.mzt.test")
public class Main {public static void main(String[] args) {SpringApplication.run(Main.class, args);}
}

Spring Boot 最新教程推荐看下面这个。

https://github.com/javastacks/spring-boot-best-practice

日志埋点

1. 普通的记录日志
  • pefix:是拼接在 bizNo 上作为 log 的一个标识。避免 bizNo 都为整数 ID 的时候和其他的业务中的 ID 重复。比如订单 ID、用户 ID 等

  • bizNo:就是业务的 ID,比如订单ID,我们查询的时候可以根据 bizNo 查询和它相关的操作日志

  • success:方法调用成功后把 success 记录在日志的内容中

  • SpEL 表达式:其中用双大括号包围起来的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL表达式。Spring中支持的它都支持的。比如调用静态方法,三目表达式。SpEL 可以使用方法中的任何参数

@LogRecordAnnotation(success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}",prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order) {log.info("【创建订单】orderNo={}", order.getOrderNo());// db insert orderreturn true;
}

此时会打印操作日志 “张三下了一个订单,购买商品「超值优惠红烧肉套餐」,下单结果:true”

2. 期望记录失败的日志, 如果抛出异常则记录fail的日志,没有抛出记录 success 的日志
@LogRecordAnnotation(fail = "创建订单失败,失败原因:「{{#_errorMsg}}」",success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}",prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order) {log.info("【创建订单】orderNo={}", order.getOrderNo());// db insert orderreturn true;
}

其中的 #_errorMsg 是取的方法抛出异常后的异常的 errorMessage。

3. 日志支持种类

比如一个订单的操作日志,有些操作日志是用户自己操作的,有些操作是系统运营人员做了修改产生的操作日志,我们系统不希望把运营的操作日志暴露给用户看到,

但是运营期望可以看到用户的日志以及运营自己操作的日志,这些操作日志的bizNo都是订单号,所以为了扩展添加了类型字段,主要是为了对日志做分类,查询方便,支持更多的业务。

@LogRecordAnnotation(fail = "创建订单失败,失败原因:「{{#_errorMsg}}」",category = "MANAGER",success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}",prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order) {log.info("【创建订单】orderNo={}", order.getOrderNo());// db insert orderreturn true;
}
4. 支持记录操作的详情或者额外信息

如果一个操作修改了很多字段,但是success的日志模版里面防止过长不能把修改详情全部展示出来,这时候需要把修改的详情保存到 detail 字段,detail 是一个 String ,需要自己序列化。

这里的 #order.toString() 是调用了 Order 的 toString() 方法。

如果保存 JSON,自己重写一下 Order 的 toString() 方法就可以。

 @LogRecordAnnotation(fail = "创建订单失败,失败原因:「{{#_errorMsg}}」",category = "MANAGER_VIEW",detail = "{{#order.toString()}}",success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}",prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order) {
log.info("【创建订单】orderNo={}", order.getOrderNo());
// db insert order
return true;
}
5. 如何指定操作日志的操作人是什么?框架提供了两种方法

第一种:手工在LogRecord的注解上指定。这种需要方法参数上有operator

@LogRecordAnnotation(fail = "创建订单失败,失败原因:「{{#_errorMsg}}」",category = "MANAGER_VIEW",detail = "{{#order.toString()}}",operator = "{{#currentUser}}",success = "{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}",prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
public boolean createOrder(Order order, String currentUser) {log.info("【创建订单】orderNo={}", order.getOrderNo());// db insert orderreturn true;
}

这种方法手工指定,需要方法参数上有 operator 参数,或者通过 SpEL 调用静态方法获取当前用户。

第二种:通过默认实现类来自动的获取操作人,由于在大部分web应用中当前的用户都是保存在一个线程上下文中的,所以每个注解都加一个operator获取操作人显得有些重复劳动,所以提供了一个扩展接口来获取操作人 框架提供了一个扩展接口。

使用框架的业务可以 implements 这个接口自己实现获取当前用户的逻辑,对于使用 Springboot 的只需要实现 IOperatorGetService 接口,然后把这个 Service 作为一个单例放到 Spring 的上下文中。使用 Spring Mvc 的就需要自己手工装配这些 bean 了。

@Configuration
public class LogRecordConfiguration {@Bean
public IOperatorGetService operatorGetService() {return () -> Optional.of(OrgUserUtils.getCurrentUser()).map(a -> new OperatorDO(a.getMisId())).orElseThrow(() -> new IllegalArgumentException("user is null"));
}
}//也可以这么搞:
@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {@Overridepublic OperatorDO getUser() {OperatorDO operatorDO = new OperatorDO();operatorDO.setOperatorId("SYSTEM");return operatorDO;}
}
6. 日志文案调整

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

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

可以看下面例子 自定义的函数需要实现框架里面的IParseFunction的接口,需要实现两个方法:

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

  • apply()函数参数是 "{ORDER{#orderId}}"中SpEL解析的#orderId的值,这里是一个数字1223110,接下来只需要在实现的类中把 ID 转换为可读懂的字符串就可以了,一般为了方便排查问题需要把名称和ID都展示出来,例如:"订单名称(ID)"的形式。

这里有个问题:加了自定义函数后,框架怎么能调用到呢?答:对于Spring boot应用很简单,只需要把它暴露在Spring的上下文中就可以了,可以加上Spring的 @Component 或者 @Service 很方便????。Spring mvc 应用需要自己装配 Bean。

// 没有使用自定义函数
@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() {//  函数名称为 ORDERreturn "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;
}

框架的扩展点

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

@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {@Overridepublic 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 {@Resourceprivate 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);}@Overridepublic List queryLog(String bizKey, Collection types) {return Lists.newArrayList();}@Overridepublic 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@Lazyprivate UserQueryService userQueryService;@Overridepublic String functionName() {return "USER";}@Override// 11,12 返回 11(小明),12(张三)public String apply(String value) {if (StringUtils.isEmpty(value)) {return value;}List userIds = Lists.newArrayList(splitter.split(value));List misDOList = userQueryService.getUserList(userIds);Map userMap = StreamUtil.extractMap(misDOList, User::getId);StringBuilder stringBuilder = new StringBuilder();for (String userId : userIds) {stringBuilder.append(userId);if (userMap.get(userId) != null) {stringBuilder.append("(").append(userMap.get(userId).getUsername()).append(")");}stringBuilder.append(",");}return stringBuilder.toString().replaceAll(",$", "");}
}

变量相关

LogRecordAnnotation 可以使用的变量出了参数也可以使用返回值#_ret变量,以及异常的错误信息#_errorMsg,也可以通过SpEL的 T 方式调用静态方法噢

待扩展

实现一个 Log的 Context,可以解决方法参数中没有的变量但是想使用的问题,初步想法是可以通过在方法中 add 变量的形式实现,很快就可以实现了

注意点:

⚠️ 整体日志拦截是在方法执行之后记录的,所以对于方法内部修改了方法参数之后,LogRecordAnnotation 的注解上的 SpEL 对变量的取值是修改后的值哦。

IT技术分享社区

个人博客网站:https://programmerblog.xyz

文章推荐程序员效率:画流程图常用的工具程序员效率:整理常用的在线笔记软件远程办公:常用的远程协助软件,你都知道吗?51单片机程序下载、ISP及串口基础知识硬件:断路器、接触器、继电器基础知识

Java后端:一个注解搞定 Spring Boot 日志!相关推荐

  1. Java盗刷_一个依赖搞定 Spring Boot 反爬虫,防止接口盗刷!

    责编:乐乐 链接:oschina.net/news/112586/kk-anti-reptile-released kk-anti-reptile 是适用于基于 spring-boot 开发的分布式系 ...

  2. 一个依赖搞定Spring Boot 配置文件脱敏

    经常会遇到这样一种情况:项目的配置文件中总有一些敏感信息,比如数据源的url.用户名.密码....这些信息一旦被暴露那么整个数据库都将会被泄漏,那么如何将这些配置隐藏呢? 今天介绍一种方案,让你在无感 ...

  3. 一个依赖搞定 Spring Boot 反爬虫,防止接口盗刷!

    今日推荐 减少 try-catch ,这样做才叫优雅!让人上瘾的新一代开发神器,彻底告别Controller.Service.Dao等方法SpringBoot实现人脸识别功能相信我,使用 Stream ...

  4. 一个注解搞定接口防刷!还有谁不会?

    点击关注公众号,Java干货及时送达 说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考 一,技术要点:springboot的基本知识,redis基本操作, 首先是写一个注解类: ...

  5. 一个注解搞定 SpringBoot 接口防刷,还有谁不会?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:CS打赢你 blog.csdn.net/weixin ...

  6. git上托管的代码如何部署在阿里云上_居然仅用浏览器搞定Spring Boot应用的开发与部署...

    最近有幸试用了一下阿里云的一个新产品:云开发平台,体验一把全新的开发模式!虽然中间也碰到了一些问题,但整体的体验透露着未来感,因为整个过程都不需要使用我们最常用的IDEA,仅依靠浏览器就把一个Spri ...

  7. 数据翻译的代码辅助插件,一个注解搞定,减少30%SQL代码量

    一.开源项目简介 Easy Trans是一款用于做数据翻译的代码辅助插件,利用MyBatis Plus/JPA/BeetlSQL 等ORM框架的能力自动查表,让开发者可以快速的把ID/字典码 翻译为前 ...

  8. 《1天搞定Spring boot +vue 全栈开发》后端学习笔记

    前言 课程 b站链接:1.课程介绍及环境准备_哔哩哔哩_bilibili 对springboot 的开发实践有很好的指导作用(原理涉及的不多),课程很干,适合初学者,但需要有Jave EE 基础. 涉 ...

  9. Android线程创建aop,【android安卓】一个注解搞定线程切换,基于AOP的线程转换框架...

    最简单的使用方法: 模拟进度展示: @RunOnIOThread public void progress() { for (int i = 0; i <= 100; i++) { showPr ...

最新文章

  1. 机房线路老化了会有怎么样的影响?又该如何处理?
  2. 【转】删除过期数据通用程序
  3. 定义跳转插件_虚幻插件Review:Logic Driver Pro 终极状态机插件
  4. java全局变量怎么定义_Java开发知识点:如何理解Java函数式编程?
  5. netty系列之:使用netty搭建websocket服务器
  6. devexpress PivotGrid Grand Total
  7. 项目中的加减法--《最后期限》读书笔记(1)
  8. spring ioc原理_这70 道Spring高频面试题,你不好奇吗?
  9. Mysql相关问题收集
  10. Java的jdk在win10安装配置环境变量
  11. python 图像宽度_正确的方法和Python包可以找到图像的宽度
  12. Visual Studio 代码提示选不中
  13. hbase中为何不能向表中插入数据_MySQL数据库中表记录的增、删和改操作
  14. SpringBoot整合腾讯云COS
  15. 迅雷跃居全球BT市场第一
  16. 【跨境电商】EDM邮件营销完整指南(一):概念,区别与优势
  17. studio无法重命名(can not rename root module)
  18. 微信分享之分享图片/分享图标不能显示
  19. 使用FileZilla连接失败
  20. 什么是云计算, 什么是 IaaS, PaaS, SaaS

热门文章

  1. 使用Exchange 2007的几个注意事项
  2. VS2010小Bug:找不到System.Web.Extensions.dll引用
  3. HALCON示例程序color_pieces.hdev通过MLP训练器对彩色棋子进行分类识别
  4. matlab常用函数——数据类型函数
  5. 医学影像后处理服务器系统的特点,【CT影像系统工作站怎么用】CT影像系统工作站好不好_使用技巧-ZOL软件百科...
  6. 三维家可以导入别人的方案吗_广州深圳天津形位公差检测三维缺陷检测服务
  7. js reduce实现中间件_js数组高阶方法reduce经典用法代码分享
  8. 没有与参数列表匹配的 重载函数 strcpy_s 实例_Zemax光学设计实例(84)Ftheta扫描平场透镜的设计...
  9. 博客园修改页面显示样式css
  10. [Python] isinstance() for checking object type