From e651963c4e73b6c11df6d44c9c028a507bafaf7b Mon Sep 17 00:00:00 2001 From: yuanyuqin Date: Mon, 14 Mar 2022 23:13:15 +0800 Subject: [PATCH] init --- .gitignore | 26 ++ README.md | 309 ++++++++++++++ operation-log-starter/pom.xml | 22 + operation-log/pom.xml | 76 ++++ .../log/annotation/LogRecordAnnotation.java | 75 ++++ .../operation/log/annotation/OpLogField.java | 90 +++++ .../operation/log/aop/LogRecordPointcut.java | 228 +++++++++++ .../log/aop/MethodExecuteResult.java | 21 + .../core/DefaultOperatorGetServiceImpl.java | 13 + .../operation/log/core/LogRecordContext.java | 79 ++++ .../log/core/OperatorGetService.java | 31 ++ .../core/diff/AbstractObjectDiffHandler.java | 128 ++++++ .../core/diff/DefaultObjectDiffHandler.java | 130 ++++++ .../log/core/diff/ObjectDiffHandler.java | 20 + .../expression/BaseLogRecordExpression.java | 162 ++++++++ .../expression/CustomMethodExpression.java | 138 +++++++ .../log/core/expression/DiffExpression.java | 100 +++++ .../core/expression/LogRecordExpression.java | 57 +++ .../function/DefaultFunctionServiceImpl.java | 32 ++ .../log/core/function/FunctionService.java | 19 + .../log/core/function/MethodResolver.java | 29 ++ .../log/core/function/ParseFunction.java | 23 ++ .../core/function/ParseFunctionFactory.java | 34 ++ .../PrioritizedFunctionServiceImpl.java | 32 ++ .../SpringContextBeanFunctionServiceImpl.java | 59 +++ .../TargetObjectFunctionServiceImpl.java | 34 ++ .../core/parse/CachedExpressionEvaluator.java | 122 ++++++ .../parse/LogRecordEvaluationContext.java | 28 ++ .../parse/LogRecordExpressionEvaluator.java | 43 ++ .../core/parse/LogRecordExpressionParser.java | 124 ++++++ .../record/DefaultLogRecordServiceImpl.java | 18 + .../log/core/record/LogRecordService.java | 17 + .../ad/operation/log/model/CustomMethod.java | 50 +++ .../ad/operation/log/model/LogRecord.java | 32 ++ .../operation/log/model/StandaloneSpel.java | 22 + .../operation/log/model/TemplateContext.java | 49 +++ .../ad/operation/log/util/RegexpUtil.java | 23 ++ .../ad/operation/log/util/TemplateUtil.java | 23 ++ .../log/test/LogRecordPointcutTest.java | 382 ++++++++++++++++++ .../log/test/LogRecordPointcutTestConfig.java | 91 +++++ pom.xml | 48 +++ 41 files changed, 3039 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 operation-log-starter/pom.xml create mode 100644 operation-log/pom.xml create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/annotation/LogRecordAnnotation.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/annotation/OpLogField.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/aop/LogRecordPointcut.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/aop/MethodExecuteResult.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/DefaultOperatorGetServiceImpl.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/LogRecordContext.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/OperatorGetService.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/diff/AbstractObjectDiffHandler.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/diff/DefaultObjectDiffHandler.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/diff/ObjectDiffHandler.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/expression/BaseLogRecordExpression.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/expression/CustomMethodExpression.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/expression/DiffExpression.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/expression/LogRecordExpression.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/DefaultFunctionServiceImpl.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/FunctionService.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/MethodResolver.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunction.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunctionFactory.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/PrioritizedFunctionServiceImpl.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/SpringContextBeanFunctionServiceImpl.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/function/TargetObjectFunctionServiceImpl.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/parse/CachedExpressionEvaluator.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordEvaluationContext.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionEvaluator.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionParser.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/record/DefaultLogRecordServiceImpl.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/core/record/LogRecordService.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/model/CustomMethod.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/model/LogRecord.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/model/StandaloneSpel.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/model/TemplateContext.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/util/RegexpUtil.java create mode 100644 operation-log/src/main/java/com/water/ad/operation/log/util/TemplateUtil.java create mode 100644 operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTest.java create mode 100644 operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTestConfig.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e1332f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +out/ +build/ +classes/ +target/ +session_data/ +logs/ +*.jar +*.war +*.class + +#ide-eclipse +.project +.settings +.classpath +.idea/ +*.iml +*.eml +*.log + +#mac os +.DS_Store +.settings/ +node_modules/ +upload/ +package.json +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff79658 --- /dev/null +++ b/README.md @@ -0,0 +1,309 @@ +# operation-log-parent +操作日志生成组件(操作日志又称系统变更日志、审计日志等) + +# 背景 +不管是B端还是C端系统,在用户使用过程中,都会涉及到对相关资源进行更新或者删除的操作,如电商系统中商家修改商品售价,OA系统中管理员修改用户的权限等,数据库中一般记录的都是资源的最后修改时间和修改人。第一是可读性比较差,只能是程序员能够查询使用,第二是缺少修改前的值,无法对数据进行追溯。 +# 问题 +1. 如何生成可读性高的操作日志 +2. 操作日志内容包含修改前后的值,方便后期的数据追溯 +# 如何生成可读性高的操作日志 +一个可读性高的操作日志,应包含下面几部分 +- 操作人:张三 +- 操作时间:2022-03-23 19:00:00 +- 业务模块:商品 +- 业务标识号:100878(这里是操作的资源对象id,当前案例是商品id) +- 操作内容:修改了商品价格,xxxx +# 操作日志内容包含修改前后的值 +- 操作内容:修改了商品价格,从12¥调整到0.1¥ +- 操作内容:修改了商品价格,从14¥调整到0.01¥ +# 理想的操作日志列表展示 +| 操作人 | 操作时间 | 业务模块 | 业务标识号 | 操作内容 | +| --- | --- | --- | --- | --- | +| 张三 | 2022-03-23 19:00:00 | 商品 | 100878 | 修改了商品价格,从12¥调整到0.1¥ | +| 李四 | 2022-03-24 19:00:00 | 商品 | 200878 | 修改了商品价格,从14¥调整到0.01¥ | +# 如何优雅的生成操作日志 +## 方案一:手动在业务代码中记录(不够优雅) +所有的埋点在业务代码里面手动埋入,在变更事件触发之前,查询一次资源对应的状态并记录在内存中,变更之后再记录变更后资源的状态,最后将前后状态、操作人、变更时间一起写到数据库埋点的代码中。 +``` +public void updateApp(SmtApp newApp){ + + //操作日志实体对象 + OperateLog operateLog = new OperateLog(); + operateLog.setUserId("当前用户ID"); + //业务对象标识-这里是应用id + operateLog.setBizNo(newApp.id); + //操作发生时间 + operateLog.setCreatedTime(new Date); + //操作类别,这里是应用变更 + operateLog.setCategory("应用变更"); + //操作日志详情,记录操作的业务对象的变更信息 + operateLog.setDetail("更新了应用信息,应用名称:oldName --> newName"); + //保存操作日志 + operateLogService.save(operateLog); + + /******忽略后续的应用更新代码************/ +} +``` +优点: +- 方案简单,没有难度,堆人堆时间就能完成,简单明了 + +缺点: +- 业务侵入性大,完全耦合正常的业务 +- 扩展性非常差,想增加其他维度的埋点时,需要修改所有埋点的业务代码 + +## 方案二:使用 Canal 监听数据库记录操作日志(不够优雅) +canal 是一款基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费的开源组件,通过采用监听数据库 Binlog 的方式,这样可以从底层知道是哪些数据做了修改,然后根据更改的数据记录操作日志。 + +优点: +- 和业务逻辑完全分离 + +缺点: +- 难以记录操作前的内容 +- 只能针对数据库的更改做操作记录,如涉及到和外部交互的部分,无法记录,如发送邮件、短信、RPC调用 +- 记录的操作结果内容只适合开发人员看,无法给到产品和运营人员使用 +## 基于AOP方法注解实现操作日志 +为了解决上面几个方案所带来的问题,一般采用 AOP 的方式记录日志,让操作日志和业务逻辑解耦,接下来看一个简单的 AOP 日志的例子。伪代码如下: +``` +@LogRecordAnnotation(detail = "更新了用户名称,从{#oldName}改为{#newName}", bizNo = "#userId", category = "用户更改") +public void updateNameById(Long userId, Sting oldName,String newName) { + // do update action +} +``` +# 使用AOP方案优雅的记录操作日志 +结合上一节如何生成可读性高的操作日志,那么AOP注解接口需要包含以下几个关键属性: + +``` +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LogRecordAnnotation { + + String operator() default ""; + + String bizNo(); + + String category(); + + String detail() default ""; + + String condition() default "true"; +} +``` + +| 属性 | 是否必须 | 说明 | +| --- | --- | --- | +| operator | 否 | 操作人 | +| bizNo | 是 | 操作的业务模块 | +| category | 是 | 操作的业务资源标识 | +| detail | 是 | 操作日志详情 | +| condition | 否 | 操作日志记录条件,表达式返回boolean类型 | + +> conditon属性我们稍后再展开说明 + +**operator**属性设置成非必填主要是为了减少冗余的赋值操作,正常的项目中,都会保存当前的用户信息到一个请求上下文中,如UserContext,操作日志组件提供统一的OperatorGetService接口,由使用方实现,返回当前用户信息 +``` +//组件提供接口 +public interface OperatorGetService { + + Operator getUser(); + + @Data + class Operator { + + private String id; + + private String name; + } +} +//使用方实现 +public static class MarketOperatorGetServiceImpl implements OperatorGetService { + @Override + public Operator getUser() { + Long userId = UserContext.getUserId(); + String userName = UserContext.getRealName(); + return Operator.builder() + .id(String.valueOf(userId)) + .name(userName) + .build(); + } +} +``` + +下面的讲解统一使用更新用户信息为例子展开 +``` + @Data + public class User { + /** + * 用户id + */ + private Integer id; + /** + * 用户所属部门id + */ + private Long departmentId; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private Integer age; + + /** + * 用户状态 + * 0-禁用,1-启用 + */ + private Integer status; + + private Date createdTime; + + private Date updatedTime; + } +``` +## 更新用户单个属性(名称) +常规的更新用户名称的业务方法如下 +``` +void updateNameById(Integer id, String newName) { + //doUpdate +} +``` +在方法上面加上我们定义的注解 +``` + @LogRecordAnnotation(detail = "更新了用户名称,改为{#newName}", bizNo = "#id", category = "用户更改") +void updateNameById(Integer id, String newName) { + //doUpdate +} +``` +很明显,操作日志详情缺少旧的用户名,无法满足使用需要,解决方法有两种: + +**第一种是让开发在方法参数中加上旧的用户名称**: +``` + @LogRecordAnnotation(detail = "更新了用户名称,从{#oldName}改为{#newName}", bizNo = "#id", category = "用户更改") +void updateNameById(Integer id,String oldName, String newName) { + //doUpdate +} +``` +这种方式在原有方法上面强行增加了一个也业务无关的参数,既不符合相关设计原则,也无法说服带有强迫症的开发,毕竟这种方式违反开发常识了。 + +**第二种是使用自定义模板表达式,来获取旧的用户名称:** +``` + @LogRecordAnnotation(detail = "更新了用户名称,从{getUserNameById(#id)}改为{#newName}", bizNo = "#id", category = "用户更改") +void updateNameById(Integer id,String newName) { + //doUpdate +} + +String getUserNameById(Integer id){ + User user =; //get user by select db + return user.getName(); +} +``` +- 模板表达式```{getUserNameById(#id)}```用来获取旧的用户名称,代表调用```getUserNameById(#id)``` 方法来获取用户名称,其中的参数使用`spel`表达式引用了原方法中的参数`id`(用户id) +- 模板表达式`{#newName}` 用来获取新用户名称,`#newName`使用spel表达式引用了原方法中的参数`newName` +- 外层使用大括号`{}`括起来是为了方便和`spel`表达式作区分 +> 自定义模板相关的详细说明将放在代码实现章节讲解 + +## 更新用户多个属性 +上一节我们讲解了在更新单个属性的时候,如何去记录操作日志,如果是多个属性的情况,我们又改如何去记录呢?如下面的业务方法: +``` +/** +* 通过用户id更新用户信息 +* @param userId 用户id +* @param newUser 新的用户信息 +*/ +void updateById(Integer userId, User newUser) { + //doUpdate +} +``` +用户信息的更新场景包含下面几种情形: +- 只是更新了单个属性 +- 同时更新了多个属性 +- 不需要记录`updatedTime`属性的变更,因为操作日志本身包含了操作时间 + +那么我们如何在不修改原有业务代码的情况下实现上面3中情形呢? + +**第一需要使用到对象的``diff``操作** +>对象`diff`操作说明:对比两个同类型对象的属性值差异,并得出差异结果,如更改了用户的多个属性,那么期望得到的操作内容:name: yyq-old --> yyq-plus,age: 28 --> 29 + +我们将自定义模板中的diff声音定义如下: +``` +diff({oldObject},{newObject}) +``` +使用约定大于配置的理论,将需要`diff`的两个对象使用`diff()`表达式包起来,其中的`{oldObject}`和`{newObject}`两个子表达式用法和上一节中的用法一样,可以调用方法,也可以直接引用方法参数 + +**第二需要定义一个注解来标识目标对象有哪些属性是需要进行diff操作:** +``` +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface OpLogField { + + String fieldName() default ""; + + String fieldMapping() default "{}"; + + String dateFormat() default "yyyy-MM-dd HH:mm:ss"; + + String decimalFormat() default ""; + + //先讲关键属性,暂时跳过其他属性 +``` + +| 属性 | 是否必须 | 说明 | +| --- | --- | --- | +| fieldName| 否 | diff结果中显示属性别名,起到易读作用,如name显示为名称 | +| fieldMapping| 否 | diff结果中显示属性值别名,起到易读作用,json格式,如status可设置为{"0": "禁用", "1": "启用"} | +| dateFormat| 否 | diff结果中的时间类型属性值格式化样式,起到易读作用 | +| decimalFormat| 否 | diff结果中的数值类型格式化样式,默认不格式化,如设置为#,###.##则Double d = 554545.4545454的数值将被显示为 554,545.45| + +使用该注解对User相关属性进行标识: +``` + @Data + private static class User { + + private Integer id; + + @OpLogField(fieldName="部门id") + private Long departmentId; + + @OpLogField(fieldName="名称") + private String name; + + @OpLogField(fieldName="年龄") + private Integer age; + + @OpLogField(fieldName="状态",="{"0": "禁用", "1": "启用"}") + private Integer status; + + private Date createdTime; + + pricate Date updatedTime; + } +``` +接着使用操作日志注解标注该方法 +``` +/** +* 通过用户id更新用户信息 +* @param userId 用户id +* @param newUser 新的用户信息 +*/ +@LogRecordAnnotation(detail = "更新了用户名称,diff({getUserById(#userId)},{#newUser})", bizNo = "#id", category = "用户更改") +void updateById(Integer userId, User newUser) { + //doUpdate +} + +String getUserById(Integer id){ + User user =; //get user by select db + return user; +} +``` +假定`oldUser`是`(id=1, departmentId=1, name=yyq-old, age=28, status=0)` +假定`newUser`是`(id=1, departmentId=1, name=yyq-plus, age=29, status=1)` +执行该方法将得到的操作日志内容: +> 更改用户信息,名称: yyq-old --> yyq-plus,年龄: 28 --> 29,状态:禁用 --> 启用 + +# 代码实现架构 +持续补充 +# 使用手册 +持续补充 diff --git a/operation-log-starter/pom.xml b/operation-log-starter/pom.xml new file mode 100644 index 0000000..75bb773 --- /dev/null +++ b/operation-log-starter/pom.xml @@ -0,0 +1,22 @@ + + + + operation-log-parent + com.water + 1.0 + + 4.0.0 + + operation-log-starter + 1.0 + + + com.water + operation-log + 1.0 + + + + diff --git a/operation-log/pom.xml b/operation-log/pom.xml new file mode 100644 index 0000000..41b4845 --- /dev/null +++ b/operation-log/pom.xml @@ -0,0 +1,76 @@ + + + + operation-log-parent + com.water + 1.0 + + 4.0.0 + + operation-log + 1.0 + + + + 1.18.20 + 4.3.16.RELEASE + 1.2.76 + 3.12.0 + 4.4 + 31.0.1-jre + + + + + org.springframework.boot + spring-boot-starter-test + test + + + asm + org.ow2.asm + + + + + org.projectlombok + lombok + ${lombok.version} + + + org.springframework.boot + spring-boot-starter-aop + + + junit + junit + + + org.springframework + spring-expression + ${spring.expression.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + org.apache.commons + commons-collections4 + ${common.collection4} + + + com.google.guava + guava + ${guava.version} + + + diff --git a/operation-log/src/main/java/com/water/ad/operation/log/annotation/LogRecordAnnotation.java b/operation-log/src/main/java/com/water/ad/operation/log/annotation/LogRecordAnnotation.java new file mode 100644 index 0000000..99b1f66 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/annotation/LogRecordAnnotation.java @@ -0,0 +1,75 @@ +package com.water.ad.operation.log.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * + * @author yyq + **/ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LogRecordAnnotation { + + String BEFORE_METHOD_PREFIX = "beforeExecute."; + + /** + * 操作日志的执行人 + * + * @return operator + */ + String operator() default ""; + + /** + * 业务标识号,支持spel表达式 + * + * @return bizNo + */ + String bizNo(); + + /** + * 操作日志种类,字符串常量 + * + * @return category + */ + String category(); + + /** + * 操作内容 + * 普通字符串模式:修改了名称,从{xxx}修改为{xxx} + * diff模式:更新的应用,diff({xxx},{xxx}) + * + * @return detail + */ + String detail() default ""; + + /** + * diff操作时指定只对应特定的field,默认全部,逗号分割 + * field1,field2 + * + * @return diffField + */ + String diffField() default ""; + + /** + * 是否记录操作日志判断表达式,支持spel表达式,返回值必须是布尔类型 + * + * @return condition boolean + */ + String condition() default "true"; + + /** + * 普通模板(非diff情况)下的返回值映射 + * JSON string mapper

+ * A typical value should look like:

+ * ---- {"0": "disabled", "1": "enabled"} ----

+ * Map the field values like [0/1] to a human-readable string like [enabled/disabled]

+ * + * @return + */ + String commonValueMapping() default "{}"; + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/annotation/OpLogField.java b/operation-log/src/main/java/com/water/ad/operation/log/annotation/OpLogField.java new file mode 100644 index 0000000..ae75ea2 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/annotation/OpLogField.java @@ -0,0 +1,90 @@ +/** + * Copyright 2020 Jasper J B Deng(djbing85@gmail.com) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.water.ad.operation.log.annotation; + + +import java.lang.annotation.*; + +/** + * @author djbing85@gmail.com + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface OpLogField { + + + /** + * field name in a human-friendly way, + * default/empty field name in a BO means to generate oplog with it's raw field name + * + * @return field name + */ + String fieldName() default ""; + + + /** + * JSON string mapper

+ * A typical value should look like:

+ * ---- {"0": "disabled", "1": "enabled"} ----

+ * Map the field values like [0/1] to a human-readable string like [enabled/disabled]

+ * Oplog intercepter will take the field value as key, + * "translate" it to the mapped value while generating the oplog automatically

+ * Default or empty means field value will be used directly

+ * A malformed JSON string will result to a false translate: that is, to use raw value. + * + * @return Default "{}" + */ + String fieldMapping() default "{}"; + + /** + * Specify if the field should be ignored when generate oplog detail

+ * An ignore field will be skip when compare differences between two BO object

+ * + * @return Default false + */ + boolean ignore() default false; + + + /** + * Date format will only apply on field type list below:

+ * java.util.Date

+ * java.util.Calendar

+ * java.time.LocalDate

+ * java.time.LocalTime

+ * java.time.LocalDateTime

+ * Invalid date format will result to a format failure,

+ * + * @return Default "yyyy-MM-dd HH:mm:ss" + */ + String dateFormat() default "yyyy-MM-dd HH:mm:ss"; + + /** + * A valid decimalFormat should looks like: #,###.##

+ * Will try to format Double/Float/Long/Integer/BigDecimal fields

+ * Empty decimalFormat will be ignore

+ *

+ * For example: #,###.##

+ * Double d = 554545.4545454;

+ * Formatted String: 554,545.45;

+ * Long l = 1234567890;

+ * Formatted String: 1,234,567,890

+ * + * @return Default "" + */ + String decimalFormat() default ""; + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/aop/LogRecordPointcut.java b/operation-log/src/main/java/com/water/ad/operation/log/aop/LogRecordPointcut.java new file mode 100644 index 0000000..656c849 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/aop/LogRecordPointcut.java @@ -0,0 +1,228 @@ +package com.water.ad.operation.log.aop; + +import com.water.ad.operation.log.annotation.LogRecordAnnotation; +import com.water.ad.operation.log.core.LogRecordContext; +import com.water.ad.operation.log.core.OperatorGetService; +import com.water.ad.operation.log.core.expression.LogRecordExpression; +import com.water.ad.operation.log.core.parse.LogRecordExpressionEvaluator; +import com.water.ad.operation.log.core.parse.LogRecordExpressionParser; +import com.water.ad.operation.log.core.record.LogRecordService; +import com.water.ad.operation.log.model.LogRecord; +import com.water.ad.operation.log.model.TemplateContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.annotation.Order; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; +import java.util.Date; + + +/** + * @author yyq + */ +@Aspect +@Slf4j +@Order(1) +public class LogRecordPointcut { + + + private LogRecordExpressionEvaluator logRecordExpressionEvaluator; + private LogRecordService iLogRecordService; + private OperatorGetService operatorGetService; + + public LogRecordPointcut(LogRecordExpressionEvaluator logRecordExpressionEvaluator, + LogRecordService logRecordService, + OperatorGetService operatorGetService, + LogRecordExpressionParser logRecordExpressionParser) { + Assert.notNull(logRecordExpressionEvaluator, "logRecordExpressionEvaluator can not be null"); + Assert.notNull(logRecordService, "logRecordService can not be null"); + Assert.notNull(operatorGetService, "operatorGetService can not be null"); + this.logRecordExpressionEvaluator = logRecordExpressionEvaluator; + this.iLogRecordService = logRecordService; + this.operatorGetService = operatorGetService; + } + + @Around("@annotation(com.water.ad.operation.log.annotation.LogRecordAnnotation)") + public Object record(ProceedingJoinPoint joinPoint) throws Throwable { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method method = methodSignature.getMethod(); + + Object ret = null; + MethodExecuteResult methodExecuteResult = new MethodExecuteResult(); + LogRecordAnnotation logRecordAnnotation = null; + Object[] args = joinPoint.getArgs(); + Object target = joinPoint.getTarget(); + Class targetClass = target.getClass(); + TemplateContext templateContext = null; + try { + logRecordAnnotation = method.getAnnotation(LogRecordAnnotation.class); + templateContext = parseTemplate(method, args, target, logRecordAnnotation); + LogRecordContext.putVariable(LogRecordContext.TEMPLATE_CONTEXT_KEY, templateContext); + processBeforeExecuteFunction(templateContext); + } catch (Exception e) { + methodExecuteResult.setSuccess(false); + log.error("log record parse before function exception {}", e.getMessage(), e); + } + try { + ret = joinPoint.proceed(args); + } catch (Exception e) { + methodExecuteResult = new MethodExecuteResult(false, e, e.getMessage()); + } + try { + if (logRecordAnnotation != null && templateContext != null && methodExecuteResult.isSuccess()) { + recordExecute(ret, templateContext, logRecordAnnotation); + } + } catch (Exception t) { + //记录日志错误不要影响业务 + log.error("log record parse exception", t); + } finally { + LogRecordContext.clear(); + } + if (methodExecuteResult.getThrowable() != null) { + throw methodExecuteResult.getThrowable(); + } + return ret; + } + + + /** + * 模板解析 + * + * @param logRecordAnnotation logRecordAnnotation + * @return TemplateContext + */ + private TemplateContext parseTemplate(Method method, Object[] args, Object targetObject, + LogRecordAnnotation logRecordAnnotation) { + String detail = logRecordAnnotation.detail(); + String bizNo = logRecordAnnotation.bizNo(); + String operator = logRecordAnnotation.operator(); + String category = logRecordAnnotation.category(); + String condition = logRecordAnnotation.condition(); + TemplateContext template = new TemplateContext(); + + if (StringUtils.isEmpty(bizNo)) { + throw new ExpressionException("bizNo expression is empty"); + } + if (StringUtils.isEmpty(category)) { + throw new ExpressionException("category expression is empty"); + } + if (StringUtils.isEmpty(condition)) { + throw new ExpressionException("condition expression is empty"); + } + + //自定义模板解析 + EvaluationContext context = logRecordExpressionEvaluator.createEvaluationContext(method, args, targetObject); + AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(method, targetObject.getClass()); + + LogRecordExpression detailExpression = logRecordExpressionEvaluator.parseExpression(detail, annotatedElementKey); + Expression bizNoExpression = logRecordExpressionEvaluator.parseExpression(bizNo, annotatedElementKey); + if (StringUtils.isNotEmpty(operator)) { + Expression operatorExpression = logRecordExpressionEvaluator.parseExpression(operator, annotatedElementKey); + template.setOperatorExpression(operatorExpression); + + } + Expression conditionExpression = logRecordExpressionEvaluator.parseExpression(condition, annotatedElementKey); + template.setDetailExpression(detailExpression); + template.setBiNoExpression(bizNoExpression); + template.setConditionExpression(conditionExpression); + template.setEvaluationContext(context); + template.setTargetObject(targetObject); + return template; + } + + /** + * 自定义函数-前置执行逻辑 + * + * @param template template + */ + private void processBeforeExecuteFunction(TemplateContext template) { + + Expression[] beforeExpressions = template.getDetailExpression().beforeExecute(); + if (beforeExpressions != null) { + for (Expression beforeExpression : beforeExpressions) { + beforeExpression.getValue(template.getEvaluationContext()); + } + } + } + + /** + * @param ret target method return + * @param template template + * @param logRecordAnnotation logRecordAnnotation + */ + private void recordExecute(Object ret, TemplateContext template, LogRecordAnnotation + logRecordAnnotation) { + + EvaluationContext context = template.getEvaluationContext(); + //返回值也放到context中 + context.setVariable("ret", ret); + //是否记录 + boolean condition = handlerCondition(template); + if (!condition) { + log.warn("condition expression [{}] is false ,skip record log", template.getConditionExpression().getExpressionString()); + return; + } + //指定所有操作内容 + String content = logRecordAnnotation.detail(); + if (LogRecordContext.getVariables().containsKey(LogRecordContext.CUSTOM_LOG_DETAIL_KEY)) { + content = (String) LogRecordContext.getVariables().get(LogRecordContext.CUSTOM_LOG_DETAIL_KEY); + } else { + content = template.getDetailExpression().getValue(context, String.class); + } + //指定附加内容 + String appendContent; + if (LogRecordContext.getVariables().containsKey(LogRecordContext.CUSTOM_LOG_APPEND_DETAIL_KEY)) { + appendContent = (String) LogRecordContext.getVariables().get(LogRecordContext.CUSTOM_LOG_APPEND_DETAIL_KEY); + content = content + appendContent; + } + String category = logRecordAnnotation.category(); + String bizNo = handlerBizNo(template); + String operator = handlerOperator(template); + iLogRecordService.record(LogRecord.builder() + .time(new Date()) + .operatorId(operator) + .category(category) + .bizNo(bizNo) + .detail(content).build()); + } + + private String handlerBizNo(TemplateContext template) { + String bizNo = template.getBiNoExpression().getValue(template.getEvaluationContext(), String.class); + if (StringUtils.isEmpty(bizNo)) { + throw new EvaluationException("bizNo is null"); + } + return bizNo; + } + + private String handlerOperator(TemplateContext template) { + String operator; + if (template.getOperatorExpression() != null) { + operator = template.getOperatorExpression().getValue(template.getEvaluationContext(), String.class); + } else { + if (operatorGetService.getUser() == null) { + throw new EvaluationException("operatorGetService return null info"); + } + operator = operatorGetService.getUser().getId(); + } + if (StringUtils.isEmpty(operator)) { + throw new EvaluationException("operator is null"); + } + return operator; + } + + private boolean handlerCondition(TemplateContext template) { + return template.getConditionExpression().getValue(template.getEvaluationContext(), Boolean.class); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/aop/MethodExecuteResult.java b/operation-log/src/main/java/com/water/ad/operation/log/aop/MethodExecuteResult.java new file mode 100644 index 0000000..2ecb7e1 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/aop/MethodExecuteResult.java @@ -0,0 +1,21 @@ +package com.water.ad.operation.log.aop; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author yyq + * @create 2022-02-17 + **/ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class MethodExecuteResult { + + private boolean success = true; + private Throwable throwable; + private String errorMsg; +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/DefaultOperatorGetServiceImpl.java b/operation-log/src/main/java/com/water/ad/operation/log/core/DefaultOperatorGetServiceImpl.java new file mode 100644 index 0000000..812bdc2 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/DefaultOperatorGetServiceImpl.java @@ -0,0 +1,13 @@ +package com.water.ad.operation.log.core; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public class DefaultOperatorGetServiceImpl implements OperatorGetService { + + @Override + public Operator getUser() { + return Operator.builder().id("-1").name("unknown").build(); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/LogRecordContext.java b/operation-log/src/main/java/com/water/ad/operation/log/core/LogRecordContext.java new file mode 100644 index 0000000..ba254a6 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/LogRecordContext.java @@ -0,0 +1,79 @@ +package com.water.ad.operation.log.core; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +/** + * @author yyq + * @create 2022-02-17 + **/ +@Getter +@Setter +public class LogRecordContext { + + /** + * 使用栈结构,解决嵌套操作记录问题 + */ + private static final InheritableThreadLocal>> VARIABLE_MAP_STACK = new InheritableThreadLocal>>() { + @Override + protected Stack> initialValue() { + Stack> stack = new Stack<>(); + stack.push(new HashMap<>(16)); + return stack; + } + }; + + /** + * 自定义操作内容 + */ + public static final String CUSTOM_LOG_DETAIL_KEY = "$$CUSTOM_LOG_DETAIL"; + + public static final String CUSTOM_LOG_APPEND_DETAIL_KEY = "$$CUSTOM_LOG_APPEND_DETAIL"; + + public static final String TEMPLATE_CONTEXT_KEY = "$$TEMPLATE_CONTEXT"; + + + public static void putVariable(String key, Object value) { + Map map = getVariables(); + map.put(key, value); + } + + public static Map getVariables() { + if (VARIABLE_MAP_STACK.get().empty()) { + //压入一个新的 + VARIABLE_MAP_STACK.get().push(new HashMap<>(16)); + } + return VARIABLE_MAP_STACK.get().peek(); + } + + /** + * 用户直接定义操作明细 + * + * @param detail detail + */ + public static void putLogDetail(String detail) { + putVariable(CUSTOM_LOG_DETAIL_KEY, detail); + } + + /** + * 用户在切面解析的基础上,在原内容后面附加内容 + * + * @param detailAppend detailAppend + */ + public static void putLogDetailAppend(String detailAppend) { + putVariable(CUSTOM_LOG_APPEND_DETAIL_KEY, detailAppend); + } + + /** + * 出栈释放 + */ + public static void clear() { + if (!VARIABLE_MAP_STACK.get().isEmpty()) { + VARIABLE_MAP_STACK.get().pop(); + } + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/OperatorGetService.java b/operation-log/src/main/java/com/water/ad/operation/log/core/OperatorGetService.java new file mode 100644 index 0000000..78f1e1d --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/OperatorGetService.java @@ -0,0 +1,31 @@ +package com.water.ad.operation.log.core; + +import lombok.*; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public interface OperatorGetService { + + + /** + * get operator user + * + * @return operator user + */ + Operator getUser(); + + + @Getter + @Setter + @Builder + @AllArgsConstructor + @NoArgsConstructor + class Operator { + + private String id; + + private String name; + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/diff/AbstractObjectDiffHandler.java b/operation-log/src/main/java/com/water/ad/operation/log/core/diff/AbstractObjectDiffHandler.java new file mode 100644 index 0000000..44d6884 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/diff/AbstractObjectDiffHandler.java @@ -0,0 +1,128 @@ +package com.water.ad.operation.log.core.diff; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Calendar; +import java.util.Date; + +/** + * @author yyq + * @create 2022-02-23 + **/ +@Slf4j +public abstract class AbstractObjectDiffHandler implements ObjectDiffHandler { + + protected Object formatDateField(Class modelClass, String dateFormat, Object value, Field field) { + if (StringUtils.isEmpty(dateFormat)) { + return value; + } + if (value == null) { + return value; + } + DateFormat sdf; + if (value instanceof Date) { + try { + sdf = new SimpleDateFormat(dateFormat); + value = sdf.format((Date) value); + } catch (Exception e) { + log.error("error format date {} with given dateFormat: {} in class: {}, field: {}", value, dateFormat, modelClass, field.getName(), e); + } + } + if (value instanceof Calendar) { + try { + sdf = new SimpleDateFormat(dateFormat); + value = sdf.format(((Calendar) value).getTime()); + } catch (Exception e) { + log.error("error format Calendar {} with given dateFormat: {} in class: {}, field: {}", value, dateFormat, modelClass, field.getName(), e); + + } + } + if (value instanceof LocalDate) { + try { + LocalDate tempLocalDate = (LocalDate) value; + DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern(dateFormat); + value = formatter1.format(tempLocalDate); + } catch (Exception e) { + log.error("error format LocalDate {} with given dateFormat: {} in class: {}, field: {}", value, dateFormat, modelClass, field.getName(), e); + } + } + if (value instanceof LocalTime) { + try { + LocalTime tempLocalTime = (LocalTime) value; + DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern(dateFormat); + value = formatter1.format(tempLocalTime); + } catch (Exception e) { + log.error("error format LocalTime {} with given dateFormat: {} in class: {}, field: {}", value, dateFormat, modelClass, field.getName(), e); + } + } + if (value instanceof LocalDateTime) { + try { + LocalDateTime tempLocalDateTime = (LocalDateTime) value; + DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern(dateFormat); + value = formatter1.format(tempLocalDateTime); + } catch (Exception e) { + log.error("error format LocalDateTime {} with given dateFormat: {} in class: {}, field: {}", value, dateFormat, modelClass, field.getName(), e); + } + } + return value; + } + + protected JSONObject parseFieldMapping(Class modelClass, String fieldMapping, Field field) { + JSONObject fieldMap; + try { + if (!StringUtils.isEmpty(fieldMapping)) { + fieldMap = JSONObject.parseObject(fieldMapping); + } else { + fieldMap = new JSONObject(); + } + } catch (Exception e) { + log.error("fieldMapping found wrong json format: {} in class: {}, field: {}", fieldMapping, modelClass, field.getName(), e); + fieldMap = new JSONObject(); + } + return fieldMap; + } + + protected Object doFieldMapping(JSONObject fieldMap, Object value) { + if (fieldMap != null && fieldMap.size() > 0) { + Object mapVal = fieldMap.get(value); + if (mapVal != null) { + value = mapVal; + } + } + return value; + } + + protected Object formatDecimal(Class modelClass, String decimalFormat, Object value, Field field) { + if (!StringUtils.isEmpty(decimalFormat)) { + if (value instanceof BigDecimal || value instanceof Double || + value instanceof Float || value instanceof Long || + value instanceof Integer) { + try { + DecimalFormat df = new DecimalFormat(decimalFormat); + value = df.format(value); + } catch (Exception e) { + log.error("error format digit with given decimalFormat:{} in class: {}, field: {}", decimalFormat, modelClass, field.getName(), e); + } + } + } + return value; + } + + protected boolean objectEquals(Object valuePre, Object valuePost) { + if (valuePre instanceof BigDecimal && valuePost instanceof BigDecimal) { + return ((BigDecimal) valuePre).stripTrailingZeros().equals(((BigDecimal) valuePost).stripTrailingZeros()); + } + return valuePre.equals(valuePost); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/diff/DefaultObjectDiffHandler.java b/operation-log/src/main/java/com/water/ad/operation/log/core/diff/DefaultObjectDiffHandler.java new file mode 100644 index 0000000..fbf9557 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/diff/DefaultObjectDiffHandler.java @@ -0,0 +1,130 @@ +package com.water.ad.operation.log.core.diff; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.water.ad.operation.log.annotation.OpLogField; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.springframework.expression.EvaluationException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author yyq + * @create 2022-02-23 + **/ +@Slf4j +public class DefaultObjectDiffHandler extends AbstractObjectDiffHandler { + + /** + * 4 space + */ + private final static String INDENT_APPENDER = " "; + + @Override + public String diff(Object pre, Object post, List specificFieldList) { + + Assert.notNull(pre, "diff pre Object can not be null"); + Assert.notNull(post, "diff post Object can not be null"); + + if (pre.getClass() != post.getClass()) { + throw new EvaluationException(String.format("diff object must be the same type pre [%s] , post [%s]", pre.getClass(), post.getClass())); + } + String indent = ""; + StringBuilder sb = new StringBuilder(); + OpLogField opLogFieldAnnotation; + Field[] fields; + fields = pre.getClass().getDeclaredFields(); + List fieldList; + if (CollectionUtils.isNotEmpty(specificFieldList)) { + fieldList = new ArrayList(); + for (Field field : fields) { + if (specificFieldList.contains(field.getName())) { + fieldList.add(field); + } + } + if (CollectionUtils.isEmpty(specificFieldList)) { + throw new EvaluationException("specified field list not found when diff " + JSON.toJSONString(specificFieldList)); + } + } else { + fieldList = Arrays.asList(fields); + } + Class modelClass = pre.getClass(); + String fieldName; + String fieldMapping; + String dateFormat; + String decimalFormat; + JSONObject fieldMap; + Object valuePre; + Object valuePost; + indent = indent + INDENT_APPENDER; + try { + for (Field field : fieldList) { + boolean hasAnnotation = field.isAnnotationPresent(OpLogField.class); + if (!hasAnnotation) { + log.warn("field without OpLogField annotation,skip diff field name {}", field.getName()); + continue; + } + opLogFieldAnnotation = field.getAnnotation(OpLogField.class); + boolean ignore = opLogFieldAnnotation.ignore(); + if (ignore) { + //skip the ignore fields + continue; + } + fieldName = opLogFieldAnnotation.fieldName(); + if (StringUtils.isEmpty(fieldName)) { + fieldName = field.getName(); + } + + dateFormat = opLogFieldAnnotation.dateFormat(); + decimalFormat = opLogFieldAnnotation.decimalFormat(); + fieldMapping = opLogFieldAnnotation.fieldMapping(); + + valuePre = FieldUtils.readDeclaredField(pre, field.getName(), true); + valuePost = FieldUtils.readDeclaredField(post, field.getName(), true); + + if (valuePre == null && valuePost == null) { + continue; + } + //OpLogModel should have it's own equals method. + if (valuePre != null && valuePost != null) { + if (objectEquals(valuePre, valuePost)) { + continue; + } + } + valuePre = formatDateField(modelClass, dateFormat, valuePre, field); + valuePost = formatDateField(modelClass, dateFormat, valuePost, field); + + valuePre = formatDecimal(modelClass, decimalFormat, valuePre, field); + valuePost = formatDecimal(modelClass, decimalFormat, valuePost, field); + + fieldMap = parseFieldMapping(modelClass, fieldMapping, field); + + valuePre = doFieldMapping(fieldMap, valuePre); + valuePost = doFieldMapping(fieldMap, valuePost); + + Class subModelClz = null; + if (valuePre != null) { + subModelClz = valuePre.getClass(); + } + if (valuePost != null) { + subModelClz = valuePost.getClass(); + } + sb.append(fieldName).append(": ") + .append(valuePre).append(" --> ").append(valuePost).append("\n"); + } + } catch (Exception e) { + throw new EvaluationException("diff Object error " + e.getMessage(), e); + } + if (sb.length() > 1 && sb.toString().endsWith("\n")) { + return sb.substring(0, sb.length() - 1); + } + return sb.toString(); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/diff/ObjectDiffHandler.java b/operation-log/src/main/java/com/water/ad/operation/log/core/diff/ObjectDiffHandler.java new file mode 100644 index 0000000..1191726 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/diff/ObjectDiffHandler.java @@ -0,0 +1,20 @@ +package com.water.ad.operation.log.core.diff; + +import java.util.List; + +/** + * @author yyq + * @create 2022-02-23 + **/ +public interface ObjectDiffHandler { + + /** + * 对象diff + * + * @param pre pre object + * @param post post Object + * @param specificFieldList 只对比特定的field + * @return diff差异结果 + */ + String diff(Object pre, Object post, List specificFieldList); +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/expression/BaseLogRecordExpression.java b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/BaseLogRecordExpression.java new file mode 100644 index 0000000..dc665cf --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/BaseLogRecordExpression.java @@ -0,0 +1,162 @@ +package com.water.ad.operation.log.core.expression; + + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.*; +import org.springframework.expression.common.ExpressionUtils; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * @author yyq + * @create 2022-03-09 + **/ +public abstract class BaseLogRecordExpression implements Expression { + + static SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + + + String expressionString; + + public BaseLogRecordExpression(String expressionString) { + Assert.notNull(expressionString, "expressionString is null"); + this.expressionString = expressionString; + } + + /** + * 目标方法前置执行 + * + * @return expression array execute before target method + */ + abstract Expression[] beforeExecute(); + + + @Override + public String getExpressionString() { + return expressionString; + } + + /** + * get expression value + * + * @param context context + * @return value + * @throws EvaluationException + */ + @Override + public abstract Object getValue(EvaluationContext context) throws EvaluationException; + + @Override + public T getValue(EvaluationContext context, Class expectedResultType) + throws EvaluationException { + Object value = getValue(context); + return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); + } + + @Override + public Object getValue() throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValue() on a BaseEmptyExpression "); + } + + @Override + public Class getValueType(EvaluationContext context) { + Assert.notNull(context, "EvaluationContext is required"); + TypeDescriptor typeDescriptor = new TypedValue(getValue(context)).getTypeDescriptor(); + return (typeDescriptor != null ? typeDescriptor.getType() : null); + } + + + @Override + public T getValue(Class expectedResultType) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValue(Class expectedResultType) on a BaseEmptyExpression"); + } + + @Override + public Object getValue(Object rootObject) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValue(Object rootObject) on a BaseEmptyExpression"); + + } + + @Override + public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValue(Object rootObject, Class desiredResultType) on a BaseEmptyExpression"); + + } + + @Override + public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValue(EvaluationContext context, Object rootObject) on a BaseEmptyExpression"); + } + + @Override + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) + throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValue(EvaluationContext context, Object rootObject, Class desiredResultType) on a BaseEmptyExpression"); + } + + @Override + public Class getValueType() { + throw new EvaluationException(this.expressionString, "Cannot call getValueType() on a BaseEmptyExpression"); + } + + @Override + public Class getValueType(Object rootObject) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValueType(Object rootObject) on a BaseEmptyExpression"); + } + + @Override + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValueType(EvaluationContext context, Object rootObject) on a BaseEmptyExpression"); + } + + @Override + public TypeDescriptor getValueTypeDescriptor() { + throw new EvaluationException(this.expressionString, "Cannot call getValueTypeDescriptor() on a BaseEmptyExpression"); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValueTypeDescriptor(Object rootObject) on a BaseEmptyExpression"); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) { + throw new EvaluationException(this.expressionString, "Cannot call getValueTypeDescriptor(EvaluationContext context) on a BaseEmptyExpression"); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) + throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call getValueTypeDescriptor(EvaluationContext context, Object rootObject) on a BaseEmptyExpression"); + } + + @Override + public boolean isWritable(Object rootObject) throws EvaluationException { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context) { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + return false; + } + + @Override + public void setValue(Object rootObject, Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a BaseEmptyExpression"); + } + + @Override + public void setValue(EvaluationContext context, Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a BaseEmptyExpression"); + } + + @Override + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a BaseEmptyExpression"); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/expression/CustomMethodExpression.java b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/CustomMethodExpression.java new file mode 100644 index 0000000..8cfff9b --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/CustomMethodExpression.java @@ -0,0 +1,138 @@ +package com.water.ad.operation.log.core.expression; + +import com.water.ad.operation.log.core.function.FunctionService; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.expression.*; +import org.springframework.expression.common.CompositeStringExpression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.common.TemplateAwareExpressionParser; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.util.Assert; + +/** + * 自定义方法表达式 + *

+ * NOTE:不支持方法没有参数的情况 + *

+ * { method(#p1) } 单参数 + *

+ * { method(#p1,#p2) } 多参数 + * + * @author yyq + * @create 2022-03-09 + **/ +public class CustomMethodExpression extends BaseLogRecordExpression { + + private static final String BEFORE_EXECUTE_PREFIX = "$$before."; + private static final MethodArgsExpressionParser METHOD_ARGS_EXPRESSION_PARSER = new MethodArgsExpressionParser(); + + /** + * 目标方法之前执行 + */ + private boolean beforeExecute; + + private String fullNameMethod; + + /** + * 涉及到前置执行,这里需要保存执行结果 + */ + private Object value; + + /** + * 方法参数(spel表达式,这里无效再拆分) + */ + private Expression[] argsExpressions; + + private FunctionService functionService; + + public CustomMethodExpression(String expressionString, FunctionService functionService) { + super(expressionString); + Assert.notNull(functionService, "function service is null"); + this.functionService = functionService; + //解析当前表达式 + parse(); + } + + @Override + Expression[] beforeExecute() { + if (beforeExecute) { + return new Expression[]{this}; + } + return null; + } + + @Override + public Object getValue(EvaluationContext context) throws EvaluationException { + if (value != null) { + return value; + } + Object[] args = getArgsValue(context); + value = functionService.apply(context.getRootObject().getValue(), fullNameMethod, args); + return value; + } + + private void parse() { + //调用目标方法前执行 + if (expressionString.startsWith(BEFORE_EXECUTE_PREFIX)) { + beforeExecute = true; + } + //这里使用自定义的解析器,默认的遇到method(#p1,#p2多参数会报错 + Expression expression = METHOD_ARGS_EXPRESSION_PARSER.parseExpression(expressionString, new TemplateParserContext("(", ")")); + if (!(expression instanceof CompositeStringExpression)) { + throw new ExpressionException(String.format("expressionString [{%s}] incongruity for CustomMethod expression", expression)); + } + CompositeStringExpression compositeStringExpression = (CompositeStringExpression) expression; + Expression[] expressions = compositeStringExpression.getExpressions(); + if (expressions.length != 2) { + throw new ExpressionException(String.format("expressionString [{%s}] incongruity for CustomMethod expression", expression)); + } + //方法全名 + fullNameMethod = expressions[0].getExpressionString().replace(BEFORE_EXECUTE_PREFIX, ""); + //参数 + String argsExpression = expressions[1].getExpressionString(); + String[] argsExpressionString = argsExpression.split(","); + argsExpressions = new Expression[argsExpressionString.length]; + for (int i = 0; i < argsExpressionString.length; i++) { + argsExpressions[i] = spelExpressionParser.parseExpression(argsExpressionString[i]); + } + } + + + /** + * 方法参数解析 + * + * @param context + * @return + */ + private Object[] getArgsValue(EvaluationContext context) { + if (ArrayUtils.isEmpty(argsExpressions)) { + return null; + } + Object[] args = new Object[argsExpressions.length]; + for (int i = 0; i < argsExpressions.length; i++) { + Expression expression = argsExpressions[i]; + Object arg = argsExpressions[i].getValue(context); + if (arg == null) { + throw new EvaluationException(expression.getExpressionString(), String.format("Cannot call get value for [%s] expression", + expression.getExpressionString())); + } + args[i] = arg; + } + return args; + } + + public boolean isBeforeExecute() { + return beforeExecute; + } + + + /** + * 方法参数解析器 + */ + static class MethodArgsExpressionParser extends TemplateAwareExpressionParser { + @Override + protected Expression doParseExpression(String expressionString, ParserContext context) throws ParseException { + return new LiteralExpression(expressionString); + } + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/expression/DiffExpression.java b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/DiffExpression.java new file mode 100644 index 0000000..8b848c5 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/DiffExpression.java @@ -0,0 +1,100 @@ +package com.water.ad.operation.log.core.expression; + +import com.water.ad.operation.log.core.function.FunctionService; +import com.water.ad.operation.log.core.diff.ObjectDiffHandler; +import com.water.ad.operation.log.util.TemplateUtil; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.expression.common.CompositeStringExpression; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.List; + +/** + * 对象diff表达式 + *

+ * diff( {methodA(#p1)},{methodB(#p2)} ) preObject引用方法,postObject引用方法 + *

+ * diff( {methodA(#p1)},{#objB} ) preObject引用方法,postObject引用参数对象 + *

+ * diff( {#objA},{#methodB(#p2)} ) preObject引用参数对象,postObject引用方法 + *

+ * diff( {#objA},{#objB} ) preObject引用参数对象,postObject引用参数对象 + * + * @author yyq + * @create 2022-03-09 + **/ +public class DiffExpression extends BaseLogRecordExpression { + + private Expression preExpression; + private Expression postExpression; + + private ObjectDiffHandler objectDiffHandler; + private FunctionService functionService; + + + public DiffExpression(String expressionString, ObjectDiffHandler objectDiffHandler, FunctionService functionService) { + super(expressionString); + Assert.notNull(objectDiffHandler, "objectDiffHandler is null"); + Assert.notNull(functionService, "functionService is null"); + this.expressionString = expressionString; + this.objectDiffHandler = objectDiffHandler; + this.functionService = functionService; + + parse(); + } + + @Override + Expression[] beforeExecute() { + List expressions = new ArrayList<>(); + if (preExpression instanceof CustomMethodExpression) { + expressions.add(preExpression); + } + if (postExpression instanceof CustomMethodExpression) { + expressions.add(postExpression); + } + if (expressions.isEmpty()) { + return null; + } + return expressions.toArray(new Expression[0]); + } + + private void parse() { + Expression expression = spelExpressionParser.parseExpression(expressionString, + new TemplateParserContext("{", "}")); + if (!(expression instanceof CompositeStringExpression)) { + throw new ExpressionException(String.format("expressionString [diff(%s)] incongruity for diff expression", expressionString)); + } + CompositeStringExpression compositeStringExpression = (CompositeStringExpression) expression; + Expression[] expressions = compositeStringExpression.getExpressions(); + if (expressions.length != 3) { + throw new ExpressionException(String.format("expressionString [diff(%s)] incongruity for diff expression", expressionString)); + } + //中间的逗号,跳过 + preExpression = transferExpression(expressions[0]); + postExpression = transferExpression(expressions[2]); + + } + + private Expression transferExpression(Expression expression) { + if (TemplateUtil.isSourceSpelExpression(expression.getExpressionString())) { + //引用对象 + return expression; + } else { + //引用方法 + return new CustomMethodExpression(expression.getExpressionString(), functionService); + } + } + + + @Override + public String getValue(EvaluationContext context) throws EvaluationException { + Object pre = preExpression.getValue(context); + Object post = postExpression.getValue(context); + return objectDiffHandler.diff(pre, post, null); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/expression/LogRecordExpression.java b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/LogRecordExpression.java new file mode 100644 index 0000000..bc1669c --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/expression/LogRecordExpression.java @@ -0,0 +1,57 @@ +package com.water.ad.operation.log.core.expression; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.common.CompositeStringExpression; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 操作日志表达式 + * + * @author yyq + * @create 2022-03-09 + **/ +public class LogRecordExpression extends BaseLogRecordExpression { + + + private Expression expression; + + public LogRecordExpression(String expressionString, Expression expression) { + super(expressionString); + Assert.notNull(expression, "expression is null"); + this.expression = expression; + } + + @Override + public Object getValue(EvaluationContext context) throws EvaluationException { + return expression.getValue(context); + } + + @Override + public Expression[] beforeExecute() { + List beforeExpression = new ArrayList<>(); + if (expression instanceof CompositeStringExpression) { + Expression[] expressions = ((CompositeStringExpression) expression).getExpressions(); + for (Expression exp : expressions) { + if (exp instanceof BaseLogRecordExpression) { + Expression[] bex = ((BaseLogRecordExpression) exp).beforeExecute(); + if (bex != null) { + beforeExpression.addAll(Arrays.asList(bex)); + } + } + } + } + if (expression instanceof BaseLogRecordExpression) { + Expression[] bex = ((BaseLogRecordExpression) expression).beforeExecute(); + if (bex != null) { + beforeExpression.addAll(Arrays.asList(bex)); + } + } + return beforeExpression.toArray(new Expression[]{}); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/DefaultFunctionServiceImpl.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/DefaultFunctionServiceImpl.java new file mode 100644 index 0000000..0e706c8 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/DefaultFunctionServiceImpl.java @@ -0,0 +1,32 @@ +package com.water.ad.operation.log.core.function; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public class DefaultFunctionServiceImpl implements FunctionService { + + private final ParseFunctionFactory parseFunctionFactory; + + public DefaultFunctionServiceImpl(ParseFunctionFactory parseFunctionFactory) { + this.parseFunctionFactory = parseFunctionFactory; + } + + /** + * 自定义函数执行 + * + * @param functionName 自定义函数名称 + * @param args method args + * @return method execute result + */ + @Override + public Object apply(Object targetObject, String functionName, Object[] args) { + + ParseFunction function = parseFunctionFactory.getFunction(functionName); + if (function == null) { + //这里原值返回还是null返回好呢 + return null; + } + return function.apply(args); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/FunctionService.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/FunctionService.java new file mode 100644 index 0000000..042ffbd --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/FunctionService.java @@ -0,0 +1,19 @@ +package com.water.ad.operation.log.core.function; + +/** + * 自定义方法执行接口 + * + * @author yyq + * @create 2022-02-17 + **/ +public interface FunctionService { + + + /** + * @param targetObject 目标方法所属对象 + * @param functionName 自定义函数名称 + * @param args 参数 + * @return function execute result + */ + Object apply(Object targetObject, String functionName, Object[] args); +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/MethodResolver.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/MethodResolver.java new file mode 100644 index 0000000..d4a4cba --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/MethodResolver.java @@ -0,0 +1,29 @@ +package com.water.ad.operation.log.core.function; + +import com.alibaba.fastjson.JSON; +import org.springframework.expression.EvaluationException; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; + +/** + * @author yyq + * @create 2022-03-12 + **/ +public class MethodResolver { + public Method resolve(Object targetObject, String functionName, Object[] args) { + Class targetClass = targetObject.getClass(); + Method method; + Class[] paramsType; + paramsType = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + paramsType[i] = args[i].getClass(); + } + method = ReflectionUtils.findMethod(targetClass, functionName, paramsType); + if (method == null) { + throw new EvaluationException(String.format("not found method [%s] param %s for class [%s]", functionName, JSON.toJSONString(paramsType), targetObject.getClass().getName())); + } + return method; + } + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunction.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunction.java new file mode 100644 index 0000000..eff2889 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunction.java @@ -0,0 +1,23 @@ +package com.water.ad.operation.log.core.function; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public interface ParseFunction { + + /** + * 方法名称 + * + * @return + */ + String functionName(); + + /** + * 方法执行 + * + * @param args + * @return + */ + Object apply(Object[] args); +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunctionFactory.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunctionFactory.java new file mode 100644 index 0000000..12751bd --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/ParseFunctionFactory.java @@ -0,0 +1,34 @@ +package com.water.ad.operation.log.core.function; + + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public class ParseFunctionFactory { + + private Map allFunctionMap = new HashMap<>(16); + + public ParseFunctionFactory(List parseFunctions) { + if (CollectionUtils.isEmpty(parseFunctions)) { + return; + } + for (ParseFunction parseFunction : parseFunctions) { + if (StringUtils.isEmpty(parseFunction.functionName())) { + continue; + } + allFunctionMap.put(parseFunction.functionName(), parseFunction); + } + } + + public ParseFunction getFunction(String functionName) { + return allFunctionMap.get(functionName); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/PrioritizedFunctionServiceImpl.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/PrioritizedFunctionServiceImpl.java new file mode 100644 index 0000000..8865cb4 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/PrioritizedFunctionServiceImpl.java @@ -0,0 +1,32 @@ +package com.water.ad.operation.log.core.function; + +import java.util.List; + +/** + * 按照加入 {@link PrioritizedFunctionServiceImpl#functionServices} 集合中的顺序作为优先级执行 + * + * @author yyq + * @create 2022-02-21 + **/ +public class PrioritizedFunctionServiceImpl implements FunctionService { + + private List functionServices; + + public PrioritizedFunctionServiceImpl(List functionServices) { + if (functionServices == null) { + throw new IllegalArgumentException("functionServices can not be null"); + } + this.functionServices = functionServices; + } + + @Override + public Object apply(Object targetObject, String functionName, Object[] args) { + for (FunctionService functionService : functionServices) { + Object value = functionService.apply(targetObject, functionName, args); + if (value != null) { + return value; + } + } + return null; + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/SpringContextBeanFunctionServiceImpl.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/SpringContextBeanFunctionServiceImpl.java new file mode 100644 index 0000000..822ad3a --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/SpringContextBeanFunctionServiceImpl.java @@ -0,0 +1,59 @@ +package com.water.ad.operation.log.core.function; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.expression.EvaluationException; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; + +/** + * 基于spring容器中bean的方法调用 + * + * @author yyq + * @create 2022-02-18 + **/ +@Slf4j +public class SpringContextBeanFunctionServiceImpl extends MethodResolver implements FunctionService { + + private BeanFactory beanFactory; + + private static final String FULL_NAME_SPIL = "."; + + public SpringContextBeanFunctionServiceImpl(BeanFactory beanFactory) { + if (beanFactory == null) { + throw new IllegalArgumentException("beanFactory can not be null"); + } + this.beanFactory = beanFactory; + } + + + /** + * 这里的自定义名称代表方法全名称,如fm.lizhi.ad.operation.log.handler.function.SpringContextFunctionServiceImpl.apply + * + * @param functionName 自定义函数名称 + * @param args 参数 + * @return + */ + @Override + public Object apply(Object targetObject, String functionName, Object[] args) { + if (!functionName.contains(FULL_NAME_SPIL)) { + return null; + } + int indexOf = functionName.lastIndexOf(FULL_NAME_SPIL); + String classFullName = functionName.substring(0, indexOf); + String methodName = functionName.substring(indexOf + 1); + Object object; + Class targetClass; + try { + targetClass = Class.forName(classFullName); + } catch (ClassNotFoundException e) { + String errorMsg = String.format("not found class [%s]", classFullName); + log.error(errorMsg, e); + throw new EvaluationException(errorMsg, e); + } + object = beanFactory.getBean(targetClass); + Method method = resolve(object, methodName, args); + return ReflectionUtils.invokeMethod(method, object, args); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/function/TargetObjectFunctionServiceImpl.java b/operation-log/src/main/java/com/water/ad/operation/log/core/function/TargetObjectFunctionServiceImpl.java new file mode 100644 index 0000000..2f48dd1 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/function/TargetObjectFunctionServiceImpl.java @@ -0,0 +1,34 @@ +package com.water.ad.operation.log.core.function; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; + +/** + * 基于切面代理的目标对象所属方法调用 + * + * @author yyq + * @create 2022-02-18 + **/ +@Slf4j +public class TargetObjectFunctionServiceImpl extends MethodResolver implements FunctionService { + + private static final String FULL_NAME_SPIL = "."; + + /** + * @param targetObject 目标方法所属对象 + * @param functionName 自定义函数名称 + * @param args 参数 + * @return + */ + @Override + public Object apply(Object targetObject, String functionName, Object[] args) { + if (functionName.contains(FULL_NAME_SPIL)) { + return null; + } + Method method = resolve(targetObject, functionName, args); + return ReflectionUtils.invokeMethod(method, targetObject, args); + + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/parse/CachedExpressionEvaluator.java b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/CachedExpressionEvaluator.java new file mode 100644 index 0000000..b5ae621 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/CachedExpressionEvaluator.java @@ -0,0 +1,122 @@ +package com.water.ad.operation.log.core.parse; + +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.Map; + +/** + * copy from {@link org.springframework.context.expression.CachedExpressionEvaluator} + *

+ * 修改 getParser的返回类型 + * + * @author yyq + * @create 2022-03-10 + **/ +public class CachedExpressionEvaluator { + private final ExpressionParser parser; + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + /** + * Create a new instance with the specified {@link ExpressionParser}. + */ + protected CachedExpressionEvaluator(ExpressionParser parser) { + Assert.notNull(parser, "LogRecordExpressionParser must not be null"); + this.parser = parser; + } + + /** + * + */ + protected ExpressionParser getParser() { + return this.parser; + } + + /** + * Return a shared parameter name discoverer which caches data internally. + * + * @since 4.3 + */ + protected ParameterNameDiscoverer getParameterNameDiscoverer() { + return this.parameterNameDiscoverer; + } + + + /** + * Return the {@link Expression} for the specified SpEL value + *

Parse the expression if it hasn't been already. + * + * @param cache the cache to use + * @param elementKey the element on which the expression is defined + * @param expression the expression to parse + */ + protected Expression getExpression(Map cache, + AnnotatedElementKey elementKey, String expression) { + + ExpressionKey expressionKey = createKey(elementKey, expression); + Expression expr = cache.get(expressionKey); + if (expr == null) { + expr = getParser().parseExpression(expression); + cache.put(expressionKey, expr); + } + return expr; + } + + private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) { + return new ExpressionKey(elementKey, expression); + } + + + protected static class ExpressionKey implements Comparable { + + private final AnnotatedElementKey element; + + private final String expression; + + protected ExpressionKey(AnnotatedElementKey element, String expression) { + Assert.notNull(element, "AnnotatedElementKey must not be null"); + Assert.notNull(expression, "Expression must not be null"); + this.element = element; + this.expression = expression; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ExpressionKey)) { + return false; + } + ExpressionKey otherKey = (ExpressionKey) other; + return (this.element.equals(otherKey.element) && + ObjectUtils.nullSafeEquals(this.expression, otherKey.expression)); + } + + @Override + public int hashCode() { + return this.element.hashCode() * 29 + this.expression.hashCode(); + } + + @Override + public String toString() { + return this.element + " with expression \"" + this.expression + "\""; + } + + @Override + public int compareTo(ExpressionKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0) { + result = this.expression.compareTo(other.expression); + } + return result; + } + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordEvaluationContext.java b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordEvaluationContext.java new file mode 100644 index 0000000..710feff --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordEvaluationContext.java @@ -0,0 +1,28 @@ +package com.water.ad.operation.log.core.parse; + +import com.water.ad.operation.log.core.LogRecordContext; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.util.CollectionUtils; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public class LogRecordEvaluationContext extends MethodBasedEvaluationContext { + + public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments, ParameterNameDiscoverer parameterNameDiscoverer) { + super(rootObject, method, arguments, parameterNameDiscoverer); + + //把 LogRecordContext 中的变量都放到 RootObject 中 + Map variables = LogRecordContext.getVariables(); + if (!CollectionUtils.isEmpty(variables)) { + for (Map.Entry entry : variables.entrySet()) { + setVariable(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionEvaluator.java b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionEvaluator.java new file mode 100644 index 0000000..6b40b63 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionEvaluator.java @@ -0,0 +1,43 @@ +package com.water.ad.operation.log.core.parse; + +import com.water.ad.operation.log.core.expression.LogRecordExpression; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 表达式执行器 + * + * @author yyq + * @create 2022-02-17 + **/ +public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator { + + private Map expressionCache = new ConcurrentHashMap<>(64); + + /** + * Create a new instance with the specified {@link ExpressionParser}. + * + * @param parser + */ + public LogRecordExpressionEvaluator(ExpressionParser parser) { + super(parser); + } + + /** + * expression 解析 + */ + public LogRecordExpression parseExpression(String conditionExpression, AnnotatedElementKey methodKey) { + return (LogRecordExpression) getExpression(this.expressionCache, methodKey, conditionExpression); + } + + public EvaluationContext createEvaluationContext(Method method, Object[] args, Object targetObject) { + return new LogRecordEvaluationContext(targetObject, method, args, this.getParameterNameDiscoverer()); + } + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionParser.java b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionParser.java new file mode 100644 index 0000000..6b7edb6 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/parse/LogRecordExpressionParser.java @@ -0,0 +1,124 @@ +package com.water.ad.operation.log.core.parse; + +import com.water.ad.operation.log.core.expression.CustomMethodExpression; +import com.water.ad.operation.log.core.expression.DiffExpression; +import com.water.ad.operation.log.core.expression.LogRecordExpression; +import com.water.ad.operation.log.core.function.FunctionService; +import com.water.ad.operation.log.core.diff.ObjectDiffHandler; +import com.water.ad.operation.log.util.RegexpUtil; +import com.water.ad.operation.log.util.TemplateUtil; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.common.TemplateAwareExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * @author yyq + * @create 2022-03-09 + **/ +public class LogRecordExpressionParser extends TemplateAwareExpressionParser { + + private static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser(); + + /** + * diff模板解析 + */ + private static final ParserContext DIFF_PARSE_CONTEXT = new DiffParserContext(); + /** + * 普通模板解析 + */ + private static final ParserContext COMMON_PARSER_CONTEXT = new CommonParserContext(); + private static final String DIFF_TEMPLATE_REG = "diff\\(.+\\)"; + private static final String CUSTOM_METHOD_REG = "\\(.+\\)"; + private static final String COMMON_TEMPLATE_REG = "\\{.+\\}"; + + private ObjectDiffHandler objectDiffHandler; + private FunctionService functionService; + + public LogRecordExpressionParser(FunctionService functionService, ObjectDiffHandler objectDiffHandler) { + Assert.notNull(objectDiffHandler, "objectDiffHandler is null"); + Assert.notNull(objectDiffHandler, "functionService is null"); + this.objectDiffHandler = objectDiffHandler; + this.functionService = functionService; + } + + /** + * @param expressionString 表达式 + * @return TemplateExpression + * @throws ParseException + */ + @Override + public LogRecordExpression parseExpression(String expressionString) throws ParseException { + + Expression expression; + //diff expression contain diff() + if (RegexpUtil.hasStr(expressionString, DIFF_TEMPLATE_REG)) { + expression = super.parseExpression(expressionString, DIFF_PARSE_CONTEXT); + } else if (RegexpUtil.hasStr(expressionString, COMMON_TEMPLATE_REG)) { + //common expression,contain {} + expression = super.parseExpression(expressionString, COMMON_PARSER_CONTEXT); + } else if (TemplateUtil.isSourceSpelExpression(expressionString)) { + //spel表达式 + expression = super.parseExpression(expressionString); + } else { + //常量 + expression = new LiteralExpression(expressionString); + } + return new LogRecordExpression(expressionString, expression); + } + + @Override + protected Expression doParseExpression(String expressionString, ParserContext context) throws ParseException { + Expression expression; + if (context instanceof DiffParserContext) { + // diff expression + expression = new DiffExpression(expressionString, objectDiffHandler, functionService); + } else if (RegexpUtil.hasStr(expressionString, CUSTOM_METHOD_REG)) { + //method expression + expression = new CustomMethodExpression(expressionString, functionService); + } else { + //spel expression + expression = SPEL_EXPRESSION_PARSER.parseExpression(expressionString); + } + return expression; + } + + + public static class DiffParserContext implements ParserContext { + @Override + public boolean isTemplate() { + return true; + } + + @Override + public String getExpressionPrefix() { + return "diff("; + } + + @Override + public String getExpressionSuffix() { + return ")"; + } + } + + public static class CommonParserContext implements ParserContext { + @Override + public boolean isTemplate() { + return true; + } + + @Override + public String getExpressionPrefix() { + return "{"; + } + + @Override + public String getExpressionSuffix() { + return "}"; + } + } + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/record/DefaultLogRecordServiceImpl.java b/operation-log/src/main/java/com/water/ad/operation/log/core/record/DefaultLogRecordServiceImpl.java new file mode 100644 index 0000000..699f932 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/record/DefaultLogRecordServiceImpl.java @@ -0,0 +1,18 @@ +package com.water.ad.operation.log.core.record; + +import com.water.ad.operation.log.model.LogRecord; +import lombok.extern.slf4j.Slf4j; + +/** + * @author yyq + * @create 2022-02-17 + **/ +@Slf4j +public class DefaultLogRecordServiceImpl implements LogRecordService { + + @Override + public void record(LogRecord logRecord) { + + log.info("【logRecord】log={}", logRecord); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/core/record/LogRecordService.java b/operation-log/src/main/java/com/water/ad/operation/log/core/record/LogRecordService.java new file mode 100644 index 0000000..4e88e03 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/core/record/LogRecordService.java @@ -0,0 +1,17 @@ +package com.water.ad.operation.log.core.record; + +import com.water.ad.operation.log.model.LogRecord; + +/** + * @author yyq + * @create 2022-02-17 + **/ +public interface LogRecordService { + + /** + * 保存log + * + * @param logRecord 日志实体 + */ + void record(LogRecord logRecord); +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/model/CustomMethod.java b/operation-log/src/main/java/com/water/ad/operation/log/model/CustomMethod.java new file mode 100644 index 0000000..607ebe0 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/model/CustomMethod.java @@ -0,0 +1,50 @@ +package com.water.ad.operation.log.model; + +import lombok.*; + +import java.util.List; + +/** + * 自定义函数 + * + * @author yyq + * @create 2022-02-21 + **/ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CustomMethod { + + /** + * 方法名 + */ + private String name; + /** + * 方法参数 + */ + private Object[] args; + + /** + * 方法返回值 + */ + private Object value; + + /** + * 原始模板 {m1{#p1}} + */ + private String sourceTemplate; + + /** + * 原始的方法参数spel表达式(通过sourceTemplate解析出来的) + */ + private List argsSpelExpression; + + /** + * 是否是前置执行 + */ + private boolean beforeMethod; + + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/model/LogRecord.java b/operation-log/src/main/java/com/water/ad/operation/log/model/LogRecord.java new file mode 100644 index 0000000..ac2a710 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/model/LogRecord.java @@ -0,0 +1,32 @@ +package com.water.ad.operation.log.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * @author yyq + * @create 2022-02-17 + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class LogRecord { + + private Date time; + + private String operatorId; + + private String bizNo; + + private String detail; + + /** + * 类别 + */ + private String category; +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/model/StandaloneSpel.java b/operation-log/src/main/java/com/water/ad/operation/log/model/StandaloneSpel.java new file mode 100644 index 0000000..c4bdd9c --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/model/StandaloneSpel.java @@ -0,0 +1,22 @@ +package com.water.ad.operation.log.model; + +import lombok.*; + +/** + * 单独的表达式 + * + * @author yyq + * @create 2022-02-22 + **/ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class StandaloneSpel { + + + private String template; + + private Object value; +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/model/TemplateContext.java b/operation-log/src/main/java/com/water/ad/operation/log/model/TemplateContext.java new file mode 100644 index 0000000..a2abff9 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/model/TemplateContext.java @@ -0,0 +1,49 @@ +package com.water.ad.operation.log.model; + +import com.water.ad.operation.log.core.expression.LogRecordExpression; +import lombok.*; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; + + +/** + * 模板上下文 + * + * @author yyq + * @create 2022-02-21 + **/ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TemplateContext { + + private LogRecordExpression detailExpression; + /** + * 业务标识号,使用spel表达式 + */ + private Expression biNoExpression; + /** + * 操作人,使用spel表达式 + */ + private Expression operatorExpression; + + /** + * 记录条件,使用spel表达式 + */ + private Expression conditionExpression; + + /** + * 操作日志类别,常量字符串 + */ + private String category; + + private EvaluationContext evaluationContext; + + /** + * 切面拦截的目标对象 + */ + private Object targetObject; + +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/util/RegexpUtil.java b/operation-log/src/main/java/com/water/ad/operation/log/util/RegexpUtil.java new file mode 100644 index 0000000..bf36409 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/util/RegexpUtil.java @@ -0,0 +1,23 @@ +package com.water.ad.operation.log.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author yyq + */ +public class RegexpUtil { + + /** + * 判断给定字符串中是否含有制定字符串 + * + * @param src + * @param reg + * @return + */ + public static boolean hasStr(String src, String reg) { + Pattern p = Pattern.compile(reg); + Matcher m = p.matcher(src); + return m.find(); + } +} diff --git a/operation-log/src/main/java/com/water/ad/operation/log/util/TemplateUtil.java b/operation-log/src/main/java/com/water/ad/operation/log/util/TemplateUtil.java new file mode 100644 index 0000000..d68a8d5 --- /dev/null +++ b/operation-log/src/main/java/com/water/ad/operation/log/util/TemplateUtil.java @@ -0,0 +1,23 @@ +package com.water.ad.operation.log.util; + +import org.apache.commons.lang3.StringUtils; + +/** + * @author yyq + * @create 2022-03-12 + **/ +public class TemplateUtil { + + + /** + * 判断是否是原始的spel表达式 + * + * @param expression + * @return + */ + public static boolean isSourceSpelExpression(String expression) { + return StringUtils.isNotEmpty(expression) && + expression.startsWith("#") || + expression.startsWith("@"); + } +} diff --git a/operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTest.java b/operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTest.java new file mode 100644 index 0000000..1b11ef3 --- /dev/null +++ b/operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTest.java @@ -0,0 +1,382 @@ +package com.water.ad.operation.log.test; + +import com.water.ad.operation.log.annotation.LogRecordAnnotation; +import com.water.ad.operation.log.annotation.OpLogField; +import com.water.ad.operation.log.aop.LogRecordPointcut; +import com.water.ad.operation.log.core.LogRecordContext; +import com.water.ad.operation.log.core.record.LogRecordService; +import com.water.ad.operation.log.model.LogRecord; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * @author yyq + * @create 2022-02-17 + **/ +@SuppressWarnings("") +public class LogRecordPointcutTest { + + private ApplicationContext applicationContext; + private UserService userServiceProxy; + + @Before + public void before() { + applicationContext = new AnnotationConfigApplicationContext(LogRecordPointcutTestConfig.class); + UserService userService = applicationContext.getBean(UserService.class); + LogRecordPointcut logRecordPointcut = applicationContext.getBean(LogRecordPointcut.class); + Assert.assertNotNull(userService); + AspectJProxyFactory factory = new AspectJProxyFactory(userService); + factory.addAspect(logRecordPointcut); + userServiceProxy = factory.getProxy(); + } + + /** + * pre方法调用(单参数),post引用方法参数 + */ + @Test + public void testPreMethodSingleParamPostQuote() { + userServiceProxy.updateNameById(1, "yyq-plus"); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更新了用户名称,从yyq-old改为yyq-plus", logRecordService.record().getDetail()); + } + + + /** + * pre方法调用(多参数),post引用方法参数 + */ + @Test + public void testPreMethodMultiParamPostQuote() { + userServiceProxy.updateNameById(2L, 1, "yyq-plus"); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更新了用户名称,从yyq-old改为yyq-plus", logRecordService.record().getDetail()); + } + + /** + * pre引用方法参数,post方法调用(单参数) + */ + @Test + public void testPreQuotePostMethodSigleParam() { + userServiceProxy.updateNameByIdWithOldName(1, "yyq-old"); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更新了用户名称,从yyq-old改为yyq-plus", logRecordService.record().getDetail()); + } + + /** + * pre引用方法参数,post方法调用(多参数) + */ + @Test + public void testPreQuotePostMethodMultiParam() { + userServiceProxy.updateNameByIdWithOldName(2L, 1, "yyq-old"); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更新了用户名称,从yyq-old改为yyq-plus", logRecordService.record().getDetail()); + } + + + /** + * diff比较 + * pre方法调用(单参数),post引用方法参数 + */ + @Test + public void testDiffPreMethodSingleParamPostQuote() { + userServiceProxy.updateUser(User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29", logRecordService.record().getDetail()); + } + + /** + * diff比较 + * pre方法调用(多参数),post引用方法参数 + */ + @Test + public void testDiffPreMethodMultiParamPostQuote() { + userServiceProxy.updateUserWithCompanyId(2L, User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29", logRecordService.record().getDetail()); + } + + /** + * diff比较 + * pre引用方法参数,post方法调用(单参数) + */ + @Test + public void testDiffPreQuotePostMethodSingleParam() { + userServiceProxy.updateUserWithOldUser(User.builder().id(1).name("yyq-old").age(28).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29", logRecordService.record().getDetail()); + } + + /** + * diff比较 + * pre引用方法参数,post方法调用(单参数) + */ + @Test + public void testDiffPreQuotePostMethodMultiParam() { + userServiceProxy.updateUserWithOldUser(2L, User.builder().id(1).name("yyq-old").age(28).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29", logRecordService.record().getDetail()); + } + + /** + * diff比较 + * pre引用方法参数,post引用方法参数 + */ + @Test + public void testDiffPreQuotePostQuote() { + userServiceProxy.updateUser(User.builder().id(1).name("yyq-old").age(28).build(), User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29", logRecordService.record().getDetail()); + } + + + /** + * 用户直接指定操作内容 + */ + @Test + public void testCustomContent() { + userServiceProxy.updateUserWithCustomContent(User.builder().id(1).name("yyq-old").age(28).build(), User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("直接指定操作内容,更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29", logRecordService.record().getDetail()); + } + + /** + * 用户附加操作内容 + */ + @Test + public void testCustomAppendContent() { + userServiceProxy.updateUserWithOldUserAppend(User.builder().id(1).name("yyq-old").age(28).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("更改用户信息,名称: yyq-old --> yyq-plus\n" + + "年龄: 28 --> 29 我是附加的内容", logRecordService.record().getDetail()); + } + + /** + * condition false + */ + @Test + public void testConditionFalse() { + userServiceProxy.updateUserWithConditionFalse(User.builder().id(1).name("yyq-old").age(28).build(), User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNull(logRecordService.record()); + + } + + /** + * condition false + */ + @Test + public void testConditionFalseUserRet() { + User user = userServiceProxy.updateUserWithConditionFalseUseRet(User.builder().id(1).name("yyq-old").age(28).build(), User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNull(user); + Assert.assertNull(logRecordService.record()); + } + + /** + * condition false + */ + @Test + public void testConditionFalseTrueRet() { + User user = userServiceProxy.updateUserWithConditionTrueUseRet(User.builder().id(1).name("yyq-old").age(28).build(), User.builder().id(1).name("yyq-plus").age(29).build()); + TestLogRecordRecordService logRecordService = applicationContext.getBean(TestLogRecordRecordService.class); + Assert.assertNotNull(logRecordService); + Assert.assertNotNull(user); + Assert.assertNotNull(logRecordService.record()); + Assert.assertEquals("updateUserWithConditionTrueUseRet", logRecordService.record().getDetail()); + } + + + @Slf4j + public static class UserService { + + /** + * + * @param id 用户id + * @param newName 新用户名 + */ + @LogRecordAnnotation(detail = "更新了用户名称,从{$$before.getNameById(#id)}改为{#newName}", bizNo = "#id", category = "用户更改") + void updateNameById(Integer id, String newName) { + log.info("######### updateNameById id {} newName {} #####", id, newName); + } + + + @LogRecordAnnotation(detail = "更新了用户名称,从{$$before.getNameById(#departmentId,#id)}改为{#newName}", bizNo = "#id", category = "用户更改") + void updateNameById(Long departmentId, Integer id, String newName) { + log.info("######### updateNameById departmentId {},id {},newName {}#####", departmentId, id, newName); + } + + @LogRecordAnnotation(detail = "更新了用户名称,从{#oldName}改为{getNameByIdNew(#id)}", bizNo = "#id", category = "用户更改") + void updateNameByIdWithOldName(Integer id, String oldName) { + log.info("######### updateNameById id {},oldName {} #####", id, oldName); + } + + @LogRecordAnnotation(detail = "更新了用户名称,从{#oldName}改为{getNameByIdNew(#departmentId,#id)}", bizNo = "#id", category = "用户更改") + void updateNameByIdWithOldName(Long departmentId, Integer id, String oldName) { + log.info("######### updateNameById departmentId {},id {},oldName {} ##### ", departmentId, id, oldName); + } + + @LogRecordAnnotation(detail = "更改用户信息,diff({$$before.getUserById(#newUser.id)},{#newUser})", bizNo = "#newUser.id", category = "用户更改") + void updateUser(User newUser) { + log.info("######### update user #####"); + } + + @LogRecordAnnotation(detail = "更改用户信息,diff({#oldUser},{#newUser})", bizNo = "#oldUser.id", category = "用户更改") + void updateUser(User oldUser, User newUser) { + log.info("######### update user #####"); + } + + @LogRecordAnnotation(detail = "更改用户信息,diff({$$before.getUserById(#departmentId,#newUser.id)},{#newUser})", bizNo = "#newUser.id", category = "用户更改") + void updateUserWithCompanyId(Long departmentId, User newUser) { + log.info("######### update user #####"); + } + + @LogRecordAnnotation(detail = "更改用户信息,diff({#oldUser},{getUserByIdNew(#oldUser.id)})", bizNo = "#oldUser.id", category = "用户更改") + void updateUserWithOldUser(User oldUser) { + log.info("######### update user #####"); + } + + @LogRecordAnnotation(detail = "更改用户信息,diff({#oldUser},{$$before.getUserByIdNew(#departmentId,#oldUser.id)})", bizNo = "#oldUser.id", category = "用户更改") + void updateUserWithOldUser(Long departmentId, User oldUser) { + log.info("######### update user #####"); + } + + @LogRecordAnnotation(detail = "更改用户信息,diff({#oldUser},{getUserByIdNew(#oldUser.id)})", bizNo = "#oldUser.id", category = "用户更改") + void updateUserWithOldUserAppend(User oldUser) { + log.info("######### update user #####"); + LogRecordContext.putLogDetailAppend(" 我是附加的内容"); + } + + @LogRecordAnnotation(bizNo = "#oldUser.id", category = "用户更改") + void updateUserWithCustomContent(User oldUser, User newUser) { + LogRecordContext.putLogDetail(String.format("直接指定操作内容,更改用户信息,名称: %s --> %s\n" + + "年龄: %s --> %s", oldUser.getName(), newUser.getName(), oldUser.getAge(), newUser.getAge())); + log.info("######### update user #####"); + } + + @LogRecordAnnotation(bizNo = "#oldUser.id", category = "用户更改", condition = "false") + void updateUserWithConditionFalse(User oldUser, User newUser) { + log.info("######### update user #####"); + } + + @LogRecordAnnotation(bizNo = "#oldUser.id", category = "用户更改", condition = "#ret!=null") + User updateUserWithConditionFalseUseRet(User oldUser, User newUser) { + log.info("######### update user #####"); + return null; + } + + @LogRecordAnnotation(bizNo = "#oldUser.id", detail = "updateUserWithConditionTrueUseRet", category = "用户更改", condition = "#ret!=null") + User updateUserWithConditionTrueUseRet(User oldUser, User newUser) { + log.info("######### update user #####"); + return newUser; + } + + public String getNameById(Integer id) { + return getUserById(id).getName(); + } + + public String getNameByIdNew(Integer id) { + return getUserByIdNew(id).getName(); + } + + public String getNameById(Long departmentId, Integer id) { + return getUserById(departmentId, id).getName(); + } + + public String getNameByIdNew(Long departmentId, Integer id) { + return getUserByIdNew(departmentId, id).getName(); + + } + + public User getUserById(Integer id) { + return User.builder().id(id).name("yyq-old").age(28).build(); + } + + public User getUserByIdNew(Integer id) { + return User.builder().id(id).name("yyq-plus").age(29).build(); + } + + public User getUserById(Long departmentId, Integer id) { + return User.builder().id(id).departmentId(departmentId).name("yyq-old").age(28).build(); + } + + public User getUserByIdNew(Long departmentId, Integer id) { + return User.builder().id(id).departmentId(departmentId).name("yyq-plus").age(29).build(); + } + + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class User { + /** + * 用户id + */ + private Integer id; + /** + * 用户所属部门id + */ + private Long departmentId; + + @OpLogField(fieldName = "名称") + private String name; + + @OpLogField(fieldName = "年龄") + private Integer age; + } + + @Slf4j + public static class TestLogRecordRecordService implements LogRecordService { + + static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void record(LogRecord logRecord) { + threadLocal.set(logRecord); + log.info("【logRecord】log={}", logRecord); + } + + LogRecord record() { + return threadLocal.get(); + } + } +} diff --git a/operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTestConfig.java b/operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTestConfig.java new file mode 100644 index 0000000..44bb331 --- /dev/null +++ b/operation-log/src/test/java/com/water/ad/operation/log/test/LogRecordPointcutTestConfig.java @@ -0,0 +1,91 @@ +package com.water.ad.operation.log.test; + +import com.google.common.collect.Lists; +import com.water.ad.operation.log.aop.LogRecordPointcut; +import com.water.ad.operation.log.core.DefaultOperatorGetServiceImpl; +import com.water.ad.operation.log.core.OperatorGetService; +import com.water.ad.operation.log.core.diff.DefaultObjectDiffHandler; +import com.water.ad.operation.log.core.diff.ObjectDiffHandler; +import com.water.ad.operation.log.core.function.*; +import com.water.ad.operation.log.core.parse.LogRecordExpressionEvaluator; +import com.water.ad.operation.log.core.parse.LogRecordExpressionParser; +import com.water.ad.operation.log.core.record.LogRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.util.List; + +/** + * @author yyq + * @create 2022-02-22 + **/ +@Configuration +@Slf4j +public class LogRecordPointcutTestConfig { + + + @Bean + public LogRecordPointcutTest.UserService testService() { + return new LogRecordPointcutTest.UserService(); + } + + @Bean + public LogRecordPointcut logRecordInterceptor(LogRecordExpressionEvaluator logRecordExpressionEvaluator, + LogRecordService logRecordService, + OperatorGetService operatorGetService, + LogRecordExpressionParser logRecordExpressionParser) { + return new LogRecordPointcut(logRecordExpressionEvaluator, logRecordService, operatorGetService, logRecordExpressionParser); + } + + @Bean + @ConditionalOnMissingBean(LogRecordExpressionEvaluator.class) + public LogRecordExpressionEvaluator logRecordValueParser(LogRecordExpressionParser logRecordExpressionParser, FunctionService functionService) { + return new LogRecordExpressionEvaluator(logRecordExpressionParser); + } + + @Bean + @ConditionalOnMissingBean(OperatorGetService.class) + public OperatorGetService operatorGetService() { + return new DefaultOperatorGetServiceImpl(); + } + + @Bean + @ConditionalOnMissingBean(FunctionService.class) + @Primary + public FunctionService functionService(ParseFunctionFactory parseFunctionFactory, BeanFactory beanFactory) { + DefaultFunctionServiceImpl defaultFunctionService = new DefaultFunctionServiceImpl(parseFunctionFactory); + SpringContextBeanFunctionServiceImpl contextFunctionService = new SpringContextBeanFunctionServiceImpl(beanFactory); + TargetObjectFunctionServiceImpl objectFunctionService = new TargetObjectFunctionServiceImpl(); + return new PrioritizedFunctionServiceImpl(Lists.newArrayList(defaultFunctionService, contextFunctionService, objectFunctionService)); + } + + + @Bean + public ParseFunctionFactory parseFunctionFactory(@Autowired(required = false) List parseFunctions) { + return new ParseFunctionFactory(parseFunctions); + } + + @Bean + @ConditionalOnMissingBean(LogRecordService.class) + public LogRecordService recordService() { + return new LogRecordPointcutTest.TestLogRecordRecordService(); + } + + @Bean + @ConditionalOnMissingBean(ObjectDiffHandler.class) + public ObjectDiffHandler objectDiffHandler() { + return new DefaultObjectDiffHandler(); + } + + @Bean + public LogRecordExpressionParser logRecordExpressionParser(FunctionService functionService, + ObjectDiffHandler objectDiffHandler) { + return new LogRecordExpressionParser(functionService, objectDiffHandler); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0fab145 --- /dev/null +++ b/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + com.water + operation-log-parent + pom + 1.0 + + operation-log + operation-log-starter + + + + 1.8 + 2.0.9.RELEASE + true + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + + + + +