Code Monkey home page Code Monkey logo

log-record's Introduction

log-record


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

通过Java注解优雅的记录操作日志,并支持SpEL表达式,自定义上下文,自定义函数,实体类DIFF等功能,最终日志可由用户自行采集并处理,或推送至预配置的消息队列,支持SpringBoot1&2&3(JDK8~JDK21)。

采用SpringBoot Starter的方式,只需一个依赖,一句注解,日志轻松记录,不侵入业务逻辑:

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

SpringBoot1&SpringBoot2(JDK8+)请引用:

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

SpringBoot3(JDK17+)请引用:

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

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

项目背景

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

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

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

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) {
  // 业务执行逻辑
}

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

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

项目介绍

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

本项目特点:

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

日志实体(LogDTO)内包含:

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项目中引入依赖

SpringBoot1&SpringBoot2(JDK8+)请引用:

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

SpringBoot3(JDK17+)请引用:

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

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

推荐使用 >= 1.6.x版本

第二步: 配置日志处理方式

支持处理方式:

  1. 自定义采集处理
  2. 直接发送至RabbitMQ
  3. 直接发送至RocketMQ
  4. 直接发送至SpringCloud Stream

1. 自定义采集处理

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

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

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. 直接发送至SpringCloud Stream

配置SpringCloud 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

非静态自定义方法:

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

在1.6.x版本之前,部分版本(1.5.x)支持非静态自定义函数,但由于其大量使用反射,写法较为Hack,兼容性不佳(在JDk11+后反射限制更加严格),在1.6.x+ 版本后删除,仅支持静态方法。

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

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从上到下的顺序输出日志

自定义消息线程池

starter提供了如下配置:

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

在组装好logDTO后,默认会使用线程池对消息进行处理,发送至本地监听函数或者消息队列发送者,也可以通过配置关闭线程池,让主线程执行全部消息处理逻辑。

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

默认线程池配置如下(拒绝策略为丢弃):

return new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), THREAD_FACTORY, new ThreadPoolExecutor.AbortPolicy());

此外,还提供了用户传入自定义线程池的方式,用户可自行实现cn.monitor4all.logRecord.thread.ThreadPoolProvider,传入线程池。

示例:

public class CustomThreadPoolProvider implements ThreadPoolProvider {

    private static ThreadPoolExecutor EXECUTOR;

    private static final ThreadFactory THREAD_FACTORY = new CustomizableThreadFactory("custom-log-record-");


    private CustomThreadPoolProvider() {
        log.info("CustomThreadPoolProvider init");
        EXECUTOR = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), THREAD_FACTORY, new ThreadPoolExecutor.AbortPolicy());
    }

    @Override
    public ThreadPoolExecutor buildLogRecordThreadPool() {
        return EXECUTOR;
    }
}

函数返回值记录开关

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

非注解方式

在实际业务场景中,很多时候由于注解的限制,无法很好的使用注解记录日志,此时可以使用纯手动的方式进行日志记录。

框架提供了手动记录日志的方法:

cn.monitor4all.logRecord.util.OperationLogUtil

LogRequest logRequest = LogRequest.builder()
        .bizId("testBizId")
        .bizType("testBuildLogRequest")
        .success(true)
        .msg("testMsg")
        .tag("testTag")
        .returnStr("testReturnStr")
        .extra("testExtra")
        // 其他字段
        .build();
OperationLogUtil.log(logRequest);

使用该方式记录日志,注解带来的相关功能则无法使用,如SpEL表达式,自定义函数等。

操作日志数据表结构推荐

以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表达式校验。

SpringBoot3(JDK17+)版本与SpringBoot1&SpringBoot2(JDK8+)版本使用差异

本框架尽可能在不同SpringBoot版本下提供统一的功能和特性,但由于JDk兼容等问题,在使用上仍有一些差异。

在这里列举需要本框架使用者注意的差异:

SpringBoot3无法获取函数入参

由于JDK11+以上收紧了对反射的使用,导致SpringBoot3无法获取函数入参,所以在SpringBoot3版本下,无法使用参数名获取函数入参。

例如在SpringBoot1&SpringBoot2中可以这样做:

@OperationLog(bizId = "#bizId", bizType = "'testBizIdWithSpEL'")
public void testBizIdWithSpEL(String bizId) {
}

但是SpringBoot3中,只能使用p0p1等参数名获取函数入参(参数的绝对位置下标),如下:

@OperationLog(bizId = "#p0", bizType = "'testBizIdWithSpEL'")
public void testBizIdWithSpEL(String bizId) {
}

应用场景

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

操作日志

CRM系统,在用户进行了编辑操作后,拿到用户操作的数据,执行日志写入。

系统日志

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

后端埋点

与系统日志类似,可以记录一些用户操作埋点。

通知

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

Demo

当你觉得用法不熟悉,可以查看单元测试用例,里面有最为详细且最全的使用示例。

另外提供完整SpringBoot2&3 Demo项目:

https://github.com/qqxx6661/systemLog

Release Note

Release

附录

编译注意

由于拆分了父子模块,在不同JDK下,请重新编译log-record-core,再编译对应版本的log-record-starter,否则会导致编译失败(单元测试异常)。

发布版本注意

请将log-record-core, log-record-starter, log-record-springboot3-starter都编译打包发布到Maven公共仓库。

配套教程文章

关注我

公众号:后端技术漫谈

全网博客名:蛮三刀酱

如果觉得该项目对你有用,请点个star,谢谢!

Star History

Star History Chart

log-record's People

Contributors

dependabot[bot] avatar duanluan avatar javaeasyrob avatar oldratlee avatar pumbf avatar qqxx6661 avatar xiersa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

log-record's Issues

项目集成后通过docker部署到服务器javassist.NotFoundException:xxxxx.utils.function.CustomFunctionObjectDiff

tils.function.CustomFunctionObjectDiff
at javassist.ClassPool.get(ClassPool.java:430)
at com.zeekr.speech.config.utils.function.CustomFunctionRegistrar.constructCtClass(CustomFunctionRegistrar.java:122)
at com.zeekr.speech.config.utils.function.CustomFunctionRegistrar.proxy2static(CustomFunctionRegistrar.java:108)
at com.zeekr.speech.config.utils.function.CustomFunctionRegistrar.lambda$setApplicationContext$4(CustomFunctionRegistrar.java:74)
at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608)
at com.zeekr.speech.config.utils.function.CustomFunctionRegistrar.setApplicationContext(CustomFunctionRegistrar.java:45)
at org.springframework.context.support.ApplicationContextAwareProcessor.invokeAwareInterfaces(ApplicationContextAwareProcessor.java:123)
at org.springframework.context.support.ApplicationContextAwareProcessor.postProcessBeforeInitialization(ApplicationContextAwareProcessor.java:100)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:415)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1791)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:755)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:402)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1247)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1236)
at com.zeekr.speech.config.Application.main(Application.java:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)

局部变量有没有优雅方式记录到日志里

大佬你好,感谢提供了这么优秀的日志记录框架
image
我日志中需要记录的bizId是方法中的局部变量,除了通过LogRecordContext.putVariables("id", id),使用函数的spel表达式看样子不太行。

StopWatch 能否在生产环境使用.

是线程不安全,好像不能在生产环境使用吧?

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/StopWatch.html

Simple stop watch, allowing for timing of a number of tasks, exposing total running time and running time for each named task.
Conceals use of [System.nanoTime()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/System.html#nanoTime()), improving the readability of application code and reducing the likelihood of calculation errors.

Note that this object is not designed to be thread-safe and does not use synchronization.

This class is normally used to verify performance during proof-of-concept work and in development, rather than as part of production applications.

As of Spring Framework 5.2, running time is tracked and reported in nanoseconds.

SystemLogAspect 中 原方法执行异常时的logDTOList 添加数据有问题

    // 原方法执行异常
    catch (Throwable throwable) {
        // 方法异常执行后日志切面
        try {
            if (stopWatch != null) {
                stopWatch.stop();
                executionTime = stopWatch.getTotalTimeMillis();
            }
            // 在LogRecordContext中写入执行后信息
            LogRecordContext.putVariable(LogRecordContext.CONTEXT_KEY_NAME_ERROR_MSG, throwable.getMessage());
            for (OperationLog annotation : annotations) {
                if (!annotation.executeBeforeFunc()) {
                    logDtoMap.put(annotation, resolveExpress(annotation, pjp));
                }
            }
            // 写入异常执行后日志
            logDTOList = new ArrayList<>(logDtoMap.values());
            logDTOList.forEach(logDTO -> {                                      //  此处的是否有问题??
                logDTO.setSuccess(false);
                logDTO.setException(throwable.getMessage());
            });
        } catch (Throwable throwableAfterFuncFailure) {
            log.error("OperationLogAspect doAround after function failure, error:", throwableAfterFuncFailure);
        }
        // 抛出原方法异常
        throw throwable;
    } finally {

LogRecordFunc 注解是否只支持标注static方法

大多情况下自定义的函数为了查old的值都会调用spring容器中的方法去获取,在@component注解标记的类中使用LogRecordFunc注解会报空指针,代理类获取不到Annotation,微调后发现非静态方法无法放到StandardEvaluationContext里

自定义 log-record.diff-msg-format 结果错误

版本:v1.5.1

application.yml:

log-record:
  # Diff 时忽略新对象中 null 值字段,默认为 false
  diff-ignore-new-object-null-value: true
  # Diff 格式
  diff-msg-format: 【${_fieldName}】:【${_oldValue}】->【${_newValue}】
  # Diff 分隔符
  diff-msg-separator: ;

UserController:

@OperationLog(bizId = "#id", bizType = "'user:update'", msg = "'更新用户:' + #_DIFF(#oldObj, #obj)")
public ApiResponse update(@Min(value = 1, message = "参数错误") @PathVariable Long id, @Validated @RequestBody UserUpdateQO obj) {
  User user = userService.getById(id);
  if (user == null) {
    throw new ServiceException("用户不存在");
  }
  LogRecordContext.putVariable("oldObj", user);
  // ……
}

User:

@LogRecordDiffObject(alias = "用户")
public class User {
  @LogRecordDiffField(alias = "用户名")
  private String username;
  // ……
}

UserUpdateQO:

@LogRecordDiffObject
public class UserUpdateQO {
  @LogRecordDiffField(alias = "用户名")
  private String username;
  // ……
}

结果:更新用户:?电话?:?18888888887?->?18888888888?;?昵称?:?234?->?23?;?角色?:?2,1671440885033914373?->?2?

我所有的文件编码都是UTF-8
我试过给diff-msg-format的内容前后加单引号也没用。

关于自定义 success、exception 和全局异常处理的顺序问题

目前createLog方法中LogDTO.exception获取的仅为e.getMessage(),而我想获取的是e.printStackTrace()的内容。

大多项目都会有一个全局异常处理。
GlobalExceptionHandler:

/**
 * 全局异常处理
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler  {

  @ExceptionHandler(value = Exception.class)
  @ResponseBody
  public ApiResponse<String> handlerException(Exception e) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    e.printStackTrace(pw);
    LogRecordContext.putVariable("exception", sw.toString());

    // ……
  }
  1. 上文代码中的LogRecordContext.putVariable("exception", sw.toString());仅为我的猜想,能否通过这种方式去设置LogDTO.exception的值(put 中可以特殊点叫 $exception 或者加新方法),不然每个@OperationLog都要加属性(当然现在还没有可以配置exception的属性)。

  2. 假设可以自定义LogDTO.exception的值的话,GlobalExceptionHandler.handlerException的执行顺序也总是在createLog方法后,@Order注解无效。或者 LogDTO 可以获取到 Exception 对象也行。

批量操作是如何支持的?

针对业务中的批量操作,比如批量删除、批量新增操作,通过注解的方式记录日志时貌似无法满足业务需求,请问您这种情况有考虑吗?

使用diff场景问题

实际业务中,当修改的对象是复杂对象,里面又包含List对像集合,这种情况时支持吗?我自测没通过

CustomFunctionObjectDiff 组件取newData alias使用的是oldObject

CustomFunctionObjectDiff 类里第54行
// 对象类名
51 String oldClassName = oldObject.getClass().getName();
52 String newClassName = newObject.getClass().getName();
53 LogRecordDiff oldClassLogRecordDiff = oldObject.getClass().getDeclaredAnnotation(LogRecordDiff.class);
54 LogRecordDiff newClassLogRecordDiff = oldObject.getClass().getDeclaredAnnotation(LogRecordDiff.class);

使用默认_DIFF方法对日期进行比对的时候会出现异常

报错内容如下,原因是因为Date转JSONObject的时候出错,建议进行优化一下默认的_DIFF方法

2023-05-19 11:49:21.555 [http-nio-8080-exec-5] ERROR c.m.logRecord.function.CustomFunctionObjectDiff:124 - objectDiff error
cn.monitor4all.logRecord.exception.LogRecordException: fieldValueEquals error
	at cn.monitor4all.logRecord.function.CustomFunctionObjectDiff.fieldValueEquals(CustomFunctionObjectDiff.java:228)
	at cn.monitor4all.logRecord.function.CustomFunctionObjectDiff.objectDiff(CustomFunctionObjectDiff.java:115)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.expression.spel.ast.FunctionReference.executeFunctionJLRMethod(FunctionReference.java:121)
	at org.springframework.expression.spel.ast.FunctionReference.getValueInternal(FunctionReference.java:80)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getTypedValue(SpelNodeImpl.java:117)
	at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:308)
	at cn.monitor4all.logRecord.aop.SystemLogAspect.resolveExpress(SystemLogAspect.java:259)
	at cn.monitor4all.logRecord.aop.SystemLogAspect.doAround(SystemLogAspect.java:118)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at org.jeecg.common.aspect.PrintLogAspect.doAround(PrintLogAspect.java:155)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at org.jeecg.common.aspect.DictAspect.doAround(DictAspect.java:56)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at org.jeecg.common.aspect.AutoLogAspect.around(AutoLogAspect.java:57)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
	at org.jeecg.modules.sys.controller.SysConfigController$$EnhancerBySpringCGLIB$$d91be327.edit(<generated>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:920)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.jeecg.common.config.mqtoken.TransmitUserTokenFilter.doFilter(TransmitUserTokenFilter.java:26)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:450)
	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:387)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter.doFilterInternal(HttpTraceFilter.java:88)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassCastException: java.util.Date cannot be cast to com.alibaba.fastjson.JSONObject
	at cn.monitor4all.logRecord.function.CustomFunctionObjectDiff.fieldValueEquals(CustomFunctionObjectDiff.java:214)
	... 150 common frames omitted

MQ支持Kafka吗

我看介绍支持RabbitMQ、RocketMQ、SpringCloud Stream。如果想接入其他MQ,有对应的扩展点嘛?

_DIFF对于对象/对象数组的msg输出

diff函数内,我看作者大大在msg输出时是直接调用 old/newValue.toString()。请问能否支持通过注解别名优化一下输出。

例子:【列表A】从【[TestDiffJob(jobId=22, jobName=222, dutyList=[TestDiffDuty(dutyId=222, dutyName=222)])]】 改为
【列表A】从【[实体A(编号=22, 名字=222, 列表A=[实体B(编号=222, 名=222)])]】

原输出虽然开发人员能够明白,但对于业务人员的查看并不友好

以上效果虽然可以直接通过覆写toString实现,但不算是一个太好的方案,容易干扰用到toString的相关代码。

关于 @LogRecordDiffObject 和 @LogRecordDiffField 的一些建议

  1. diff 时,oldObj 和 obj 必须都要加 @LogRecordDiffObject 注解,否则 diff 内容为空。我觉得这个注解可以不加,主要只用 @LogRecordDiffField 的 alias。或者没有都加 @LogRecordDiffObject 的时候报错提醒下。
  2. diff 时,oldObj 和 obj 的字段都得写 @LogRecordDiffField(alias = "用户名"),结果才会有中文,否则就是字段名,我觉得其中一个加就行了,根据字段名来判断。

关于DIFF

作者你好,使用DIFF的时候需要从业务代码中获取原来的值,这个还有调整的空间吗

新增常用公共函数和可序列化对象注解的建议

我再提些建议,你别烦我🥺

我在使用过程中自定义了一个toLogString函数,作用是传进去对象或集合,输出有@LogRecordDiffField的属性,而且还和diff-ignore-new-object-null-valuediff-msg-separator结合起来使用了。我觉得将参数属性名转为中文是很常见的需求,可以新增类似的公共函数。

我觉得@LogRecordDiffField注解可以改成@LogRecordField,然后同时适用于 Diff 和其他功能,就像 MyBatis Plus 的@TableField或 Hibernate 的@Column一样。

我很久以前写过一个基于 Hibernate 的注解日志记录,会自动将带有@Column的属性名转为这个注解的 comment 输出,或者将它的属性名、@column 注解的 comment 属性的值、属性值传到一个集合里后续再使用。

我觉得除了新增类似的公共函数之外,也可以给@OperationLog加一个属性,为 true 就直接把属性名转成@LogRecordField的 alias 的值了。

CustomLogFn

/**
 * 日志自定义函数
 */
@Slf4j
@Component
@LogRecordFunc("custom")
public class CustomLogFn {

  private static boolean ignoreNullValues;
  private static String diffMsgSeparator;

  @Value("${log-record.diff-ignore-new-object-null-value}")
  public void setIgnoreNullValues(boolean ignoreNullValues) {
    CustomLogFn.ignoreNullValues = ignoreNullValues;
  }

  @Value("${log-record.diff-msg-separator}")
  public void setDiffMsgSeparator(String diffMsgSeparator) {
    CustomLogFn.diffMsgSeparator = diffMsgSeparator;
  }

  @LogRecordFunc("toLogString")
  public static String toLogString(Object object) {
    List<String> fields = new ArrayList<>();

    // 操作多个对象
    if (object instanceof Iterable<?>) {
      for (Object o : (Iterable<?>) object) {
        fields.add(toLogString(o));
      }
      return StringUtils.join(fields, " | ");
    } else {
      // 遍历对象的所有字段
      for (Field field : object.getClass().getDeclaredFields()) {
        // 跳过没有 @LogRecordDiffField 的字段
        if (!field.isAnnotationPresent(LogRecordDiffField.class)) {
          continue;
        }
        field.setAccessible(true);
        LogRecordDiffField annotation = field.getAnnotation(LogRecordDiffField.class);
        // 跳过忽略的字段
        if (annotation.ignored()) {
          continue;
        }
        try {
          Object value = field.get(object);
          // 是否忽略 null 值
          if (ignoreNullValues && value == null) {
            continue;
          }
          fields.add(annotation.alias() + ":" + (value != null ? value.toString() : "null"));
        } catch (IllegalAccessException e) {
          log.error("获取字段值失败", e);
        }
      }
      return StringUtils.join(fields, diffMsgSeparator);
    }
  }
}
@OperationLog(bizId = "#bizId", bizType = "'user:save'", msg = "'【保存用户】' + #custom_toLogString(#obj)")
// 结果:【保存用户】用户名:test2;昵称:test2;电话:18888888888;角色:3

@OperationLog(bizId = "#ids", bizType = "'user:remove'", msg = "'【删除用户】' + #custom_toLogString(#removedList)")
// 结果:【删除用户】用户名:test1;电话:18888888888;昵称:test1;角色:2 | 用户名:test2;电话:18888888888;昵称:test2;角色:3

完善单元测试

需要完善单元测试,用于PR做回归测试,保证代码质量

请教一个小问题😂

获取请求ip地址或者request对象,您是怎么传递的
我看图片当中也提到了访问ip

有一点小小的建议

作者你好,注解上的SpEL表达式执行顺序总是晚于被注解的整体方法执行的,有一种逻辑就会产生问题。
比如,有个方法是删除数据,参数只有一个ID,这种情况下需要通过id查到这条数据的信息,然后一起记录到日志。
但是现在的结果是,数据先被删除,然后SpEL函数去查数据时数据已经是null了。
看到您有个LogRecordContext的上下文,可以在方法体先查出来,但是这样子的话入侵太严重。
现在入参ID不变,不动入参,不能影响到前端。这种情况怎么处理比较好,后续会优化这种情况吗?

@OperationLog的success参数建议

我有个问题
@OperationLog 这个注解有个success() 的参数 可以判断函数是否执行成功,文档如下图

  • image
    因为我这个项目是已完成的项目,返回的参数都是统一的,如下图
  • image

这样操作的话就侵入了业务逻辑。而且要改很多的接口,我就是想能不能出个接口,让我自己来根据方法的返回参数判断接口是否执行成功,如果方法执行抛出了错误那么自动认为方法执行失败了

编码文件格式问题

目前看代码是默认按照ISO-8859-1编码格式读取配置文件,但是这个似乎有问题,这种编码不支持配置中文?而且springboot之类的框架目前大多数也是默认按照utf-8来更改读取配置文件,所以目前自定义更改中文提示语是有问题的
image

说明文档里diffDTOList数组为什么同样的数据存储两条,没理解

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

===========================
"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"
}]

关于 Diff 根据内容忽略字段的建议

  1. 只修改部分字段时,msg 显示了全部字段。
    编辑接口使用 Diff,msg最终为:“【username】从【xxx】变成了【 】 【xxx】……”。实际上因为 MyBatis 配置的更新策略,是不会更新参数中为 null 的字段(此处即 username)的,但是操作日志中却显示了。
    建议新增一个全局配置,用于 Diff 时忽略 newObj 中为 null 或为空或指定内容的字段。还可以给@LogRecordDiffField新增一个属性用于单独配置。
  2. 感觉@LogRecordDiffIgnoreField这个注解,可以变成@LogRecordDiffField的一个属性。

关于第 1 点,我现在自己的做法是在implements IOperationLogGetService的类中判断diffDTOList是否不为空,如果不为空的话,获取其中 newValue 为 null 的,再去处理 msg。

项目集成log-record通过k8s发布启动后报javassist.NotFoundException: cn.monitor4all.logRecord.function.CustomFunctionObjectDiff异常

项目集成log-record通过k8s发布启动后报javassist.NotFoundException: cn.monitor4all.logRecord.function.CustomFunctionObjectDiff异常
但是本地通过idea启动是没问题的,以下是详细的错误日志,希望作者帮忙看一下

DEBUG 2022-12-20 10:50:39:929 main AgentPackagePath : The beacon class location is jar:file:/skywalking-agent/skywalking-agent.jar!/org/apache/skywalking/apm/agent/core/boot/AgentPackagePath.class.
INFO 2022-12-20 10:50:39:932 main SnifferConfigInitializer : Config file found in /skywalking-agent/config/agent.config.

. ____ _ __ _ _
/\ / ' __ _ () __ __ _ \ \ \
( ( )_
_ | '_ | '| | ' / ` | \ \ \
\/ )| |)| | | | | || (| | ) ) ) )
' |
| .__|| ||| |_, | / / / /
=========|
|==============|/=////
:: Spring Boot :: (v2.5.1)

10:51:23.614 [main] INFO [TID:N/A] com.toplion.download.DownloadCenterApplication - The following profiles are active: pre
10:53:03.515 [main] ERROR [TID:N/A] cn.monitor4all.logRecord.function.CustomFunctionRegistrar - cn.monitor4all.logRecord.function.CustomFunctionObjectDiff
javassist.NotFoundException: cn.monitor4all.logRecord.function.CustomFunctionObjectDiff
at javassist.ClassPool.get(ClassPool.java:430)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.constructCtClass(CustomFunctionRegistrar.java:118)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.proxy2static(CustomFunctionRegistrar.java:104)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.lambda$setApplicationContext$4(CustomFunctionRegistrar.java:72)
at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.setApplicationContext(CustomFunctionRegistrar.java:43)
at org.springframework.context.support.ApplicationContextAwareProcessor.invokeAwareInterfaces(ApplicationContextAwareProcessor.java:128)
at org.springframework.context.support.ApplicationContextAwareProcessor.postProcessBeforeInitialization(ApplicationContextAwareProcessor.java:102)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:338)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332)
at com.toplion.download.DownloadCenterApplication.main(DownloadCenterApplication.java:16)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
10:53:04.323 [main] ERROR [TID:N/A] org.springframework.boot.SpringApplication - Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'registrar' defined in class path resource [cn/monitor4all/logRecord/configuration/LogRecordConfiguration.class]: Initialization of bean failed; nested exception is java.lang.RuntimeException: javassist.NotFoundException: cn.monitor4all.logRecord.function.CustomFunctionObjectDiff
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:610)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:338)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332)
at com.toplion.download.DownloadCenterApplication.main(DownloadCenterApplication.java:16)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
Caused by: java.lang.RuntimeException: javassist.NotFoundException: cn.monitor4all.logRecord.function.CustomFunctionObjectDiff
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.lambda$setApplicationContext$4(CustomFunctionRegistrar.java:85)
at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.setApplicationContext(CustomFunctionRegistrar.java:43)
at org.springframework.context.support.ApplicationContextAwareProcessor.invokeAwareInterfaces(ApplicationContextAwareProcessor.java:128)
at org.springframework.context.support.ApplicationContextAwareProcessor.postProcessBeforeInitialization(ApplicationContextAwareProcessor.java:102)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602)
... 23 common frames omitted
Caused by: javassist.NotFoundException: cn.monitor4all.logRecord.function.CustomFunctionObjectDiff
at javassist.ClassPool.get(ClassPool.java:430)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.constructCtClass(CustomFunctionRegistrar.java:118)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.proxy2static(CustomFunctionRegistrar.java:104)
at cn.monitor4all.logRecord.function.CustomFunctionRegistrar.lambda$setApplicationContext$4(CustomFunctionRegistrar.java:72)
... 30 common frames omitted

Null return value from advice does not match primitive return type for: public abstract boolean cn.monitor4all.logRecord.service.IOperationLogGetService.createLog(cn.monitor4all.logRecord.bean.LogDTO)

org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract boolean cn.monitor4all.logRecord.service.IOperationLogGetService.createLog(cn.monitor4all.logRecord.bean.LogDTO) throws java.lang.Exception
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:229)
	at com.sun.proxy.$Proxy259.createLog(Unknown Source)
	at cn.monitor4all.logRecord.aop.SystemLogAspect.createLog(SystemLogAspect.java:323)
	at cn.monitor4all.logRecord.aop.SystemLogAspect.lambda$doAround$2(SystemLogAspect.java:167)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at cn.monitor4all.logRecord.aop.SystemLogAspect.doAround(SystemLogAspect.java:175)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at com.hiifans.brick.spring.framework.logging.RequestLogInterceptor.invoke(RequestLogInterceptor.java:44)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
	at com.hiifans.crm.admin.controller.ReportController$$EnhancerBySpringCGLIB$$773e8852.reportViewLog(<generated>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:696)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:779)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:750)

使用方式:

@PostMapping("/view-log")
    @OperationLog(bizId = "", bizType = "'VIEW'", msg = "'菜单名称:' + #reportViewLogDTO.menuName + ',进入时间:' + #enterTime")
    @ApiOperation("浏览日志上报")
    public DataResponse<?> reportViewLog(@RequestBody @Valid ReportViewLogDTO reportViewLogDTO) {
        LogRecordContext.putVariable("enterTime", LocalDateTimeUtil.formatNormal(LocalDateTime.now()));
        return DataResponse.ok();
    }
@Slf4j
@Service
public class OperationLogGetServiceImpl implements IOperationLogGetService {

    @Async
    @Override
    public boolean createLog(LogDTO logDTO) throws Exception {
        try {
            log.error("{}", JSONUtil.toJsonStr(logDTO));
        } catch (Exception e) {

        }
        return true;
    }
}

找不到依赖

配置了github生效后,依赖仍爆红。Could not find artifact cn.monitor4all:log-record-starter:pom:1.0.0 in central (https://repo1.maven.org/maven2)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.