本文档讲述内容与代码示例可参见 Shoulder-Demo1

能力激活方式:

Maven
<dependency>
    <groupId>cn.itlym</groupId>
    <artifactId>shoulder-starter-operation-log</artifactId>
</dependency>
Gradle
compile 'cn.itlym:shoulder-starter-operation-log'

AOP 操作日志 @OperationLog

基本使用

Shoulder 为方便地记录操作日志,提供了 @OperationLog @OperationLogParam 注解,使用示例如下

注解式操作日志
@RestController
@RequestMapping("user")
public class MyUserController {

    @OperationLog(operation = "testOpLogAnnotation")
    @GetMapping("testOpLogAnnotation")
    public String register(
            // 参数未加注解,不会记录到操作日志
            @Cookie String cookie,

            // 参数名默认为变量名 param0
            @OperationLogParam String param0,

            // 设置支持多语言,自定义参数名
            @OperationLogParam(supportI18n = true, name = "myParam") String param1) {

        return "ok";
    }

}

该写法日志记录示例:

注解式操作日志简单样例
2024-06-18 18:22:27.953 INFO  [39536] --- [ulder-opLogger1] SHOULDER-OPERATION : userId:"system.demo1.0",terminalAddress:"127.0.0.1",terminalId:"349D273CCE05DBAC0565374297BA1654",terminalInfo:"Mozilla/5.0 (Macintosh; Intel Mac OS X …",operation:"test",tenantCode:"DEFAULT",appId:"demo1",terminalType:"1",result:"0",operationTime:"2024-06-18T18:22:23.625 +0800",endTime:"2024-06-18T18:22:23.652 +0800",userAgent:"..."

若使用 @OperationLogParam,日志将额外记录以下内容:

注解式操作日志-记录参数-额外内容
params:"[{"name"="testOpLogAnnotation.param0", "value"=""},{"name"="testOpLogAnnotation.myParam", "value"=""}]"
若带有 @OperationLog 的方法中发生异常,Shoulder 会自动修改日志记录状态为 success=false
@OperationLog 默认只记录方法中携带 @OperationLogParam 声明的参数,您可以通过配置 logAllParams = true 来记录所有参数, 示例: @OperationLog(operation = "xxx", logAllParams = true)
若您想修改日志打印格式,请参考进阶部分 [自定义:操作日志格式]
操作日志 AOP 是基于 Spring AOP 实现的,所以遵循 Spring AOP 使用方式,比如注解必须加在 @Bean 类型对象的非 staticpublic 方法上,且对于 this.xxxMethod 是无效的(this. 写法未过 Spring 的代理)。简单起见,建议初学者在 Controller 对外暴露的接口方法上使用 @OperationLog 注解。

填充更多字段

可以在有日志上下文的方法中使用 OpLogContextHolder.getLog() 获取当前的操作日志对象,以实现更多操作。

注解式操作日志-填充更多字段
    @OperationLog(operation = "testLogObj")
    @GetMapping("testLogObj")
    public String testLogObj() {
        // 从日志上下文中拿出日志 DTO,getObjectType 获取类型
        OpLogContextHolder.getLog().setXXX();
        return OpLogContextHolder.getLog().getObjectType();
    }
OpLogContextHolder.getLog() 方法是取 ThreadLocal 中的日志对象,若线程上下文中 logDTO 为 null(如误清理、@OperationLog 嵌套使用),则将会出现 NullPointException 异常!

完整的操作日志

可参考 日志记录规范-操作日志 中定义的字段:

必填的字段已经在第一列加粗。
Table 1. 操作日志格式
字段名称 长度 必填 说明

操作者相关信息

userId

128

用户标识(如用户名、userId);为系统内部操作时(如定时任务、接收消息通知),填写执行操作的服务实例名,格式为 system.应用标识.实例标识

userName

128

用户昵称

personId

64

用户对应人员标识

userOrgId

128

用户所属 用户组群组部门 的编号。

userOrgName

255

用户组名称。

terminalType

1

操作者所使用终端的类别 0 表示 系统内部1 表示 浏览器2 表示 APP3 表示 PC客户端

ip

255

操作者所在机器 IP;系统内部操作时,填写服务所在机器 IP

terminalId

128

操作者所使用终端的唯一标识:如 MAC 地址。

terminalInfo

256

操作者所使用终端详情信息,如浏览器中的 UserAgent

操作动作相关信息

operation

255

操作动作标识。

operationTime

128

操作时间,格式为 年-月-日T时:分:秒.毫秒时区,如 2017-09-20T13:42:38.349+08:00

params

-

操作对应业务方法/接口的入参,一般关键/特殊业务才会使用。JSON 形式,见下方 操作日志参数格式

result

1

业务操作结果:0 表示 成功正确1 表示 失败不正确2 表示 部分成功

errorCode

32

当操作结果为失败时,记录操作失败对应的错误码。

detailKey

128

操作详情( 支持 多语言时填写),多语言 key 格式:op.detail.<操作内容标识>,支持占位符,采用 %1, %2, %n 形式,n表示第n个参数;不支持多语言时留空。

detailItems

-

操作详情占位参数( 支持 多语言时填写),用于填充 detailKey 的占位符。

detail

4096

操作详情( 不支持 多语言时填写)详细的描述用户的操作内容,也可填写被操作对象的 JSON 字符串。

被操作对象相关信息

objectType

128

操作对象的类型标识。

objectId

128

操作对象的标识,若存在多个值时, 以 , 分隔,如 [1,2,3,4,5]

objectName

255

操作对象的名称,若存在多个值时,以 , 分隔,如 [角色1、角色2]

其他信息

businessId

128

存放与本次操作所关联其他业务操作的业务号。用于多个请求完共同完成一个业务功能时。如:上传csv进行数据的批量导入场景:上传导入文件、校验导入数据、点击确认导入、导入成功业务相关可以填同一个标识符

appId

128

服务唯一标识。

traceId

128

调用链标识。

自定义扩展字段

-

扩展字段,用于个别服务需要,一般留空。

代码中可参考 OperationLogDTO

完整参数记录样例

示例:

自定义操作日志格式-校验

国际化

您可使用特殊格式的 i18nKey 代替实际文本,并在展示时还原为需要的语言。操作日志中以下字段支持国际化。

Table 2. 多语言字段对照表
字段名称 说明 建议方案

operation

操作动作标识

支持多语言时添加特定前缀,如 op.op.<操作动作标识>

objectType

被操作对象类型

支持多语言时添加特定前缀,如 op.objType.<操作对象类型标识>

detail

操作详情

使用 detailKey 作为多语言key,格式:op.detail.<操作内容标识>,detailItem 填充占位符

terminalType

终端类型

采用枚举/数据字典,展示层翻译

result

操作结果

采用枚举/数据字典,展示层翻译

appId

应用标识

结合基本规范,采用数据字典,展示层翻译

errorCode

错误码

结合错误码规范,给出错误原因和排查建议

operationTime

操作时间

结合国际化规范,时间格式可改变

操作日志-国际化
    public static final String OPERATION_CREATE_USER = "op.operation.user.create.i18n";

    @OperationLog(operation = OPERATION_CREATE_USER)
    public void createUser() {
        //... 操作日志中 operation 被记录为 op.operation.user.create.i18n
    }

参考 Shoulder-国际化说明,新增对应语言资源文件即可。

以中文为例,只需要在 reousrce/language/zh_CN/userModule.properties 中加入对应配置即可在查询时做对应的翻译展示,其他语言同理。

国际化资源文件
op.operation.user.create.i18n=创建用户

基础配置

若想关闭操作日志相关所有功能,可通过设置 shoulder.log.operation.enable=false 实现。

若想让值为 null 的参数值输出自定义格式,如 "NUL",可设置 shoulder.log.operation.nullParamOutput=NUL


推荐阅读
快速上路指引
 可前往 目录页 查看 Shoulder 其他模块功能。
面向进阶使用者
 继续阅读本文档 以查看 Shoulder-操作日志 进阶能力。

存储与可视化

以下部分适合有一定开发经验的进阶开发者,会涉及一些 JavaSpring Boot 的进阶知识点。

切换存储

存储至日志文件【默认】

这是 Shoulder 的默认日志输出方式,实际输出目标会以日志系统为准,无需设置。

存储至数据库

请确保数据库相关依赖已引入并成功连接数据库,并在您数据库中传教完毕表 log_operation

-表结构
CREATE TABLE IF NOT EXISTS log_operation
(
    id               bigint auto_increment comment '主键' primary key,
    app_id           varchar(32)                           not null comment '应用id',
    version          varchar(64)                           null comment '应用版本',
    instance_id      varchar(64)                           null comment '操作服务器节点标识(支持集群时用于定位具体哪台服务器执行)',
    user_id          varchar(64)                           not null comment '用户标识',
    user_name        varchar(64)                           null comment '用户名',
    user_real_name   varchar(128)                          null comment '用户真实姓名',
    user_org_id      varchar(64)                           null comment '用户组标识',
    user_org_name    varchar(64)                           null comment '用户组名',
    terminal_type    int                                   not null comment '终端类型。0:服务内部定时任务等触发;1:浏览器;2:客户端;3:移动App;4:小程序。推荐前端支持多语言',
    terminal_address varchar(64)                           null comment '操作者所在终端地址,如 IPv4(15) IPv6(46)',
    terminal_id      varchar(64)                           null comment '操作者所在终端标识,如PC的 MAC;手机的 IMSI、IMEI、ESN、MEID;甚至持久化的 UUID',
    terminal_info    varchar(255)                          null comment '操作者所在终端信息,如操作系统类型、浏览器、版本号等',
    object_type      varchar(128)                          null comment '操作对象类型;建议支持多语言',
    object_id        varchar(128)                          null comment '操作对象id',
    object_name      varchar(128)                          null comment '操作对象名称',
    operation_param  text                                  null comment '触发该操作的参数, json 格式',
    operation        varchar(128)                          not null comment '操作动作;建议支持多语言',
    detail           text                                  null comment '操作详情。详细的描述用户的操作内容、json对象,仅在深入排差时查看',
    detail_i18n_key  varchar(128)                          null comment '操作详情对应的多语言key',
    detail_i18n_item varchar(255)                          null comment '填充 detail_i18n_key 对应的多语言翻译。数组类型',
    result           int                                   not null comment '操作结果,0成功;1失败;2部分成功;建议支持多语言',
    error_code       varchar(32)                           null comment '错误码',
    operation_time   timestamp                             not null comment '操作触发时间,注意采集完成后替换为日志服务所在服务器时间',
    end_time         timestamp                             null comment '操作结束时间',
    duration         bigint                                null comment '操作持续时间,冗余字段,单位 ms',
    trace_id         varchar(64)                           null comment '调用链id',
    relation_id      varchar(64)                           null comment '关联的调用链id/业务id',
    tenant_code      varchar(20) default ''                null comment '租户编码',
    create_time      timestamp   default CURRENT_TIMESTAMP null comment '数据入库时间',
    update_time      timestamp   default CURRENT_TIMESTAMP null comment '数据更新时间,日志表在非必要的订正前提下,一般不更新',
    extended_field0  varchar(1024)                         null,
    extended_field1  varchar(1024)                         null,
    extended_field2  varchar(1024)                         null,
    extended_field3  varchar(1024)                         null,
    extended_field4  varchar(1024)                         null
)
    comment '业务日志';

CREATE INDEX IF NOT EXISTS idx_operation_time
    on log_operation (operation_time);

CREATE INDEX IF NOT EXISTS idx_terminal_address
    on log_operation (terminal_address);

CREATE INDEX IF NOT EXISTS idx_trace_id
    on log_operation (trace_id);

CREATE INDEX IF NOT EXISTS idx_user_id
    on log_operation (user_id);

只需一项配置即可改为写到

Properties
shoulder.log.operation.logger.type=jdbc
YAML
shoulder:
  log:
    operation:
      logger:
        type: jdbc

完成设置后,操作日志将由 JdbcOperationLogger 保存至您的数据库内。

API 查询接口

数据库保存方式下,若您的依赖还包含 shoulder-webShoulder 将自动开启日志查询接口(POST /api/v1/oplogs/page),让搭建操作日志可视化页面更简单。

shoulder-web 依赖引入方式:在 pom.xml 中引入 shoulder-web-starter

Maven
<dependency>
    <groupId>cn.itlym</groupId>
    <artifactId>shoulder-starter-web</artifactId>
    <version>1.1.0<version>
</dependency>
Gradle
compile 'cn.itlym:shoulder-starter-web:1.1.0'

若您想调整接口路径或关闭该功能,修改相关配置即可

Properties
# 是否开启操作日志 api 查询接口,true 开启 false 关闭
shoulder.web.ext.oplog.enable=true
shoulder.web.ext.oplog.path=/api/v1/oplogs
YAML
shoulder:
  web:
    ext:
      oplog:
        # 是否开启操作日志 api 查询接口,true 开启 false 关闭
        enable: true
        path: /api/v1/oplogs
空存储

shoulder.log.operation.logger.type=none

适合部分场景暂时不需要输出日志,但希望操作日志能随时启用的场景。

自定义 Logger

Shoulder 允许您自定义保存操作日志的存储方式,实现 OperationLogger 接口,并将实现类作为 @Bean 注入至 Spring 上下文中即可。

自定义操作日志存储
@Component
public class MyOperationLogger implements OperationLogger {
    //...自行处理日志,如上报到日志中心、发送至 MQ..
}

可视化页面

对于使用数据库存储操作日志的场景,Shoulder 提供了默认的查询接口以及响应式的 WEB 可视化查询页面,如图

shoulder oplog ui
Properties
shoulder.log.operation.logger.type=jdbc
shoulder.web.ext.oplog.enable=true # 激活分页查询接口 + UI 页面,默认false 不开启

shoulder.web.ext.oplog.path=/api/v1/oplogs # enable=true 之后的默认值,用于控制操作日志分页查询接口 api 路径
shoulder.web.ext.oplog.pageUrl=/ui/oplogs/page.html # enable=true 之后的默认值,用于控制操作日志 ui 访问路径
YAML
shoulder:
  log:
    operation:
      logger:
        type: jdbc

  web:
    ext:
      oplog:
        enable: true
很是实用呢,快去试试吧~

进阶使用

简化代码

Shoulder 提供了一些接口,合理使用可大幅简化复杂操作下的业务代码,代码对比:

以货物管理场景为例,操作员通过系统从仓库中出库一个货物,希望系统完整记录操作员信息、货物信息、操作时间、操作结果、操作员所在设备信息、请求 traceId 内容。

以下两种方式都可以正确记录这些信息:

简化前
@OperationLog(operation = "出货")
public OutboundResult outbound(User operator, Goods goods, HttpServletRequest request) {

    OutboundResult result = goodsService.outbound(goods);

    OpLogContextHolder.getLog()
            // 填写用户、部门信息
            .setUserId(operator.getUserId())
            .setUserName(operator.getUserName())
            .setUserOrgId(operator.getUserOrgName())
            .setUserOrgName(operator.getUserOrgName())

            // 操作对象
            .setObjectId(goods.getId())
            .setObjectName(goods.getName())
            .setObjectType(goods.getType());

    return result;
}
简化后
@OperationLog(operation = "出货")
public void outbound(User operator, Goods goods, HttpServletRequest request) {

    OutboundResult result = goodsService.outbound(goods);

    OpLogContextHolder.setOperator(operator);
    OpLogContextHolder.setOperableObject(goods);

    // 操作人设备信息、处理应用、操作结果等信息将由框架自动填充,无需填写

    return result;
}

// 省略 UserI implements Operator...
// 省略 Goods implements Operable...
合理使用这些接口可大幅减少您的代码~ OperableOperateResultOperationDetailAbleOperatorOperateRecord

多操作对象 & 批量记录

场景举例:

场景1:某一接口,允许创建一个用户,在接口调用完毕后,需要记录一条操作日志,保存此次操作信息,同时记录该用户的id信息。

场景2:某一接口,允许批量添加 10 个用户,在接口调用完毕后,需要记录一条操作日志,保存此次操作信息,同时记录这 10 个用户的id信息。

场景3:某一接口,允许批量添加/导入 100 个用户,在接口调用完毕后,需要记录 10 条操作日志,保存此次操作信息,同时以每条日志记录 10 个用户信息,分别记录这 100 个用户的id信息。(避免单条日志过长过大而无法采集或检索困难)

Shoulder @OperationLog 可以做到一个方法添加注解,共记录 M 个操作对象,N 条操作日志,每条日志记录 M / N 个操作对象。

跨线程使用

借助 Shoulder-Core 中的 ThreadEnhancer,可实现操作日志跨线程使用!

开启跨线程后若只需要子线程记录日志,主线程不需要记录,可以调用 OpLogContextHolder.disableAutoLog() 关闭本次日志记录。
操作日志-跨线程使用
@Service
public class DemoService {
    @Async // Spring 的异步注解
    public void asyncTest() {
        Thread.sleep(2000);
    }
}

@Service
public class OperationDemoService {
    @Autowired
    private DemoService demoService;

    @Autowired
    @Qualifier("shoulderThreadPool") // 这里显式使用了 shoulder 线程池,若希望使用自定义线程池,要确保自己的线程池已作为 Bean 被 Spring 管理
    Executor shoulderThreadPool;

    @OperationLog(operation = "asyncTest")
    public String async() {
        demoService.asyncTest();
        // 将在 demoService.asyncTest 方法结束后会打印操作日志
        return "return~";
    }

    @OperationLog(operation = "threadpoolTest")
    public String threadpoolTest() {
        shoulderThreadPool.asyncTest();
        // 将在 demoService.asyncTest 方法结束后会打印操作日志
        return "return~";
    }

shoulderThreadPool.execute(xxx)
不同线程之间操作也是线程安全的,操作日志线程变量拷贝使用了深克隆,父线程修改操作日志内容,不会影响子线程的日志内容,反之亦然。

异步记录 & 缓冲记录

为提高记录性能,保障业务服务的吞吐,Shoulder 为您提供了“异步记录”与“缓冲记录”操作日志的能力,可在不影响主线程的情况下处理记录流程,若开启了“缓冲记录”,还会合并多条日志统一记录以减少 IO 操作。

开启方式:

Properties
# 是否以异步线程记录操作日志,默认开
shoulder.log.operation.logger.async=true
# 异步记录的线程数,默认1
shoulder.log.operation.logger.threadNum=1
# 异步记录的线程名
shoulder.log.operation.logger.threadName=shoulder-opLogger

# 是否开启缓冲记录,默认 false 不开启
shoulder.log.operation.logger.buffered=true
# 缓冲记录-缓冲区刷新间隔时间(满足后立即记录)
shoulder.log.operation.logger.flushInterval=10s
# 缓冲记录-当缓冲区日志积压数量超过多少会刷新缓冲区(满足后立即记录)
shoulder.log.operation.logger.flushThreshold=10
# 缓冲记录-日志记录器单次最大记录数,用于保护记录器,避免单次批量提交的压力过大
shoulder.log.operation.logger.perFlushMax=20
YAML
shoulder:
  log:
    operation:
      logger:
        # 是否以异步线程记录操作日志
        async: true
        # 异步记录的线程数
        threadNum: 1
        # 异步记录的线程名
        threadName: shoulder-opLogger

        # 是否开启缓冲记录,默认 false 不开启
        buffered: true
        # 缓冲记录-缓冲区刷新间隔时间(满足后立即记录)
        flushInterval: 10s
        # 缓冲记录-当缓冲区日志积压数量超过多少会刷新缓冲区(满足后立即记录)
        flushThreshold: 10
        # 缓冲记录-日志记录器单次最大记录数,用于保护记录器,避免单次批量提交的压力过大
        perFlushMax: 20
关于操作日志记录器的配置类可参考 OperationLogProperties.LoggerProperties

二次加工

Shoulder 允许您在记录前拦截完善日志内容,以实现筛选日志或加工日志内容等操作,实现 OperationLoggerInterceptor 接口,并将实现类作为 @Bean 注入至 Spring 上下文中即可。

示例代码
@Component
public class DemoOperationLogInterceptor implements OperationLoggerInterceptor {

    @Override
    public boolean beforeLog(OperationLogDTO opLog) {
        // 与 spring web 拦截器一样,return true 继续打印日志,false 则不打印
        return "create".equals(opLog.getOperation());
    }

    @Override
    public void afterLog(OperationLogDTO opLog) {
        System.out.println("记录操作日志后回调钩子");
    }
}
更多拦截逻辑的示例
@Component
public class DemoOperationLogInterceptor implements OperationLoggerInterceptor {

    @Override
    public boolean beforeLog(OperationLogDTO opLog) {
        if (StringUtils.isEmpty(opLog.getObjectName())) {
            // 删除操作为例:删除用户接口的参数可能只有 userId 而没有 userName
            //但希望在展示操作日志时,显示被删除的用户昵称、真实姓名,因此需要再查一次数据库补充

            //fillMoreInfoFromDB(opLog);
        }
        return true;
    }

    @Override
    public void afterLog(OperationLogDTO opLog) {
        // 这里可以做的事情举例:
        //
        // 1. 清理一些由于 beforeValidate 引入的变量或者垃圾

        // 2. 统计各个业务操作的频次,看看哪些业务比较热门 / 受欢迎 / 重要

        // 3. 审计调用参数

        // 4. ...

    }

    // shoulder 支持操作日志异步处理,无需担心会阻塞主处理请求
    private void fillMoreInfoFromDB(OperationLogDTO opLog) {
        // 根据 opLog.getObjectType 动态从数据库里动态查对应名称并填充
        // OperableObject dbInfo = jdbcTemplate.queryForObject(sql, OperableObject.class);
        // opLog.setObjectName(dbInfo.getObjectName());

    }

  // *************************** 批量场景的拦截 ******************************

    @Override
    public List<? extends Operable> beforeAssembleBatchLogs(OperationLogDTO template, List<? extends Operable> operableList) {

        // 批量导入业务中,被操作对象可能有多个,假设操作了 100 条,这100个对象记录在一条日志中,可能导致某些字段超出最大长度限制,而记录 100 条又不利于查看,这里可以对其进行自定义分隔条数和批次
        if ("batchImportXxx".equals(template.getOperation())) {
            // 5条操作日志合成一条操作日志,
            List<? extends List<? extends Operable>> partOperableList = CollectionUtil.split(operableList, 5);
            List<Operable> result = new ArrayList<>(partOperableList.size());
            partOperableList.forEach(operables -> result.add(new MultiOperableDecorator(operables)));
            return result;
        }
        return operableList;
    }
}

日志文件采集分析

您已经计划使用 日志文件 形式记录并收集操作日志,您可参考这里。

Shoulder 根据常用的日志加工组件及其扩展,默认提供了两种日志格式,并允许您自定义日志格式。

Shoulder 默认的日志文件记录器(LogOperationLogger)中,通过 OperationLogFormatter 来确认操作日志打印格式。

1.默认的逗号分割格式

默认的日志格式是 ShoulderOpLogFormatter,这种格式牺牲一定的可读性,换来了更好的性能,适合日志采集,无需配置,默认激活。

2. JSON 格式

开发者可通过注入 JsonOperationLogFormatter 来替换默认的 keyValue 格式。

JSON 格式操作日志
@Configuration
public class OpLogConfiguration {

    @Bean
    public JsonOperationLogFormatter jsonOperationLogFormatter() {
        return new JsonOperationLogFormatter();
    }
}
这种格式可读性更好,更容易解析或处理,可直接结合 Kibana / Prometheus / Loki 等技术做进一步分析和归集,且无需经过 LogStash / Fluent 等日志采集组件的加工即可提交给日志分析服务器。

3. 复杂参数格式调整

对于复杂参数的记录时,shoulder 默认调用 toString 方法记录,您可以实现 OperationLogParamValueConverter 接口并将实现类作为 @Bean 注入至 Spring 上下文中改变记录方式。

复杂参数操作日志设置
@Configuration
public class OpLogConfiguration {

    @Bean
    public OperationLogParamValueConverter operationLogParamValueConverter() {
        return new OperationLogParamValueConverter() {
            // your impl
        };
    }
}

4. 自定义格式

对于一些成熟组织,可能配置了统一的日志采集 / 解析规范或脚本,将日志格式转成已有的格式会更利于日志分析。

自定义格式操作日志
@Configuration
public class OpLogConfiguration {

    @Bean
    public OperationLogFormatter operationLogFormatter() {
        return new OperationLogFormatter() {
            @Override
            public String format(OperationLogDTO opLog) {
                return "your implements";
            }
        };
    }
}

规范日志格式

Shoulder 允许您为操作日志制定专门的规范,以在软件交付时需要确保日志格式符合规范,如 a 字段格式必须是xxx,b 字段长度必须在 xxx 以内。

日志格式校验 OperationLogValidator

ShoulderOperationLogValidator 是内置的日志校验器,校验规则参见 [_完整的操作日志],但未在框架中激活强制校验功能,若您想开启,请参考以下代码:

操作日志格式-校验
@Configuration
public class OpLogConfiguration {

    @Bean
    public OperationLogValidator operationLogValidator() {
        return new OperationLogValidator();
    }
}

自定义:日志格式校验

Shoulder 默认实现了 OpLogValidateInterceptor 并提供了 OperationLogValidator 接口,使用时只需要将您的实现类作为 @Bean 注入至 Spring 上下文中即可。

自定义操作日志格式-校验
@Component
public class ShoulderOperationLogValidator implements OperationLogValidator {

    @Override
    public void validate(OperationLogDTO log) {
        // 您的校验逻辑
    }
}