This commit is contained in:
yuanyuqin
2022-03-14 23:13:15 +08:00
commit e651963c4e
41 changed files with 3039 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@@ -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

309
README.md Normal file
View File

@@ -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-plusage: 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状态禁用 --> 启用
# 代码实现架构
持续补充
# 使用手册
持续补充

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>operation-log-parent</artifactId>
<groupId>com.water</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>operation-log-starter</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>com.water</groupId>
<artifactId>operation-log</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>

76
operation-log/pom.xml Normal file
View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>operation-log-parent</artifactId>
<groupId>com.water</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>operation-log</artifactId>
<version>1.0</version>
<properties>
<lombok.version>1.18.20</lombok.version>
<spring.expression.version>4.3.16.RELEASE</spring.expression.version>
<fastjson.version>1.2.76</fastjson.version>
<commons.lang3.version>3.12.0</commons.lang3.version>
<common.collection4>4.4</common.collection4>
<guava.version>31.0.1-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>asm</artifactId>
<groupId>org.ow2.asm</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.expression.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${common.collection4}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -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;
/**
* <p>
*
* @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<p>
* A typical value should look like: <p>
* ---- {"0": "disabled", "1": "enabled"} ----<p>
* Map the field values like [0/1] to a human-readable string like [enabled/disabled] <p>
*
* @return
*/
String commonValueMapping() default "{}";
}

View File

@@ -0,0 +1,90 @@
/**
* Copyright 2020 Jasper J B Deng(djbing85@gmail.com)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<p>
* A typical value should look like: <p>
* ---- {"0": "disabled", "1": "enabled"} ----<p>
* Map the field values like [0/1] to a human-readable string like [enabled/disabled] <p>
* Oplog intercepter will take the field value as key,
* "translate" it to the mapped value while generating the oplog automatically<p>
* Default or empty means field value will be used directly<p>
* 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<p>
* An ignore field will be skip when compare differences between two BO object<p>
*
* @return Default false
*/
boolean ignore() default false;
/**
* Date format will only apply on field type list below: <p>
* java.util.Date<p>
* java.util.Calendar<p>
* java.time.LocalDate<p>
* java.time.LocalTime<p>
* java.time.LocalDateTime<p>
* Invalid date format will result to a format failure,<p>
*
* @return Default "yyyy-MM-dd HH:mm:ss"
*/
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
/**
* A valid decimalFormat should looks like: #,###.##<p>
* Will try to format Double/Float/Long/Integer/BigDecimal fields <p>
* Empty decimalFormat will be ignore<p>
* <p>
* For example: #,###.## <p>
* Double d = 554545.4545454; <p>
* Formatted String: 554,545.45; <p>
* Long l = 1234567890;<p>
* Formatted String: 1,234,567,890<p>
*
* @return Default ""
*/
String decimalFormat() default "";
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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<Stack<Map<String, Object>>> VARIABLE_MAP_STACK = new InheritableThreadLocal<Stack<Map<String, Object>>>() {
@Override
protected Stack<Map<String, Object>> initialValue() {
Stack<Map<String, Object>> 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<String, Object> map = getVariables();
map.put(key, value);
}
public static Map<String, Object> 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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> 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<Field> fieldList;
if (CollectionUtils.isNotEmpty(specificFieldList)) {
fieldList = new ArrayList<Field>();
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();
}
}

View File

@@ -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<String> specificFieldList);
}

View File

@@ -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> T getValue(EvaluationContext context, Class<T> 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> T getValue(Class<T> expectedResultType) throws EvaluationException {
throw new EvaluationException(this.expressionString, "Cannot call getValue(Class<T> 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> T getValue(Object rootObject, Class<T> desiredResultType) throws EvaluationException {
throw new EvaluationException(this.expressionString, "Cannot call getValue(Object rootObject, Class<T> 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> T getValue(EvaluationContext context, Object rootObject, Class<T> desiredResultType)
throws EvaluationException {
throw new EvaluationException(this.expressionString, "Cannot call getValue(EvaluationContext context, Object rootObject, Class<T> 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");
}
}

View File

@@ -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;
/**
* 自定义方法表达式
* <p>
* NOTE不支持方法没有参数的情况
* <p>
* { method(#p1) } 单参数
* <p>
* { 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);
}
}
}

View File

@@ -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表达式
* <p>
* diff( {methodA(#p1)},{methodB(#p2)} ) preObject引用方法postObject引用方法
* <p>
* diff( {methodA(#p1)},{#objB} ) preObject引用方法postObject引用参数对象
* <p>
* diff( {#objA},{#methodB(#p2)} ) preObject引用参数对象postObject引用方法
* <p>
* 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<Expression> 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);
}
}

View File

@@ -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<Expression> 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[]{});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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<String, ParseFunction> allFunctionMap = new HashMap<>(16);
public ParseFunctionFactory(List<ParseFunction> 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);
}
}

View File

@@ -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<FunctionService> functionServices;
public PrioritizedFunctionServiceImpl(List<FunctionService> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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}
* <p>
* 修改 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
* <p>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<ExpressionKey, Expression> 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<ExpressionKey> {
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;
}
}
}

View File

@@ -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<String, Object> variables = LogRecordContext.getVariables();
if (!CollectionUtils.isEmpty(variables)) {
for (Map.Entry<String, Object> entry : variables.entrySet()) {
setVariable(entry.getKey(), entry.getValue());
}
}
}
}

View File

@@ -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<ExpressionKey, Expression> 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());
}
}

View File

@@ -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 "}";
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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<String> argsSpelExpression;
/**
* 是否是前置执行
*/
private boolean beforeMethod;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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("@");
}
}

View File

@@ -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<LogRecord> threadLocal = new ThreadLocal<>();
@Override
public void record(LogRecord logRecord) {
threadLocal.set(logRecord);
log.info("【logRecord】log={}", logRecord);
}
LogRecord record() {
return threadLocal.get();
}
}
}

View File

@@ -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<ParseFunction> 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);
}
}

48
pom.xml Normal file
View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.water</groupId>
<artifactId>operation-log-parent</artifactId>
<packaging>pom</packaging>
<version>1.0</version>
<modules>
<module>operation-log</module>
<module>operation-log-starter</module>
</modules>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.0.9.RELEASE</spring-boot.version>
<maven.test.skip>true</maven.test.skip>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>