• Stars
    star
    894
  • Rank 51,071 (Top 2 %)
  • Language
    Java
  • License
    Apache License 2.0
  • Created about 3 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

使用注解优雅记录系统日志,操作日志,后端埋点等,支持SpEL表达式,自定义上下文,自定义函数,实体类DIFF等其他高阶处理。

log-record


注意:本仓库最初创作灵感来源于美团技术博客 ,若您需要寻找的是文中所提到的代码仓库,可以跳转这里 。本仓库从零实现了原文中描述的大部分特性,并吸取生产环境大量实践和用户反馈,随着持续稳定的维护和更新,期望给用户提供更多差异化的功能。

通过Java注解优雅的记录操作日志,并支持SpEL表达式,自定义上下文,自定义函数,实体类DIFF等功能,最终日志可由用户自行处理或推送至指定消息队列。

采用SpringBoot Starter的方式,只需一个依赖。

<dependency>
    <groupId>cn.monitor4all</groupId>
    <artifactId>log-record-starter</artifactId>
    <version>{最新版本号}</version>
</dependency>

最新版本号请查阅Maven公共仓库

只需一句注解,日志轻松记录,不侵入业务逻辑:

@OperationLog(bizType = "'followerChange'", bizId = "#request.orderId", msg = "'用户' + #queryUserName(#request.userId) + '修改了订单的跟进人:从' + #queryOldFollower(#request.orderId) + '修改到' + #request.newFollower")
public Response<T> function(Request request) {
  // 业务执行逻辑
}

项目背景

大家一定见过下图的操作日志:

在代码层面,如何优雅的记录上面的日志呢?

能想到最粗暴的方式,封装一个操作日志记录类,如下:

String template = "用户%s修改了订单的跟进人:从“%s”修改到“%s”"
LogUtil.log(orderNo, String.format(tempalte, "张三", "李四", "王五"),  "张三")

这种方式会导致业务代码被记录日志的代码侵入,对于代码的可读性和可维护性来说是一个灾难。

这个方式显然不够优雅,让我们试试使用注解:

@OperationLog(bizType = "'followerChange'", bizId = "'20211102001'", msg = "'用户 张三 修改了订单的跟进人:从 李四 修改到 王五'")
public Response<T> function(Request request) {
  // 业务执行逻辑
}

日志的记录被放到了注解,对业务代码没有侵入。

但是新的问题来了,我们该如何把订单ID、用户信息、数据库里的旧地址、函数入参的新地址传递给注解呢?

SpringSpEL表达式(Spring Expression Language 可以帮助我们,通过引入SpEL表达式,我们可以获取函数的入参。这样我们就可以对上面的注解进行修改:

  • 订单ID:#request.orderId
  • 新地址"王五":#request.newFollower
@OperationLog(bizType = "'followerChange'", bizId = "#request.orderId", msg = "'用户 张三 修改了订单的跟进人:从 李四 修改到' + #request.newFollower")
public Response<T> function(Request request) {
  // 业务执行逻辑
}

如此一来,订单ID和地址的新值就可以通过解析入参动态获取了。

问题还没有结束,通常我们的用户信息(user),以及老的跟进人(oldFollower),是需要在方法中查询后才能获取,入参里一般不会包含这些数据。

解决方案也不是没有,我们创建一个可以保存上下文的LogRecordContext变量,让用户手动传递代码中计算出来的值,再交给SpEL解析 ,代码如下

@OperationLog(bizType = "'followerChange'", bizId = "#request.orderId", msg = "'用户' + #userName + '修改了订单的跟进人:从' + #oldFollower + '修改到' + #request.newFollower")
public Response<T> function(Request request) {
  // 业务执行逻辑
  ...
  // 手动传递日志上下文:用户信息 地址旧值
  LogRecordContext.putVariable("userName", queryUserName(request.getUserId()));
  LogRecordContext.putVariable("oldFollower", queryOldFollower(request.getOrderId()));
}

什么?你说这不就又侵入了业务逻辑了么?

确实是的,不过这种方法足够便捷易懂,并不会有什么理解的困难。

但是对于有“强迫症”的同学,这样的实现还是不够优雅,我们可以用SpEL支持的自定义函数,解决这个问题。

SpEL支持在表达式中传入用户自定义函数,我们将queryUserNamequeryOldFollower这两个函数提前放入SpEL的解析器中,SpEL在解析表达式时,会执行对应函数。

最终,我们的注解变成了这样,并且最终记录了日志:

@OperationLog(bizType = "'followerChange'", bizId = "#request.orderId", msg = "'用户' + #queryUserName(#request.userId) + '修改了订单的跟进人:从' + #queryOldFollower(#request.orderId) + '修改到' + #request.newFollower")
public Response<T> function(Request request) {
  // 业务执行逻辑
}

用户 张三 修改了订单的跟进人:从 李四 修改到 王五

以上便是本库的大致实现原理。

项目介绍

本库帮助你通过注解优雅地记录项目中的操作日志,对业务代码无侵入。

此外,你可以方便地将所有日志推送到下列数据管道:

  1. 本地处理
  2. 发送至RabbitMQ
  3. 发送至RocketMQ
  4. 发送至SpringCloud Stream

本项目特点:

  • 快速接入:使用Spring Boot Starter实现,用户直接在pom.xml引入依赖即可使用
  • 业务无侵入:无需侵入业务代码,日志切面发生任何异常不会影响原方法执行
  • SpEL解析:支持SpEL表达式
  • 实体类Diff:支持相同甚至不同类对象的Diff
  • 条件注解:满足Condition条件后才记录日志,通过SpEL进行解析
  • 自定义上下文:支持手动传递键值对,通过SpEL进行解析
  • 自定义函数:支持注册自定义函数,通过SpEL进行解析
  • 全局操作人ID:自定义操作人ID获取逻辑
  • 指定日志数据管道:自定义操作日志处理逻辑(写数据库,TLog等..)
  • 支持重复注解:同一个方法上可以写多个操作日志注解
  • 支持自动重试和兜底处理:支持配置重试次数和处理失败兜底逻辑SPI
  • 支持控制切面执行时机(方法执行前后),支持自定义执行成功判断逻辑,等等....等你来发掘

日志实体内包含:

logId:生成的UUID
bizId:业务唯一ID
bizType:业务类型
exception:函数执行失败时写入异常信息
operateDate:操作执行时间
success:函数是否执行成功
msg:日志内容
tag:自定义标签
returnStr: 方法执行成功后的返回值(字符串或JSON化实体)
executionTime:方法执行耗时(单位:毫秒)
extra:额外信息
operatorId:操作人ID
List<diffDTO>: 实体类对象Diff数据,包括变更的字段名,字段值,类名等

日志实体复杂示例:

{
  "bizId":"1",
  "bizType":"testObjectDiff",
  "executionTime":0,
  "extra":"【用户工号】从【1】变成了【2】 【name】从【张三】变成了【李四】",
  "logId":"38f7f417-2cc3-40ed-8c98-2fe3ee057518",
  "msg":"【用户工号】从【1】变成了【2】 【name】从【张三】变成了【李四】",
  "operateDate":1651116932299,
  "operatorId":"操作人",
  "returnStr":"{\"id\":1,\"name\":\"张三\"}",
  "success":true,
  "exception":null,
  "tag":"operation",
  "diffDTOList":[
    {
      "diffFieldDTOList":[
        {
          "fieldName":"id",
          "newFieldAlias":"用户工号",
          "newValue":2,
          "oldFieldAlias":"用户工号",
          "oldValue":1
        },
        {
          "fieldName":"name",
          "newValue":"李四",
          "oldValue":"张三"
        }],
      "newClassAlias":"用户信息实体",
      "newClassName":"cn.monitor4all.logRecord.test.bean.TestUser",
      "oldClassAlias":"用户信息实体",
      "oldClassName":"cn.monitor4all.logRecord.test.bean.TestUser"
    },
    {
      "diffFieldDTOList":[
        {
          "fieldName":"id",
          "newFieldAlias":"用户工号",
          "newValue":2,
          "oldFieldAlias":"用户工号",
          "oldValue":1
        },
        {
          "fieldName":"name",
          "newValue":"李四",
          "oldValue":"张三"
        }],
      "newClassAlias":"用户信息实体",
      "newClassName":"cn.monitor4all.logRecord.test.bean.TestUser",
      "oldClassAlias":"用户信息实体",
      "oldClassName":"cn.monitor4all.logRecord.test.bean.TestUser"
    }]
}

使用方法

只需要简单的三步:

第一步: SpringBoot项目中引入依赖

<dependency>
    <groupId>cn.monitor4all</groupId>
    <artifactId>log-record-starter</artifactId>
    <version>{最新版本号}</version>
</dependency>

推荐使用 >= 1.5.0版本

第二步: 添加数据源配置

支持推送日志数据至:

  1. 本地直接处理消息
  2. RabbitMQ
  3. RocketMQ
  4. SpringCloud Stream

1. 本地直接处理消息

若只需要在同一应用内处理日志信息,只需要实现接口IOperationLogGetService,便可对日志进行处理。

@Component
public class CustomFuncTestOperationLogGetService implements IOperationLogGetService {
    @Override
    public void createLog(LogDTO logDTO) {
        log.info("logDTO: [{}]", JSON.toJSONString(logDTO));
    }
}

2. RabbitMQ

配置好RabbitMQ的发送者

log-record.data-pipeline=rabbitMq
log-record.rabbit-mq-properties.host=localhost
log-record.rabbit-mq-properties.port=5672
log-record.rabbit-mq-properties.username=admin
log-record.rabbit-mq-properties.password=xxxxxx
log-record.rabbit-mq-properties.queue-name=logRecord
log-record.rabbit-mq-properties.routing-key=
log-record.rabbit-mq-properties.exchange-name=logRecord

3. RocketMQ

配置好RocketMQ的发送者

log-record.data-pipeline=rocketMq
log-record.rocket-mq-properties.topic=logRecord
log-record.rocket-mq-properties.tag=
log-record.rocket-mq-properties.group-name=logRecord
log-record.rocket-mq-properties.namesrv-addr=localhost:9876

4. Stream

配置好stream

log-record.data-pipeline=stream
log-record.stream.destination=logRecord
log-record.stream.group=logRecord
# 为空时 默认为spring.cloud.stream.default-binder指定的Binder
log-record.stream.binder=
# rocketmq binder例子
spring.cloud.stream.rocketmq.binder.name-server=127.0.0.1:9876
spring.cloud.stream.rocketmq.binder.enable-msg-trace=false

第三步: 在需要记录系统操作的方法上,添加注解

@OperationLog(bizType = "'followerChange'", bizId = "#request.orderId", msg = "'用户 张三 修改了订单的跟进人:从 李四 修改到' + #request.newFollower")
public Response<T> function(Request request) {
  // 业务执行逻辑
}

进阶特性

SpEL的使用

SpELSpring实现的标准的表达式语言,具体的使用可以学习官方文档或者自行搜索资料,入门非常的简单,推荐几篇文章:

需要注意的是,@OperationLog注解中,除了executeBeforeFuncrecordReturnValue两个boolean类型的参数,其他的参数均需要严格遵循SpEL表达式语法。 `

举例来说,bizType中我们经常会填入常量,例如订单创建orderCreate, 订单修改orderModify

SpEL表达式中,若传入bizType="orderCreate",SpEL会解析失败,因为纯字符串会被认为是一个方法名,导致SpEL找不到方法而报错,需要使用bizType="'orderCreate'",才能被正确解析。

有时,我们会用枚举值和常量值来规范bizType等参数,合理写法如下:

@Getter
@AllArgsConstructor
public enum TestEnum {

    TYPE1("type1", "枚举1"),
    TYPE2("type2", "枚举2");

    private final String key;
    private final String name;

}
public class TestConstant {

    public static final String TYPE1 = "type1";
    public static final String TYPE2 = "type2";

}
@OperationLog(bizId = "'1'", bizType = "T(cn.monitor4all.logRecord.test.bean.TestConstant).TYPE1")
@OperationLog(bizId = "'2'", bizType = "T(cn.monitor4all.logRecord.test.bean.TestEnum).TYPE1")
@OperationLog(bizId = "'3'", bizType = "T(cn.monitor4all.logRecord.test.bean.TestEnum).TYPE1.key")
@OperationLog(bizId = "'4'", bizType = "T(cn.monitor4all.logRecord.test.bean.TestEnum).TYPE1.name")

注意:bizTypetag参数在 >= 1.2.0版本以后才要求严格遵循SpEL表达式,<= 1.1.x以下版本均为直接填写字符串,不支持SpEL解析。

自定义SpEL解析顺序

在默认配置下,注解切面的逻辑在方法执行之后才会执行,这样会带来一个问题,如果在方法内部修改了方法参数,SpEL解析后取值就变成了改变后的值。

可以使用LogRecordContext写入旧值,避免这个问题,只是有一定代码侵入性。

为了满足一些特殊需求,注解中提供boolean参数executeBeforeFunc若设置为true,则会在方法执行前先解析SpEL参数。 这样也会带来负作用,方法内写入的数值,比如自定义上下文,就不再参与SpEL解析了。

方法上加上注解:

@OperationLog(bizId = "#keyInBiz", bizType = "'testExecuteBeforeFunc1'", executeBeforeFunc = true)
@OperationLog(bizId = "#keyInBiz", bizType = "'testExecuteAfterFunc'")
@OperationLog(bizId = "#keyInBiz", bizType = "'testExecuteBeforeFunc2'", executeBeforeFunc = true)
public void testExecuteBeforeFunc() {
    LogRecordContext.putVariable("keyInBiz", "valueInBiz");
}

调用方法:

testService.testExecuteBeforeFunc();

得到结果:

[{"bizId":null, "bizType":"testExecuteBeforeFunc1","diffDTOList":[],"executionTime":0,"extra":"","logId":"8cbed2fc-bb2d-48a7-b9ec-f28e99773151","msg":"","operateDate":1651144119444,"operatorId":"操作人","returnStr":"null","success":true,"tag":"operation"}]
[{"bizId":null, "bizType":"testExecuteBeforeFunc2","diffDTOList":[],"executionTime":0,"extra":"","logId":"a130b60c-791c-4c6f-812e-0475de4b38d2","msg":"","operateDate":1651144119444,"operatorId":"操作人","returnStr":"null","success":true,"tag":"operation"}]
[{"bizId":"valueInBiz","bizType":"testExecuteAfterFunc","diffDTOList":[],"executionTime":0,"extra":"","logId":"80af92f5-8e4a-489e-a626-83f2a696fe71","msg":"","operateDate":1651144119444,"operatorId":"操作人","returnStr":"null","success":true,"tag":"operation"}]

内置自定义函数和自定义参数

  1. 可以直接使用的自定义参数:
  • _return:原方法的返回值
  • _errorMsg:原方法的异常信息(throwable.getMessage()

使用示例:

@OperationLog(bizId = "'1'", bizType = "'testDefaultParamReturn'", msg = "#_return")

注意:_return_errorMsg均为方法执行后才赋值的参数,所以若executeBeforeFunc=true(设置为方法执行前执行日志切面),则这两个值为null

  1. 可以直接使用的自定义函数:
  • _DIFF:详见下方 实体类Diff 小节

根据条件记录日志

@OperationLog注解拥有字段condition,用户可以使用SpEL表达式来决定该条日志是否记录。

方法上加上注解:

@OperationLog(bizId = "'1'", bizType = "'testCondition1'", condition = "#testUser != null")
@OperationLog(bizId = "'2'", bizType = "'testCondition2'", condition = "#testUser.id == 1")
@OperationLog(bizId = "'3'", bizType = "'testCondition3'", condition = "#testUser.id == 2")
public void testCondition(TestUser testUser) {
}

调用方法:

testService.testCondition(new TestUser(1, "张三"));

上述注解中,只有前两条注解满足condition条件,会输出日志。

全局操作人信息获取

大部分情况下,操作人ID往往不会在方法参数中传递,更多会是查询集团内BUC信息、查询外部服务、查表等获取。所以开放了SPI,只需要实现接口IOperationLogGetService,便可以统一注入操作人ID。

@Component
public class IOperatorIdGetServiceImpl implements IOperatorIdGetService {

    @Override
    public String getOperatorId() {
        // 查询操作人信息
        return "张三";
    }
}

注意:若实现了接口后仍在注解手动传入OperatorID,则以传入的OperatorID优先。

自定义上下文

直接引入类LogRecordContext,放入键值对。

@OperationLog(bizType = "'followerChange'", bizId = "#request.orderId", msg = "'用户' + #userName + '修改了订单的跟进人:从' + #oldFollower + '修改到' + #request.newFollower")
public Response<T> function(Request request) {
  // 业务执行逻辑
  ...
  // 手动传递日志上下文:用户信息 地址旧值
  LogRecordContext.putVariable("userName", queryUserName(request.getUserId()));
  LogRecordContext.putVariable("oldFollower", queryOldFollower(request.getOrderId()));
}

LogRecordContext内部使用TransmittableThreadLocal,在线程池中也可以读取到主线程的ThreadLocal。

自定义函数

@LogRecordFunc注解申明在需要注册到SpEL的自定义函数上,参与SpEL表达式的运算。

注意,需要在类上也声明@LogRecordFunc,否则无法找到该函数。

@LogRecordFunc可以添加参数value,实现自定义方法别名,若不添加,则默认不需要写前缀。

分为静态和非静态方法两种处理方式。

静态自定义方法是SpEL天生支持的,所以写法如下:

@LogRecordFunc("CustomFunctionStatic")
public class CustomFunctionStatic {

    @LogRecordFunc("testStaticMethodWithCustomName")
    public static String testStaticMethodWithCustomName(){
        return "testStaticMethodWithCustomName";
    }

    @LogRecordFunc
    public static String testStaticMethodWithoutCustomName(){
        return "testStaticMethodWithoutCustomName";
    }

}

上述代码中,注册的自定义函数名为CustomFunctionStatic_testStaticMethodWithoutCustomNameCustomFunctionStatic_testStaticMethodWithoutCustomName,若类上的注解更改为@LogRecordFunc("test"),则注册的自定义函数名为testStaticMethodWithCustomNametestStaticMethodWithoutCustomName

非静态的自定义方法(比如直接调用SpringService)写法如下:

@Service
@Slf4j
@LogRecordFunc("CustomFunctionService")
public class CustomFunctionService {

    @LogRecordFunc
    public TestUser testUser() {
        return new TestUser(1, "asd");
    }
}

其原理主要是依靠我们框架内部转换,将非静态方法需要包装为静态方法再传给SpEL。原理详见#PR25

注意:所有自定义函数可在应用启动时的日志中找到

2022-06-09 11:35:18.672  INFO 73757 --- [           main] c.a.i.l.f.CustomFunctionRegistrar        : LogRecord register custom function [public static java.lang.String cn.monitor4all.logRecord.test.service.CustomFunctionStaticService.testStaticMethodWithCustomName()] as name [CustomFunctionStatic_testStaticMethodWithoutCustomName]
2022-06-09 11:35:18.672  INFO 73757 --- [           main] c.a.i.l.f.CustomFunctionRegistrar        : LogRecord register custom function [public static java.lang.String cn.monitor4all.logRecord.test.service.CustomFunctionStaticService.testStaticMethodWithoutCustomName()] as name [CustomFunctionStatic_testStaticMethodWithoutCustomName]
2022-06-09 11:35:18.672  INFO 73757 --- [           main] c.a.i.l.f.CustomFunctionRegistrar        : LogRecord register custom function [public static java.lang.String cn.monitor4all.logRecord.function.CustomFunctionObjectDiff.objectDiff(java.lang.Object,java.lang.Object)] as name [_DIFF]

注解中使用:

@OperationLog(bizId = "#CustomFunctionStatic_testStaticMethodWithCustomName()", bizType = "'testStaticMethodWithCustomName'")
@OperationLog(bizId = "#CustomFunctionStatic_testStaticMethodWithoutCustomName()", bizType = "'testStaticMethodWithoutCustomName'")
public void testCustomFunc() {
}

自定义原方法是否执行成功

@OperationLog注解中有success参数,用于根据返回体或其他情况下自定义日志实体中的success字段。

默认情况下,方法是否执行成功取决于是否抛出异常,若未抛出异常,默认为方法执行成功。

但很多时候,我们的方法执行成功可能取决于方法内部调用的接口的返回值,如下所示:

@OperationLog(
        success = "#isSuccess",
        bizId = "#request.trade.id",
        bizType = "'createOrder'",
    )
@Override
public Result<Void> createOrder(Request request) {
    try {
        Response response = tradeCreateService.create(request);
        LogRecordContext.putVariable("isSuccess", response.getIsSuccess());
        return Result.ofSuccess();
    } catch (Exception e) {
        return Result.ofSysError();
    }
}

可以通过接口返回的response.getIsSuccess()来表名该创建订单方法是否执行成功。

实体类Diff

支持两个对象(相同或者不同的类对象皆可)对象的Diff

有如下注解:

  • @LogRecordDiffField:在字段上申明@LogRecordDiffField(alias = "用户工号", ignored = true)alias别名为可选字段。 ignored为可选字段,默认为false,若为true,则该字段不参与DIFF
  • @LogRecordDiffObject:在类上允许可以申明@LogRecordDiffObject(alias = "用户信息实体")alias别名为可选字段,默认类下所有字段会进行DIFF,可通过enableAllFields手动关闭,关闭后等于该注解只用于获取类别名。

类对象使用示例:

@LogRecordDiffObject(alias = "用户信息实体")
public class TestUser {
    private Integer id;
    private String name;
    private String job;
}

或者单独为类中的字段DIFF:

public class TestUser {
    @LogRecordDiffField(alias = "用户工号")
    private Integer id;
    @LogRecordDiffField(alias = "用户工号", ignored = true)
    private String name;
}

@OperationLog注解上,可以通过调用内置实现的自定义函数_DIFF,传入两个对象即可拿到Diff结果。

@OperationLog(bizId = "'1'", bizType = "'testObjectDiff'", msg = "#_DIFF(#oldObject, #testUser)", extra = "#_DIFF(#oldObject, #testUser)")
public void testObjectDiff(TestUser testUser) {
    LogRecordContext.putVariable("oldObject", new TestUser(1, "张三"));
}

比较完成后的结果在日志实体中以diffDTO实体呈现。

{
  "diffFieldDTOList":[
    {
      "fieldName":"id",
      "newFieldAlias":"用户工号",
      "newValue":2,
      "oldFieldAlias":"用户工号",
      "oldValue":1
    },
    {
      "fieldName":"name",
      "newValue":"李四",
      "oldValue":"张三"
    }],
  "newClassAlias":"用户信息实体",
  "newClassName":"cn.monitor4all.logRecord.test.bean.TestUser",
  "oldClassAlias":"用户信息实体",
  "oldClassName":"cn.monitor4all.logRecord.test.bean.TestUser"
}

调用方法:

testService.testObjectDiff(new TestUser(2, "李四"));

最终得到的日志消息实体logDTO

{
  "bizId":"1",
  "bizType":"testObjectDiff",
  "executionTime":0,
  "extra":"【用户工号】从【1】变成了【2】 【name】从【张三】变成了【李四】",
  "logId":"38f7f417-2cc3-40ed-8c98-2fe3ee057518",
  "msg":"【用户工号】从【1】变成了【2】 【name】从【张三】变成了【李四】",
  "operateDate":1651116932299,
  "operatorId":"操作人",
  "returnStr":"{\"id\":1,\"name\":\"张三\"}",
  "success":true,
  "exception":null,
  "tag":"operation",
  "diffDTOList":[
    {
      "diffFieldDTOList":[
        {
          "fieldName":"id",
          "newFieldAlias":"用户工号",
          "newValue":2,
          "oldFieldAlias":"用户工号",
          "oldValue":1
        },
        {
          "fieldName":"name",
          "newValue":"李四",
          "oldValue":"张三"
        }],
      "newClassAlias":"用户信息实体",
      "newClassName":"cn.monitor4all.logRecord.test.bean.TestUser",
      "oldClassAlias":"用户信息实体",
      "oldClassName":"cn.monitor4all.logRecord.test.bean.TestUser"
    },
    {
      "diffFieldDTOList":[
        {
          "fieldName":"id",
          "newFieldAlias":"用户工号",
          "newValue":2,
          "oldFieldAlias":"用户工号",
          "oldValue":1
        },
        {
          "fieldName":"name",
          "newValue":"李四",
          "oldValue":"张三"
        }],
      "newClassAlias":"用户信息实体",
      "newClassName":"cn.monitor4all.logRecord.test.bean.TestUser",
      "oldClassAlias":"用户信息实体",
      "oldClassName":"cn.monitor4all.logRecord.test.bean.TestUser"
    }]
}

可以通过Spring配置,忽略对比的新旧对象中值为null的字段,形如:

log-record.diff-ignore-new-object-null-value=true # 忽略新对象中null值字段,默认为false
log-record.diff-ignore-old-object-null-value=true # 忽略旧对象中null值字段,默认为false

此外,可以通过Spring配置自定义DIFF的标准输出格式,形如:

log-record.diff-msg-format=(默认值为【${_fieldName}】从【${_oldValue}】变成了【${_newValue}】)
log-record.diff-msg-separator=(默认值为" "空格)

还支持同一个注解中多次调用_DIFF, 如下:

/**
 * 测试实体类DIFF:使用多个_DIFF
 */
@OperationLog(bizId = "'1'", bizType = "'testMultipleDiff'", msg = "'第一个DIFF:' + #_DIFF(#oldObject1, #testUser) + '第二个DIFF' + #_DIFF(#oldObject2, #testUser)")
public void testMultipleDiff(TestUser testUser) {
    LogRecordContext.putVariable("oldObject1", new TestUser(1, "张三"));
    LogRecordContext.putVariable("oldObject2", new TestUser(3, "王五"));
}

注意:目前DIFF功能支持完全不同的类之间进行DIFF,对于同名的基础类型,进行equals对比,对于同名的非基础类型,则借用fastjsontoJSON能力,转为JSONObject进行对比,本质上是将对象映射为map进行map.equals

日志处理重试次数及兜底函数配置

无论是本地处理日志,或者发送到消息管道处理日志,都会存在处理异常需要重试的场景。可以通过properties配置:

log-record.retry.retry-times=5  # 默认为0次重试,即日志处理方法只执行1次

配置后框架会重新执行createLog直至达到最大重试次数。

若超过了重试次数,可以通过实现SPI接口 cn.monitor4all.logRecord.service.LogRecordErrorHandlerService 来进行兜底逻辑处理,这里将本地日志处理和消息管道兜底处理分开了。

@Component
public class LogRecordErrorHandlerServiceImpl implements LogRecordErrorHandlerService {

    @Override
    public void operationLogGetErrorHandler() {
        log.error("operation log get service error reached max retryTimes!");
    }

    @Override
    public void dataPipelineErrorHandler() {
        log.error("data pipeline send log error reached max retryTimes!");
    }
}

重复注解

@OperationLog(bizId = "#testClass.testId", bizType = "'testType1'", msg = "#testFunc(#testClass.testId)")
@OperationLog(bizId = "#testClass.testId", bizType = "'testType2'", msg = "#testFunc(#testClass.testId)")
@OperationLog(bizId = "#testClass.testId", bizType = "'testType3'", msg = "'用户将旧值' + #old + '更改为新值' + #testClass.testStr")

我们还加上了重复注解的支持,可以在一个方法上同时加多个@OperationLog会保证按照@OperationLog从上到下的顺序输出日志

消息分发线程池配置

在组装好logDTO后,默认使用线程池对消息进行分发,发送至本地监听函数或者消息队列发送者。

注意:logDTO的组装在切面中,该切面仍然在函数执行的线程中运行。

可以使用如下配置:

log-record.thread-pool.pool-size=4(线程池核心线程大小 默认为4)
log-record.thread-pool.enabled=true(线程池开关 默认为开启 若关闭则使用主线程进行消息处理发送)

关闭使用线程池后,所有发送由主线程执行,带来的副作用是大量日志并发发送,会降低主线程处理效率。

函数返回值记录开关

@OperationLog注解提供布尔值recordReturnValue()用于是否开启记录函数返回值,默认关闭,防止返回值实体过大,造成序列化时性能消耗过多。

操作日志数据表结构推荐

以MySQL表为例:

CREATE TABLE `operation_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `biz_id` varchar(128) NOT NULL COMMENT '业务ID',
  `biz_type` varchar(64) DEFAULT NULL COMMENT '业务类型',
  `tag` varchar(64) DEFAULT NULL COMMENT '标签',
  `operation_date` datetime DEFAULT NULL COMMENT '操作执行时间',
  `msg` varchar(512) DEFAULT NULL COMMENT '操作内容',
  `extra` varchar(512) DEFAULT NULL COMMENT '附加信息',
  `operation_status` tinyint(4) DEFAULT NULL COMMENT '操作结果状态',
  `operation_time` int(11) DEFAULT NULL COMMENT '操作耗时',
  `content_return` varchar(512) COMMENT '方法返回内容',
  `content_exception` varchar(512) COMMENT '方法异常内容',
  `operator_id` varchar(32) DEFAULT NULL COMMENT '操作人ID',
  `operator_name` varchar(32) DEFAULT NULL COMMENT '操作人姓名',
  PRIMARY KEY (`id`),
  KEY `idx_biz_id` (`biz_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

让注解支持IDEA自动补全

在自定义注解想实现类似@Cacheable的自动补全,其实是IDEAIDE自己的支持,可以在配置中将本二方库的注解添加上去,从而支持自动补全和SpEL表达式校验。

应用场景

以下罗列了一些实际的应用场景,包括我业务中实际使用,并且已经上线使用的场景。

操作日志

如最上面一张CRM系统的图描述的那样,在用户进行了编辑操作后,拿到用户操作的数据,执行日志写入。

系统日志

操作日志是主要的功能,当然也可以兼顾一些系统日志记录的操作,比如只是想简单记录方法执行时间,出入参等,也可以通过该库轻松做到。

通知

应用之间通过关键操作的日志消息,互相通知。

跨应用数据聚合

在多个应用中,如果需要做行为相同的业务逻辑,完全可以在各个系统中通过该库将数据发送到同一个消息队列中,再进行统一处理。

附录:Demo

最后,肯定有小伙伴希望有一个完整的使用Demo,这就奉上!

完整客户端Demo项目:

https://github.com/qqxx6661/systemLog

Release Note

Release

配套教程文章

关注我

公众号:后端技术漫谈

全网博客名:蛮三刀酱

More Repositories

1

miaosha

从零开始搭建秒杀系统 由浅入深,配合博客入门教程文章食用,风味极佳。
HTML
892
star
2

springboot-websocket-demo

使用WebSocket+SpringBoot搭建简易的多人聊天室 由浅入深,配合博客入门教程文章食用,风味独特。
Java
258
star
3

Price-monitor

某东商品价格监控:自定义商品价格,降价邮件/微信提醒。技术:Python爬虫/IP代理池/JS接口爬取/Selenium页面爬取
Python
94
star
4

flask_yzd

旧版某东监控网站前后端,轻量级Flask网站,可用作学习Flask
Python
72
star
5

websocket-game-demo

WebSocket实时多人答题对战游戏 服务端
Java
46
star
6

scrapy_yzd

该项目为scrapy框架脚手架,整合了自动切换agent,自动切换代理ip等中间件,可以下载后自行编写爬虫。 支持: 豆瓣电影,某东商品信息(名称价格等)。
Python
33
star
7

live_comment_control_stream

接收弹幕指令并控制电脑操作 Python3+Redis+PyAutoGUI
Python
30
star
8

springcloud-for-noob

Spring Cloud新手入门实战教程,采用Springboot2,由浅入深,配合博客入门教程文章食用,风味极佳。
Java
27
star
9

springcloud_gateway_demo

springcloud gateway demo for noob
Java
21
star
10

systemLog

使用注解优雅的记录系统日志,操作日志等,并通过数据管道传递至数据库,消息队列。 附完整Demo项目。
Java
20
star
11

tmall_ssm

简介:模仿天猫商城页面,实现Java后端+JSP页面 实现功能: 前台:天猫首页,注册登录页,分类页,查询页,购物车,支付页,我的订单页 后台:产品分类管理,产品管理,用户管理,订单管理,购物车管理
Java
18
star
12

Price-Monitor-py2

某东价格监控,用户设置监控商品和预期价格,实时监控,低于预期价格,自动发送邮件提醒用户/Python爬虫/Python2+requests+sqlite+代理池
Python
8
star
13

ML-SVM

python+opencv3 视频监控实时数据提取,目标追踪
Python
7
star
14

Price-monitor-php

某东价格监控,用户设置监控商品、预期价格,一旦低于预期价格,自动发送邮件提醒用户抢购。老版本备份:Python+php
Python
6
star
15

scrapy-redis-demo

Scrapy和Scrapy-redis的整合,完美上手的脚手架
Python
5
star
16

awesome-utils

短小精悍的后端开发工具类大全
Java
5
star
17

Price-monitor-backend

Price-monitor 价格监控网站后端代码开源
Java
4
star
18

Cryptography_exercise

密码学练习题
TeX
3
star
19

Trajectory-prediction

Person re-identification and trajectory prediction using multiple non-overlap cameras
Python
3
star
20

LDSM

Layered Data Security Model for Edge Computing
Python
3
star
21

SHAMC_Yang

A secure cloud database model based on multi-cloud
C#
2
star
22

spring-mail-sender

SpringBoot实现的邮件发送接口API,提供邮件发送服务
Java
2
star
23

tinySpring

动手实现一个简单的Spring框架,包括IOC和AOP
Java
2
star
24

awesome-python

让人大喊哇塞的Python小工具:包括文件处理,Markdown处理等
Python
1
star
25

python_practise

Python作业题目
Python
1
star