{"id":30509264,"url":"https://github.com/dk900912/oplog-spring-boot","last_synced_at":"2026-03-12T17:39:04.738Z","repository":{"id":49876358,"uuid":"445441136","full_name":"dk900912/oplog-spring-boot","owner":"dk900912","description":"spring boot support for operation log","archived":false,"fork":false,"pushed_at":"2023-09-22T01:38:59.000Z","size":489,"stargazers_count":25,"open_issues_count":0,"forks_count":8,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-07T15:42:48.534Z","etag":null,"topics":["java","operation-audit","operation-log","spring","spring-boot"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dk900912.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-01-07T07:51:41.000Z","updated_at":"2025-06-15T13:41:55.000Z","dependencies_parsed_at":"2025-08-26T00:36:38.551Z","dependency_job_id":"788bf02b-608f-4f4d-8200-b0ed61e46e61","html_url":"https://github.com/dk900912/oplog-spring-boot","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/dk900912/oplog-spring-boot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dk900912%2Foplog-spring-boot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dk900912%2Foplog-spring-boot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dk900912%2Foplog-spring-boot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dk900912%2Foplog-spring-boot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dk900912","download_url":"https://codeload.github.com/dk900912/oplog-spring-boot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dk900912%2Foplog-spring-boot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30435474,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-12T14:34:45.044Z","status":"ssl_error","status_checked_at":"2026-03-12T14:09:33.793Z","response_time":114,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","operation-audit","operation-log","spring","spring-boot"],"created_at":"2025-08-26T00:36:31.299Z","updated_at":"2026-03-12T17:39:04.717Z","avatar_url":"https://github.com/dk900912.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\r\n\u003ca href=\"https://openjdk.java.net/\"\u003e\u003cimg src=\"https://img.shields.io/badge/Java-17+-green?logo=java\u0026logoColor=white\" alt=\"Java support\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://www.apache.org/licenses/LICENSE-2.0.html\"\u003e\u003cimg src=\"https://img.shields.io/github/license/dk900912/oplog-spring-boot?color=4D7A97\u0026logo=apache\" alt=\"License\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://search.maven.org/search?q=a:oplog-spring-boot-starter\"\u003e\u003cimg src=\"https://img.shields.io/maven-central/v/io.github.dk900912/oplog-spring-boot-starter?logo=apache-maven\" alt=\"Maven Central\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://github.com/dk900912/oplog-spring-boot/stargazers\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/dk900912/oplog-spring-boot\" alt=\"GitHub Stars\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://github.com/dk900912/oplog-spring-boot/fork\"\u003e\u003cimg src=\"https://img.shields.io/github/forks/dk900912/oplog-spring-boot\" alt=\"GitHub Forks\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://github.com/dk900912/oplog-spring-boot/issues\"\u003e\u003cimg src=\"https://img.shields.io/github/issues/dk900912/oplog-spring-boot\" alt=\"GitHub issues\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://github.com/dk900912/oplog-spring-boot/graphs/contributors\"\u003e\u003cimg src=\"https://img.shields.io/github/contributors/dk900912/oplog-spring-boot\" alt=\"GitHub Contributors\"\u003e\u003c/a\u003e\r\n\u003ca href=\"https://github.com/dk900912/oplog-spring-boot\"\u003e\u003cimg src=\"https://img.shields.io/github/repo-size/dk900912/oplog-spring-boot\" alt=\"GitHub repo size\"\u003e\u003c/a\u003e\r\n\u003c/p\u003e\r\n\r\n---\r\n本组件已经发布到 maven 中央仓库，依赖于 Spring Boot 3.0+、JDK 17+，大家可以体验一下。GAV信息如下：\r\n```xml\r\n\u003cdependency\u003e\r\n\t\u003cgroupId\u003eio.github.dk900912\u003c/groupId\u003e\r\n\t\u003cartifactId\u003eoplog-spring-boot-starter\u003c/artifactId\u003e\r\n\t\u003cversion\u003e1.4.2\u003c/version\u003e\r\n\u003c/dependency\u003e\r\n```\r\n## 1 快速上手\r\n\r\n分别实现`OperatorService`和`LogRecordPersistenceService`接口，并将实现类声明为一个 Bean。更多拓展点，请大家自行阅读源码！！！\r\n\r\n#### 1.1 声明式风格\r\n```java\r\n@Validated\r\n@RestController\r\n@RequestMapping(path = \"/customer/v1/vpc\")\r\npublic class VpcController {\r\n\r\n   @OperationLog(\r\n           bizCategory = BizCategory.FIND,\r\n           bizTarget = \"VPC\", bizNo = \"#target\")\r\n   @GetMapping\r\n   public AppResult get(@RequestParam(\"target\") String target) {\r\n      return AppResult.builder().code(200).build();\r\n   }\r\n\r\n   @OperationLog(\r\n           bizCategory = BizCategory.UPDATE,\r\n           bizTarget = \"VPC\",\r\n           bizNo = \"#vpc.id\",\r\n           diffSelector = \"io.github.xiaotou.oplog.VpcService#findVpcById(Long)\"\r\n   )\r\n   @PostMapping\r\n   public AppResult post(@RequestBody Vpc vpc) {\r\n      return AppResult.builder().build();\r\n   }\r\n}\r\n```\r\n#### 1.2 编程式风格\r\n```java\r\nfinal SimpleOperationLogCallback\u003cObject, Throwable\u003e simpleOperationLogCallback\r\n        = new SimpleOperationLogCallback\u003c\u003e(BizCategory.UPDATE, \"VPC\", 123L, bizNo -\u003e vpcService.findVpcById((long)bizNo)) {\r\n    @Override\r\n    public Object doBizAction() {\r\n        System.out.println(\"=== UPDATE VPC ===\");\r\n        return \"success\";\r\n    }\r\n};\r\noperationLogTemplate.execute(simpleOperationLogCallback);\r\n```\r\n\r\n## 2 进阶\r\n\r\n1. 支持多租户，其实一个租户往往就是一个特定服务，比如：订单服务。租户信息可以通过`spring.oplog.tenant`配置项来指定。\r\n\r\n2. `LogRecordPersistenceService`用于持久化操作日志，接入方可以基于该接口来定制化持久化逻辑，如：MySQL、ElasticSearch 等；\r\n如果不自行实现 LogRecordPersistenceService 接口，那么本组件会有一个默认的实现，持久化逻辑也就是仅输出一条日志。\r\n```java\r\n    @Bean\r\n    @ConditionalOnMissingBean(LogRecordPersistenceService.class)\r\n    public LogRecordPersistenceService logRecordPersistenceService() {\r\n        return new DefaultLogRecordPersistenceServiceImpl();\r\n    }\r\n```\r\n显然，从上述内容可以看出：如果接入方自定实现了持久化逻辑并且将其声明为一个 Bean，那么本组件所声明的默认持久化策略将不再生效。`OperatorService`的拓展机制同样如此！\r\n\r\n3. 为什么要为 \u003cb\u003eOperationLogPointcutAdvisor\u003c/b\u003e 设定 order 属性呢？或者说为什么对外提供`spring.oplog.advisor.order`配置项呢？OperationLog 注解并不局限于 Controller 层面，也可以将其用于 Service 中的业务方法，无论用于哪一层级，有时需要关注 OperationLogPointcutAdvisor 的执行顺序。\r\n比如：当  OperationLog 注解应用于一个 Transactional 业务方法上，那也许要确保 `OperationLogPointcutAdvisor` 优先级高于 `BeanFactoryTransactionAttributeSourceAdvisor`，否则 OperationLogPointcutAdvisor 中的切面逻辑（持久化、RPC调用等）会拉长整个事务，如果大家想避免这种情况，那么这里就可以自行配置。\r\n\r\n4. 在同一个类中，如果业务方法 A 调用了业务方法 B，且 A 和 B 这俩方法都由 @OperationLog 标记，那么 B 方法中并不会记录操作日志，这是 Spring AOP 的老问题了，官方也提供了解决方法，比如使用`AopContext.currentProxy()`。\r\n\r\n5. 在不同的类中，如果类 A 中方法 m1 调用了 类 B 中方法 m2，且 m1 与 m2 均由 @OperationLog 标记，那么在解析 **bizNo** 的过程中会不会串了呢？不会。\r\n\r\n6. 在数据更新场景中，往往需要对同一个类型的实例进行`diff`，用于实现某人对哪些字段内容进行了修改以及修改前后的内容。diff 功能依托于开源组件，而如何实现更新前后的实例查询（一般就是根据业务 ID 从数据库中查询一条数据）呢？有两个想法：\r\n   \r\n   1）定义一个`DiffSelector`接口，接入方可能需要定义非常多的实现类，对于接入方来说非常不友好；\r\n   \r\n   2）完全依托于`@DiffSelector`注解，该注解需要指定接入方 Service Bean 的名称、方法名、参数、参数类型，然后解析并反射调用方法，但这样会搞得`@OperationLog`注解很臃肿，难看。\r\n\r\n   \u003e 从`@RequestMapping`注解得到了灵感，定义一个`@DiffSelector`注解，接入方将该注解标记在相关 Service Bean 的实例查询方法上，那么在程序启动阶段自动探测并构建`方法名`与`DiffSelectorMethod`实例的映射关系，后续接入方只需要在`@OperationLog`注解中指定方法名即可。\r\n\r\n7. 业务 ID 并不局限于 String，也可以是 int、long 等，而 bizNo 解析出来的一定是一个 String 类型，所以这里涉及一个类型转换，直接使用`ConversionService`实现的\r\n```java\r\n    private Object convertBizNoIfNecessary(Object bizNo, Class\u003c?\u003e bizNoClazz) {\r\n        if (conversionService.canConvert(bizNoClazz, bizNoClazz)) {\r\n            try {\r\n                return conversionService.convert(bizNo, bizNoClazz);\r\n            } catch (ConversionFailedException e) {\r\n                logger.warn(\"BizNo convert failed, from {} to {}\", bizNo.getClass(), bizNoClazz);\r\n            }\r\n        } else {\r\n            logger.warn(\"ConversionService can not convert this bizNo, bizNo = {}\", bizNo);\r\n        }\r\n        return bizNo;\r\n    }\r\n```\r\n\r\n8. `diff`仅仅支持普通的数据类型（基础数据类型、LocalDate、LocalDateTime、ZonedDateTime、LocalTime、Date 等），不支持集合等类型，但这一点应该是刚好够用了。\r\n\r\n9. `diff`结果在并发场景下是有可能串掉的，但这并不是本组件的 bug，应该是大家没有做好“对共享资源的互斥访问”吧。\r\n\r\n10. 在运行过程中，可能会提示若干条日志，如：`Bean 'operationLogTemplate' of type [io.github.dk900912.oplog.support.OperationLogTemplate] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)` 。\r\n大家不用慌张，直接忽略就好了，因为本组件声明的 Bean 并不需要走一遍所有的 BPP（比如有一个比较重要的 BPP 是用来生成代理 Bean 的，本组件所声明的 Bean 同样不需要为其生成代理类）。\r\n\r\n11. 在编程式更新场景中，DiffSelector 如何指定呢？直接塞进去一个`Function`即可，比如：`bizNo -\u003e vpcService.findVpcById((long)bizNo)`。\r\n\r\n12. `OperationLogContext`中保存了一些上下文信息，主要是围绕`@OperationLog`注解属性的一些内容，比如：`OperationLogInfo`实例和`diff-selector`查询到的`previous content`。而 OperationLogContext 实例贮存在何处呢？\r\n没错，就是`ThreadLocal`，本组件内置了一个实现，即`ThreadLocalOperationLogContextImplStrategy`。当然，大家也可以基于`ITL`、`TTL`来实现，这样的拓展是完全支持的，如下所示。\r\n```java\r\npublic class OperationLogSynchronizationManager {\r\n\r\n\tprivate static String strategyName = System.getProperty(\"spring.oplog.context.strategy\");\r\n\r\n\tprivate static OperationLogContextImplStrategy strategy;\r\n\r\n\tstatic {\r\n\t\tinitialize();\r\n\t}\r\n\r\n\tprivate OperationLogSynchronizationManager() {}\r\n\r\n\tprivate static void initialize() {\r\n\t\tif (!StringUtils.hasText(strategyName)) {\r\n\t\t\tstrategyName = DEFAULT_CONTEXT_STRATEGY;\r\n\t\t}\r\n\r\n\t\tif (strategyName.equals(DEFAULT_CONTEXT_STRATEGY)) {\r\n\t\t\tstrategy = new ThreadLocalOperationLogContextImplStrategy();\r\n\t\t} else {\r\n\t\t\ttry {\r\n\t\t\t\tClass\u003c?\u003e clazz = Class.forName(strategyName);\r\n\t\t\t\tConstructor\u003c?\u003e customStrategy = clazz.getConstructor();\r\n\t\t\t\tstrategy = (OperationLogContextImplStrategy) customStrategy.newInstance();\r\n\t\t\t} catch (Exception ex) {\r\n\t\t\t\tReflectionUtils.handleReflectionException(ex);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n上面代码清晰地交代了替换`OperationLogContextImplStrategy`实现类的方式，即通过 VM Options 来追加`-Dspring.oplog.context.strategy=xxx.TtlOperationLogContextImplStrategy`。\r\n\r\n话说回来，究竟什么时候需要使用阿里的 TTL 替换 TL 呢？其实是没必要的，虽然 OperationLogContext 实例是有父 OperationLogContext 的，但目前代码中并不存在这样的逻辑：当前`子OperationLogContext`从`父OperationLogContext`中获取继承的信息。\r\n唯一的影响如下场景中：父子 OperationLogContext 实例的关联关系断掉了而已。\r\n\r\n```java\r\n@Validated\r\n@RestController\r\n@RequestMapping(path = \"/customer/v1/vpc\")\r\npublic class VpcController {\r\n\r\n    @OperationLog(\r\n            bizCategory = BizCategory.FIND,\r\n            bizTarget = \"HI\", bizNo = \"#target\")\r\n    @GetMapping\r\n    public AppResult get(@RequestParam(\"target\") String target) {\r\n        final VpcController o = (VpcController) AopContext.currentProxy();\r\n        o.delete(target);\r\n        return AppResult.builder().code(200).build();\r\n    }\r\n\r\n    @Async(\"customThreadPoolTaskExecutor\")\r\n    @OperationLog(\r\n            bizCategory = BizCategory.DELETE,\r\n            bizTarget = \"HI\", bizNo = \"#target\")\r\n    @DeleteMapping\r\n    public void delete(@RequestParam(\"target\") String target) {\r\n        System.out.println(\"deleted\");\r\n    }\r\n}\r\n```\r\nDEBUG 日志如下：\r\n```\r\n2023-09-18T16:32:52.646+08:00 DEBUG 2684 --- [nio-8081-exec-1] i.g.d.o.a.a.OperationLogInterceptor      : 0={======\u003e OperationLogContextSupport[id='1601237157', parent='0', context.operation_log_info='{bizCategory=FIND, bizTarget=HI, bizNo=999, diffSelector=}'] \u003c======}=0\r\n2023-09-18T16:32:52.657+08:00 DEBUG 2684 --- [nsole_network_1] i.g.d.o.a.a.OperationLogInterceptor      : 0={======\u003e OperationLogContextSupport[id='756422871', parent='0', context.operation_log_info='{bizCategory=DELETE, bizTarget=HI, bizNo=999, diffSelector=}'] \u003c======}=0\r\n```\r\n\r\n为什么会出现这样的问题呢？customThreadPoolTaskExecutor 线程池在启动阶段就已完成了初始化，TL 就是会串掉的，TTL 也正是为了解决这一问题而诞生的。\r\n\r\nTL 替换为 TTL 后，再看 父子 OperationLogContext 实例的关联关系已经接上了：\r\n```\r\n2023-09-18T16:36:48.671+08:00 DEBUG 21304 --- [nio-8081-exec-1] i.g.d.o.a.a.OperationLogInterceptor      : 0={======\u003e OperationLogContextSupport[id='554254994', parent='0', context.operation_log_info='{bizCategory=FIND, bizTarget=HI, bizNo=999, diffSelector=}'] \u003c======}=0\r\n2023-09-18T16:36:48.685+08:00 DEBUG 21304 --- [nsole_network_1] i.g.d.o.a.a.OperationLogInterceptor      : 0={======\u003e OperationLogContextSupport[id='995785809', parent='554254994', context.operation_log_info='{bizCategory=DELETE, bizTarget=HI, bizNo=999, diffSelector=}'] \u003c======}=0\r\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdk900912%2Foplog-spring-boot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdk900912%2Foplog-spring-boot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdk900912%2Foplog-spring-boot/lists"}