mirror of https://github.com/longtai-cn/hippo4j
parent
06b42fa1b1
commit
e0009f364e
@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
@ -0,0 +1,60 @@
|
||||
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.dynamic-threadpool</groupId>
|
||||
<artifactId>tools</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>log-record-tool</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
操作日志记录工具类, 借鉴以下开源项目
|
||||
- https://github.com/mouzt/mzt-biz-log
|
||||
- https://github.com/dadiyang/equator
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,18 @@
|
||||
package com.github.dynamic.threadpool.logrecord.annotation;
|
||||
|
||||
/**
|
||||
* 日志字段, 用于标记需要比较的实体属性.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 21:29
|
||||
*/
|
||||
public @interface LogField {
|
||||
|
||||
/**
|
||||
* 字段名称
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String name();
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.github.dynamic.threadpool.logrecord.annotation;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.enums.LogRecordTypeEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 日志记录注解.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 21:29
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface LogRecord {
|
||||
|
||||
/**
|
||||
* 业务前缀
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String prefix();
|
||||
|
||||
/**
|
||||
* 操作日志文本模版
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String success();
|
||||
|
||||
/**
|
||||
* 操作日志失败的文本
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String fail() default "";
|
||||
|
||||
/**
|
||||
* 操作人
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String operator() default "";
|
||||
|
||||
/**
|
||||
* 业务码
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String bizNo();
|
||||
|
||||
/**
|
||||
* 日志详情
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String detail() default "";
|
||||
|
||||
/**
|
||||
* 日志种类
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String category();
|
||||
|
||||
/**
|
||||
* 记录类型
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
LogRecordTypeEnum recordType() default LogRecordTypeEnum.COMPLETE;
|
||||
|
||||
/**
|
||||
* 记录日志条件
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String condition() default "";
|
||||
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
package com.github.dynamic.threadpool.logrecord.aop;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.annotation.LogRecord;
|
||||
import com.github.dynamic.threadpool.logrecord.context.LogRecordContext;
|
||||
import com.github.dynamic.threadpool.logrecord.model.LogRecordInfo;
|
||||
import com.github.dynamic.threadpool.logrecord.model.LogRecordOps;
|
||||
import com.github.dynamic.threadpool.logrecord.model.MethodExecuteResult;
|
||||
import com.github.dynamic.threadpool.logrecord.parse.LogRecordOperationSource;
|
||||
import com.github.dynamic.threadpool.logrecord.parse.LogRecordValueParser;
|
||||
import com.github.dynamic.threadpool.logrecord.service.LogRecordService;
|
||||
import com.github.dynamic.threadpool.logrecord.service.OperatorGetService;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 日志记录切面.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:00
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class LogRecordAspect {
|
||||
|
||||
private final LogRecordService bizLogService;
|
||||
|
||||
private final LogRecordValueParser logRecordValueParser;
|
||||
|
||||
private final OperatorGetService operatorGetService;
|
||||
|
||||
private final LogRecordOperationSource logRecordOperationSource;
|
||||
|
||||
private final ConfigurableEnvironment environment;
|
||||
|
||||
@Around("@annotation(logRecord)")
|
||||
public Object logRecord(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
|
||||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = methodSignature.getMethod();
|
||||
Object target = joinPoint.getTarget();
|
||||
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
LogRecordContext.putEmptySpan();
|
||||
|
||||
Object result = null;
|
||||
Collection<LogRecordOps> operations = Lists.newArrayList();
|
||||
Map<String, String> functionNameAndReturnMap = Maps.newHashMap();
|
||||
MethodExecuteResult methodExecuteResult = new MethodExecuteResult(true);
|
||||
|
||||
try {
|
||||
operations = logRecordOperationSource.computeLogRecordOperations(method, targetClass);
|
||||
List<String> spElTemplates = getBeforeExecuteFunctionTemplate(operations);
|
||||
functionNameAndReturnMap = logRecordValueParser.processBeforeExecuteFunctionTemplate(spElTemplates, targetClass, method, args);
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("Log record parse before function exception.", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
} catch (Exception ex) {
|
||||
methodExecuteResult = new MethodExecuteResult(false, ex, ex.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
if (!CollectionUtils.isEmpty(operations)) {
|
||||
recordExecute(result, method, args, operations, targetClass,
|
||||
methodExecuteResult.isSuccess(), methodExecuteResult.getErrorMsg(), functionNameAndReturnMap);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Log record parse exception.", ex);
|
||||
} finally {
|
||||
LogRecordContext.clear();
|
||||
}
|
||||
|
||||
if (methodExecuteResult.getThrowable() != null) {
|
||||
throw methodExecuteResult.getThrowable();
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private List<String> getBeforeExecuteFunctionTemplate(Collection<LogRecordOps> operations) {
|
||||
List<String> spElTemplates = new ArrayList();
|
||||
for (LogRecordOps operation : operations) {
|
||||
// 执行之前的函数
|
||||
List<String> templates = getSpElTemplates(operation, operation.getSuccessLogTemplate());
|
||||
if (!CollectionUtils.isEmpty(templates)) {
|
||||
spElTemplates.addAll(templates);
|
||||
}
|
||||
}
|
||||
|
||||
return spElTemplates;
|
||||
}
|
||||
|
||||
private List<String> getSpElTemplates(LogRecordOps operation, String action) {
|
||||
List<String> spElTemplates = Lists.newArrayList(operation.getBizKey(), operation.getBizNo(), action, operation.getDetail());
|
||||
|
||||
if (!StringUtils.isEmpty(operation.getCondition())) {
|
||||
spElTemplates.add(operation.getCondition());
|
||||
}
|
||||
|
||||
return spElTemplates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志.
|
||||
*
|
||||
* @param ret
|
||||
* @param method
|
||||
* @param args
|
||||
* @param operations
|
||||
* @param targetClass
|
||||
* @param success
|
||||
* @param errorMsg
|
||||
* @param functionNameAndReturnMap
|
||||
*/
|
||||
private void recordExecute(Object ret, Method method, Object[] args, Collection<LogRecordOps> operations,
|
||||
Class<?> targetClass, boolean success, String errorMsg, Map<String, String> functionNameAndReturnMap) {
|
||||
for (LogRecordOps operation : operations) {
|
||||
try {
|
||||
String action = getActionContent(success, operation);
|
||||
if (StringUtils.isEmpty(action)) {
|
||||
// 没有日志内容则忽略
|
||||
continue;
|
||||
}
|
||||
// 获取需要解析的表达式
|
||||
List<String> spElTemplates = getSpElTemplates(operation, action);
|
||||
String operatorIdFromService = getOperatorIdFromServiceAndPutTemplate(operation, spElTemplates);
|
||||
|
||||
Map<String, String> expressionValues = logRecordValueParser.processTemplate(spElTemplates, ret, targetClass, method, args, errorMsg, functionNameAndReturnMap);
|
||||
if (logConditionPassed(operation.getCondition(), expressionValues)) {
|
||||
LogRecordInfo logRecordInfo = LogRecordInfo.builder()
|
||||
.tenant(environment.getProperty("tenant"))
|
||||
.bizKey(expressionValues.get(operation.getBizKey()))
|
||||
.bizNo(expressionValues.get(operation.getBizNo()))
|
||||
.operator(getRealOperatorId(operation, operatorIdFromService, expressionValues))
|
||||
.category(operation.getCategory())
|
||||
.detail(expressionValues.get(operation.getDetail()))
|
||||
.action(expressionValues.get(action))
|
||||
.createTime(new Date())
|
||||
.build();
|
||||
|
||||
// 如果 action 为空, 不记录日志
|
||||
if (StringUtils.isEmpty(logRecordInfo.getAction())) {
|
||||
continue;
|
||||
}
|
||||
// save log 需要新开事务, 失败日志不能因为事务回滚而丢失
|
||||
Preconditions.checkNotNull(bizLogService, "bizLogService not init");
|
||||
bizLogService.record(logRecordInfo);
|
||||
}
|
||||
} catch (Exception t) {
|
||||
log.error("log record execute exception", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getActionContent(boolean success, LogRecordOps operation) {
|
||||
if (success) {
|
||||
return operation.getSuccessLogTemplate();
|
||||
}
|
||||
|
||||
return operation.getFailLogTemplate();
|
||||
}
|
||||
|
||||
private String getOperatorIdFromServiceAndPutTemplate(LogRecordOps operation, List<String> spElTemplates) {
|
||||
String realOperatorId = "";
|
||||
if (StringUtils.isEmpty(operation.getOperatorId())) {
|
||||
realOperatorId = operatorGetService.getUser().getOperatorId();
|
||||
if (StringUtils.isEmpty(realOperatorId)) {
|
||||
throw new IllegalArgumentException("LogRecord operator is null");
|
||||
}
|
||||
} else {
|
||||
spElTemplates.add(operation.getOperatorId());
|
||||
}
|
||||
|
||||
return realOperatorId;
|
||||
}
|
||||
|
||||
private boolean logConditionPassed(String condition, Map<String, String> expressionValues) {
|
||||
return StringUtils.isEmpty(condition) || StringUtils.endsWithIgnoreCase(expressionValues.get(condition), "true");
|
||||
}
|
||||
|
||||
private String getRealOperatorId(LogRecordOps operation, String operatorIdFromService, Map<String, String> expressionValues) {
|
||||
return !StringUtils.isEmpty(operatorIdFromService) ? operatorIdFromService : expressionValues.get(operation.getOperatorId());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package com.github.dynamic.threadpool.logrecord.compare;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 比对器抽象类.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 20:25
|
||||
*/
|
||||
public class AbstractEquator implements Equator {
|
||||
|
||||
private static final List<Class<?>> WRAPPER =
|
||||
Arrays
|
||||
.asList(
|
||||
Byte.class,
|
||||
Short.class,
|
||||
Integer.class,
|
||||
Long.class,
|
||||
Float.class,
|
||||
Double.class,
|
||||
Character.class,
|
||||
Boolean.class,
|
||||
String.class
|
||||
);
|
||||
|
||||
private List<String> includeFields;
|
||||
|
||||
private List<String> excludeFields;
|
||||
|
||||
private boolean bothExistFieldOnly = true;
|
||||
|
||||
public AbstractEquator() {
|
||||
includeFields = Collections.emptyList();
|
||||
excludeFields = Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bothExistFieldOnly 是否仅比对两个类都包含的字段
|
||||
*/
|
||||
public AbstractEquator(boolean bothExistFieldOnly) {
|
||||
includeFields = Collections.emptyList();
|
||||
excludeFields = Collections.emptyList();
|
||||
this.bothExistFieldOnly = bothExistFieldOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定包含或排除某些字段.
|
||||
*
|
||||
* @param includeFields 包含字段, 若为 null 或空集则不指定
|
||||
* @param excludeFields 排除字段, 若为 null 或空集则不指定
|
||||
*/
|
||||
public AbstractEquator(List<String> includeFields, List<String> excludeFields) {
|
||||
this.includeFields = includeFields;
|
||||
this.excludeFields = excludeFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定包含或排除某些字段.
|
||||
*
|
||||
* @param includeFields 包含字段, 若为 null 或空集则不指定
|
||||
* @param excludeFields 排除字段, 若为 null 或空集则不指定
|
||||
* @param bothExistFieldOnly 是否只对比两个类都包含的字段, 默认为 true
|
||||
*/
|
||||
public AbstractEquator(List<String> includeFields, List<String> excludeFields, boolean bothExistFieldOnly) {
|
||||
this.includeFields = includeFields;
|
||||
this.excludeFields = excludeFields;
|
||||
this.bothExistFieldOnly = bothExistFieldOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEquals(Object first, Object second) {
|
||||
List<FieldInfo> diff = getDiffFields(first, second);
|
||||
return diff == null || diff.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldInfo> getDiffFields(Object first, Object second) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比两个对象的指定属性是否相等, 默认为两个对象是否 equals.
|
||||
* <p>
|
||||
* 子类可以通过覆盖此方法对某些特殊属性进行比对.
|
||||
*
|
||||
* @param fieldInfo
|
||||
* @return
|
||||
*/
|
||||
protected boolean isFieldEquals(FieldInfo fieldInfo) {
|
||||
// 先判断排除, 如果需要排除, 则无论在不在包含范围, 都一律不比对
|
||||
if (isExclude(fieldInfo)) {
|
||||
return true;
|
||||
}
|
||||
// 如果有指定需要包含的字段而且当前字段不在需要包含的字段中则不比对
|
||||
if (!isInclude(fieldInfo)) {
|
||||
return true;
|
||||
}
|
||||
return nullableEquals(fieldInfo.getFirstVal(), fieldInfo.getSecondVal());
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定是否需要需要排除这个字段, 子类可以扩展这个方法, 自定义判断方式.
|
||||
*
|
||||
* @param fieldInfo
|
||||
* @return
|
||||
*/
|
||||
protected boolean isExclude(FieldInfo fieldInfo) {
|
||||
// 如果有指定需要排除的字段,而且当前字段是需要排除字段,则直接返回 true
|
||||
return excludeFields != null && !excludeFields.isEmpty() && excludeFields.contains(fieldInfo.getFieldName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定是否需要比较这个字段, 子类可以扩展这个方法, 自定义判断方式.
|
||||
*
|
||||
* @param fieldInfo
|
||||
* @return
|
||||
*/
|
||||
protected boolean isInclude(FieldInfo fieldInfo) {
|
||||
// 没有指定需要包含的字段,则全部都包含
|
||||
if (includeFields == null || includeFields.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return includeFields.contains(fieldInfo.getFieldName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果简单数据类型的对象则直接进行比对.
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return
|
||||
*/
|
||||
protected List<FieldInfo> compareSimpleField(Object first, Object second) {
|
||||
boolean eq = Objects.equals(first, second);
|
||||
if (eq) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
Object obj = first == null ? second : first;
|
||||
Class<?> clazz = obj.getClass();
|
||||
// 不等的字段名称使用类的名称
|
||||
return Collections.singletonList(new FieldInfo(clazz.getSimpleName(), clazz, first, second));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为原始数据类型.
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return
|
||||
*/
|
||||
protected boolean isSimpleField(Object first, Object second) {
|
||||
Object obj = first == null ? second : first;
|
||||
Class<?> clazz = obj.getClass();
|
||||
return clazz.isPrimitive() || WRAPPER.contains(clazz);
|
||||
}
|
||||
|
||||
protected boolean nullableEquals(Object first, Object second) {
|
||||
if (first instanceof Collection && second instanceof Collection) {
|
||||
// 如果两个都是集合类型,尝试转换为数组再进行深度比较
|
||||
return Objects.deepEquals(((Collection) first).toArray(), ((Collection) second).toArray());
|
||||
}
|
||||
return Objects.deepEquals(first, second);
|
||||
}
|
||||
|
||||
protected Set<String> getAllFieldNames(Set<String> firstFields, Set<String> secondFields) {
|
||||
Set<String> allFields;
|
||||
// 只取交集
|
||||
if (isBothExistFieldOnly()) {
|
||||
allFields = firstFields.stream().filter(secondFields::contains).collect(Collectors.toSet());
|
||||
} else {
|
||||
// 否则取并集
|
||||
allFields = new HashSet<>(firstFields);
|
||||
allFields.addAll(secondFields);
|
||||
}
|
||||
|
||||
return allFields;
|
||||
}
|
||||
|
||||
public boolean isBothExistFieldOnly() {
|
||||
return bothExistFieldOnly;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.github.dynamic.threadpool.logrecord.compare;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对象比对器.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 20:27
|
||||
*/
|
||||
public interface Equator {
|
||||
|
||||
/**
|
||||
* 判断两个对象是否相等.
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return
|
||||
*/
|
||||
boolean isEquals(Object first, Object second);
|
||||
|
||||
/**
|
||||
* 获取两个对象不想等的属性.
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return
|
||||
*/
|
||||
List<FieldInfo> getDiffFields(Object first, Object second);
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package com.github.dynamic.threadpool.logrecord.compare;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 对象比较中不同的字段.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 20:03
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FieldInfo {
|
||||
|
||||
/**
|
||||
* 字段名称
|
||||
*/
|
||||
private String fieldName;
|
||||
|
||||
/**
|
||||
* 第一个字段的类型
|
||||
*/
|
||||
private Class<?> firstFieldType;
|
||||
|
||||
/**
|
||||
* 第二个字段的类型
|
||||
*/
|
||||
private Class<?> secondFieldType;
|
||||
|
||||
/**
|
||||
* 第一个对象的值
|
||||
*/
|
||||
private Object firstVal;
|
||||
|
||||
/**
|
||||
* 第二个对象的值
|
||||
*/
|
||||
private Object secondVal;
|
||||
|
||||
public FieldInfo(String fieldName, Class<?> firstFieldType, Class<?> secondFieldType) {
|
||||
this.fieldName = fieldName;
|
||||
this.firstFieldType = firstFieldType;
|
||||
this.secondFieldType = secondFieldType;
|
||||
}
|
||||
|
||||
public FieldInfo(String fieldName, Class<?> fieldType, Object firstVal, Object secondVal) {
|
||||
this.fieldName = fieldName;
|
||||
this.firstFieldType = fieldType;
|
||||
this.secondFieldType = fieldType;
|
||||
this.firstVal = firstVal;
|
||||
this.secondVal = secondVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
FieldInfo fieldInfo = (FieldInfo) o;
|
||||
return Objects.equals(fieldName, fieldInfo.fieldName) &&
|
||||
Objects.equals(firstFieldType, fieldInfo.firstFieldType) &&
|
||||
Objects.equals(secondFieldType, fieldInfo.secondFieldType) &&
|
||||
Objects.equals(firstVal, fieldInfo.firstVal) &&
|
||||
Objects.equals(secondVal, fieldInfo.secondVal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fieldName, firstFieldType, secondFieldType, firstVal, secondVal);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
package com.github.dynamic.threadpool.logrecord.compare;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 基于 getter 方法比对两个对象
|
||||
* <p>
|
||||
* 所有无参的 get 和 is 方法都认为是对象的属性
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 20:36
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
public class GetterBaseEquator extends AbstractEquator {
|
||||
|
||||
private static final String GET = "get";
|
||||
|
||||
private static final String IS = "is";
|
||||
|
||||
private static final String GET_IS = "get|is";
|
||||
|
||||
private static final String GET_CLASS = "getClass";
|
||||
|
||||
private static final Map<Class<?>, Map<String, Method>> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
public GetterBaseEquator(boolean bothExistFieldOnly) {
|
||||
super(bothExistFieldOnly);
|
||||
}
|
||||
|
||||
public GetterBaseEquator(List<String> includeFields, List<String> excludeFields) {
|
||||
super(includeFields, excludeFields);
|
||||
}
|
||||
|
||||
public GetterBaseEquator(List<String> includeFields, List<String> excludeFields, boolean bothExistFieldOnly) {
|
||||
super(includeFields, excludeFields, bothExistFieldOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldInfo> getDiffFields(Object first, Object second) {
|
||||
if (first == null && second == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 先尝试判断是否为普通数据类型
|
||||
if (isSimpleField(first, second)) {
|
||||
return compareSimpleField(first, second);
|
||||
}
|
||||
Set<String> allFieldNames;
|
||||
// 获取所有字段
|
||||
Map<String, Method> firstGetters = getAllGetters(first);
|
||||
Map<String, Method> secondGetters = getAllGetters(second);
|
||||
if (first == null) {
|
||||
allFieldNames = secondGetters.keySet();
|
||||
} else if (second == null) {
|
||||
allFieldNames = firstGetters.keySet();
|
||||
} else {
|
||||
allFieldNames = getAllFieldNames(firstGetters.keySet(), secondGetters.keySet());
|
||||
}
|
||||
List<FieldInfo> diffFields = new LinkedList<>();
|
||||
for (String fieldName : allFieldNames) {
|
||||
try {
|
||||
Method firstGetterMethod = firstGetters.getOrDefault(fieldName, null);
|
||||
Method secondGetterMethod = secondGetters.getOrDefault(fieldName, null);
|
||||
Object firstVal = firstGetterMethod != null ? firstGetterMethod.invoke(first) : null;
|
||||
Object secondVal = secondGetterMethod != null ? secondGetterMethod.invoke(second) : null;
|
||||
FieldInfo fieldInfo = new FieldInfo(fieldName, getReturnType(firstGetterMethod), getReturnType(secondGetterMethod));
|
||||
fieldInfo.setFirstVal(firstVal);
|
||||
fieldInfo.setSecondVal(secondVal);
|
||||
if (!isFieldEquals(fieldInfo)) {
|
||||
diffFields.add(fieldInfo);
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException("获取属性进行比对发生异常: " + fieldName, e);
|
||||
}
|
||||
}
|
||||
return diffFields;
|
||||
}
|
||||
|
||||
private Class<?> getReturnType(Method method) {
|
||||
return method == null ? null : method.getReturnType();
|
||||
}
|
||||
|
||||
private Map<String, Method> getAllGetters(Object obj) {
|
||||
if (obj == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return CACHE.computeIfAbsent(obj.getClass(), k -> {
|
||||
Class<?> clazz = obj.getClass();
|
||||
Map<String, Method> getters = new LinkedHashMap<>(8);
|
||||
while (clazz != Object.class) {
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method m : methods) {
|
||||
// getter 方法必须是 public 且没有参数的
|
||||
if (!Modifier.isPublic(m.getModifiers()) || m.getParameterTypes().length > 0) {
|
||||
continue;
|
||||
}
|
||||
if (m.getReturnType() == Boolean.class || m.getReturnType() == boolean.class) {
|
||||
// 如果返回值是 boolean 则兼容 isXxx 的写法
|
||||
if (m.getName().startsWith(IS)) {
|
||||
String fieldName = uncapitalize(m.getName().substring(2));
|
||||
getters.put(fieldName, m);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 以 get 开头但排除 getClass() 方法
|
||||
if (m.getName().startsWith(GET) && !GET_CLASS.equals(m.getName())) {
|
||||
String fieldName = uncapitalize(m.getName().replaceFirst(GET_IS, ""));
|
||||
getters.put(fieldName, m);
|
||||
}
|
||||
}
|
||||
// 得到父类, 然后赋给自己
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
return getters;
|
||||
});
|
||||
}
|
||||
|
||||
private String uncapitalize(final String str) {
|
||||
int strLen;
|
||||
if (str == null || (strLen = str.length()) == 0) {
|
||||
return str;
|
||||
}
|
||||
final int firstCodepoint = str.codePointAt(0);
|
||||
final int newCodePoint = Character.toLowerCase(firstCodepoint);
|
||||
if (firstCodepoint == newCodePoint) {
|
||||
return str;
|
||||
}
|
||||
final int[] newCodePoints = new int[strLen];
|
||||
int outOffset = 0;
|
||||
newCodePoints[outOffset++] = newCodePoint;
|
||||
for (int inOffset = Character.charCount(firstCodepoint); inOffset < strLen; ) {
|
||||
final int codepoint = str.codePointAt(inOffset);
|
||||
newCodePoints[outOffset++] = codepoint;
|
||||
inOffset += Character.charCount(codepoint);
|
||||
}
|
||||
return new String(newCodePoints, 0, outOffset);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.github.dynamic.threadpool.logrecord.config;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.parse.LogRecordOperationSource;
|
||||
import com.github.dynamic.threadpool.logrecord.service.FunctionService;
|
||||
import com.github.dynamic.threadpool.logrecord.service.LogRecordService;
|
||||
import com.github.dynamic.threadpool.logrecord.service.OperatorGetService;
|
||||
import com.github.dynamic.threadpool.logrecord.service.ParseFunction;
|
||||
import com.github.dynamic.threadpool.logrecord.parse.LogRecordValueParser;
|
||||
import com.github.dynamic.threadpool.logrecord.service.impl.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日志记录配置.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:49
|
||||
*/
|
||||
@Configuration
|
||||
public class LogRecordConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ParseFunction.class)
|
||||
public DefaultParseFunction parseFunction() {
|
||||
return new DefaultParseFunction();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public ParseFunctionFactory parseFunctionFactory(@Autowired List<ParseFunction> parseFunctions) {
|
||||
return new ParseFunctionFactory(parseFunctions);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FunctionService.class)
|
||||
public FunctionService functionService(ParseFunctionFactory parseFunctionFactory) {
|
||||
return new DefaultFunctionServiceImpl(parseFunctionFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_APPLICATION)
|
||||
@ConditionalOnMissingBean(OperatorGetService.class)
|
||||
public OperatorGetService operatorGetService() {
|
||||
return new DefaultOperatorGetServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LogRecordService.class)
|
||||
@Role(BeanDefinition.ROLE_APPLICATION)
|
||||
public LogRecordService recordService() {
|
||||
return new DefaultLogRecordServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogRecordValueParser logRecordValueParser() {
|
||||
return new LogRecordValueParser();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogRecordOperationSource logRecordOperationSource() {
|
||||
return new LogRecordOperationSource();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.github.dynamic.threadpool.logrecord.context;
|
||||
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* 日志记录上下文.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 21:47
|
||||
*/
|
||||
public class LogRecordContext {
|
||||
|
||||
private static final ThreadLocal<Stack<Map<String, Object>>> VARIABLE_MAP_STACK = new TransmittableThreadLocal();
|
||||
|
||||
/**
|
||||
* 出栈.
|
||||
*/
|
||||
public static void clear() {
|
||||
if (VARIABLE_MAP_STACK.get() != null) {
|
||||
VARIABLE_MAP_STACK.get().pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化.
|
||||
*/
|
||||
public static void putEmptySpan() {
|
||||
Stack<Map<String, Object>> mapStack = VARIABLE_MAP_STACK.get();
|
||||
if (mapStack == null) {
|
||||
Stack<Map<String, Object>> stack = new Stack<>();
|
||||
VARIABLE_MAP_STACK.set(stack);
|
||||
}
|
||||
|
||||
VARIABLE_MAP_STACK.get().push(Maps.newHashMap());
|
||||
}
|
||||
|
||||
public static Map<String, Object> getVariables() {
|
||||
Stack<Map<String, Object>> mapStack = VARIABLE_MAP_STACK.get();
|
||||
return mapStack.peek();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.github.dynamic.threadpool.logrecord.enums;
|
||||
|
||||
/**
|
||||
* 日志记录类型.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:54
|
||||
*/
|
||||
public enum LogRecordTypeEnum {
|
||||
|
||||
/**
|
||||
* 按照文字模版记录
|
||||
*/
|
||||
TEMPLATE,
|
||||
|
||||
/**
|
||||
* 比较修改前后所有区别
|
||||
*/
|
||||
COMPLETE
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package com.github.dynamic.threadpool.logrecord.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 日志记录实体.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 17:47
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LogRecordInfo {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String tenant;
|
||||
|
||||
@NotBlank(message = "bizKey required")
|
||||
@Length(max = 200, message = "appKey max length is 200")
|
||||
private String bizKey;
|
||||
|
||||
@NotBlank(message = "bizNo required")
|
||||
@Length(max = 200, message = "bizNo max length is 200")
|
||||
private String bizNo;
|
||||
|
||||
@NotBlank(message = "operator required")
|
||||
@Length(max = 63, message = "operator max length 63")
|
||||
private String operator;
|
||||
|
||||
@NotBlank(message = "opAction required")
|
||||
@Length(max = 511, message = "operator max length 511")
|
||||
private String action;
|
||||
|
||||
private String category;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
private String detail;
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.github.dynamic.threadpool.logrecord.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 日志操作记录.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:07
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class LogRecordOps {
|
||||
|
||||
private String successLogTemplate;
|
||||
|
||||
private String failLogTemplate;
|
||||
|
||||
private String operatorId;
|
||||
|
||||
private String bizKey;
|
||||
|
||||
private String bizNo;
|
||||
|
||||
private String category;
|
||||
|
||||
private String detail;
|
||||
|
||||
private String condition;
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.github.dynamic.threadpool.logrecord.model;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 方法执行结果.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:59
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public class MethodExecuteResult {
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
@NonNull
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 异常
|
||||
*/
|
||||
private Throwable throwable;
|
||||
|
||||
/**
|
||||
* 错误日志
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.github.dynamic.threadpool.logrecord.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 操作人.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:44
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Operator {
|
||||
|
||||
/**
|
||||
* 操作人 Id
|
||||
*/
|
||||
private String operatorId;
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.github.dynamic.threadpool.logrecord.parse;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.context.LogRecordContext;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Log record evaluation context.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:25
|
||||
*/
|
||||
public class LogRecordEvaluationContext extends MethodBasedEvaluationContext {
|
||||
|
||||
public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments,
|
||||
ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg) {
|
||||
super(rootObject, method, arguments, parameterNameDiscoverer);
|
||||
|
||||
Map<String, Object> variables = LogRecordContext.getVariables();
|
||||
if (variables != null && variables.size() > 0) {
|
||||
for (Map.Entry<String, Object> entry : variables.entrySet()) {
|
||||
setVariable(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
setVariable("_ret", ret);
|
||||
setVariable("_errorMsg", errorMsg);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.github.dynamic.threadpool.logrecord.parse;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.context.expression.AnnotatedElementKey;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.context.expression.CachedExpressionEvaluator;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Log record expression evaluator.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 22:22
|
||||
*/
|
||||
public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator {
|
||||
|
||||
private Map<ExpressionKey, Expression> expressionCache = Maps.newConcurrentMap();
|
||||
|
||||
private final Map<AnnotatedElementKey, Method> targetMethodCache = Maps.newConcurrentMap();
|
||||
|
||||
public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
|
||||
Object value = getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, Object.class);
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
public EvaluationContext createEvaluationContext(Method method, Object[] args, Class<?> targetClass,
|
||||
Object result, String errorMsg, BeanFactory beanFactory) {
|
||||
Method targetMethod = getTargetMethod(targetClass, method);
|
||||
LogRecordEvaluationContext evaluationContext = new LogRecordEvaluationContext(
|
||||
null, targetMethod, args, getParameterNameDiscoverer(), result, errorMsg);
|
||||
if (beanFactory != null) {
|
||||
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
|
||||
}
|
||||
return evaluationContext;
|
||||
}
|
||||
|
||||
private Method getTargetMethod(Class<?> targetClass, Method method) {
|
||||
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
|
||||
Method targetMethod = this.targetMethodCache.get(methodKey);
|
||||
if (targetMethod == null) {
|
||||
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
this.targetMethodCache.put(methodKey, targetMethod);
|
||||
}
|
||||
return targetMethod;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package com.github.dynamic.threadpool.logrecord.parse;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.annotation.LogRecord;
|
||||
import com.github.dynamic.threadpool.logrecord.model.LogRecordOps;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 日志记录操作解析.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:02
|
||||
*/
|
||||
public class LogRecordOperationSource {
|
||||
|
||||
public Collection<LogRecordOps> computeLogRecordOperations(Method method, Class<?> targetClass) {
|
||||
if (!Modifier.isPublic(method.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
|
||||
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
|
||||
|
||||
return parseLogRecordAnnotations(specificMethod);
|
||||
}
|
||||
|
||||
private Collection<LogRecordOps> parseLogRecordAnnotations(AnnotatedElement ae) {
|
||||
Collection<LogRecord> logRecordAnnotations = AnnotatedElementUtils.getAllMergedAnnotations(ae, LogRecord.class);
|
||||
Collection<LogRecordOps> ret = null;
|
||||
if (!logRecordAnnotations.isEmpty()) {
|
||||
ret = new ArrayList(1);
|
||||
for (LogRecord logRecord : logRecordAnnotations) {
|
||||
ret.add(parseLogRecordAnnotation(ae, logRecord));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private LogRecordOps parseLogRecordAnnotation(AnnotatedElement ae, LogRecord logRecord) {
|
||||
LogRecordOps recordOps = LogRecordOps.builder()
|
||||
.successLogTemplate(logRecord.success())
|
||||
.failLogTemplate(logRecord.fail())
|
||||
.bizKey(logRecord.prefix().concat("_").concat(logRecord.bizNo()))
|
||||
.bizNo(logRecord.bizNo())
|
||||
.operatorId(logRecord.operator())
|
||||
.category(StringUtils.isEmpty(logRecord.category()) ? logRecord.prefix() : logRecord.category())
|
||||
.detail(logRecord.detail())
|
||||
.condition(logRecord.condition())
|
||||
.build();
|
||||
|
||||
validateLogRecordOperation(ae, recordOps);
|
||||
return recordOps;
|
||||
}
|
||||
|
||||
private void validateLogRecordOperation(AnnotatedElement ae, LogRecordOps recordOps) {
|
||||
if (!StringUtils.hasText(recordOps.getSuccessLogTemplate()) && !StringUtils.hasText(recordOps.getFailLogTemplate())) {
|
||||
throw new IllegalStateException("Invalid logRecord annotation configuration on '" +
|
||||
ae.toString() + "'. 'one of successTemplate and failLogTemplate' attribute must be set.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package com.github.dynamic.threadpool.logrecord.parse;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.service.FunctionService;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Maps;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.expression.AnnotatedElementKey;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Log record value parser.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:27
|
||||
*/
|
||||
public class LogRecordValueParser implements BeanFactoryAware {
|
||||
|
||||
@Autowired
|
||||
private FunctionService functionService;
|
||||
|
||||
protected BeanFactory beanFactory;
|
||||
|
||||
private final LogRecordExpressionEvaluator expressionEvaluator = new LogRecordExpressionEvaluator();
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("\\{\\s*(\\w*)\\s*\\{(.*?)}}");
|
||||
|
||||
public Map<String, String> processTemplate(Collection<String> templates, Object ret,
|
||||
Class<?> targetClass, Method method, Object[] args, String errorMsg,
|
||||
Map<String, String> beforeFunctionNameAndReturnMap) {
|
||||
Map<String, String> expressionValues = Maps.newHashMap();
|
||||
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass, ret, errorMsg, beanFactory);
|
||||
|
||||
for (String expressionTemplate : templates) {
|
||||
if (expressionTemplate.contains("{")) {
|
||||
Matcher matcher = pattern.matcher(expressionTemplate);
|
||||
StringBuffer parsedStr = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String expression = matcher.group(2);
|
||||
AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(method, targetClass);
|
||||
String value = expressionEvaluator.parseExpression(expression, annotatedElementKey, evaluationContext);
|
||||
String functionReturnValue = getFunctionReturnValue(beforeFunctionNameAndReturnMap, value, matcher.group(1));
|
||||
matcher.appendReplacement(parsedStr, Strings.nullToEmpty(functionReturnValue));
|
||||
}
|
||||
matcher.appendTail(parsedStr);
|
||||
expressionValues.put(expressionTemplate, parsedStr.toString());
|
||||
} else {
|
||||
expressionValues.put(expressionTemplate, expressionTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
return expressionValues;
|
||||
}
|
||||
|
||||
public Map<String, String> processBeforeExecuteFunctionTemplate(Collection<String> templates, Class<?> targetClass, Method method, Object[] args) {
|
||||
Map<String, String> functionNameAndReturnValueMap = new HashMap<>();
|
||||
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass, null, null, beanFactory);
|
||||
|
||||
for (String expressionTemplate : templates) {
|
||||
if (expressionTemplate.contains("{")) {
|
||||
Matcher matcher = pattern.matcher(expressionTemplate);
|
||||
while (matcher.find()) {
|
||||
String expression = matcher.group(2);
|
||||
if (expression.contains("#_ret") || expression.contains("#_errorMsg")) {
|
||||
continue;
|
||||
}
|
||||
AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(method, targetClass);
|
||||
String functionName = matcher.group(1);
|
||||
if (functionService.beforeFunction(functionName)) {
|
||||
String value = expressionEvaluator.parseExpression(expression, annotatedElementKey, evaluationContext);
|
||||
String functionReturnValue = getFunctionReturnValue(null, value, functionName);
|
||||
functionNameAndReturnValueMap.put(functionName, functionReturnValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return functionNameAndReturnValueMap;
|
||||
}
|
||||
|
||||
private String getFunctionReturnValue(Map<String, String> beforeFunctionNameAndReturnMap, String value, String functionName) {
|
||||
String functionReturnValue = "";
|
||||
if (beforeFunctionNameAndReturnMap != null) {
|
||||
functionReturnValue = beforeFunctionNameAndReturnMap.get(functionName);
|
||||
}
|
||||
if (StringUtils.isEmpty(functionReturnValue)) {
|
||||
functionReturnValue = functionService.apply(functionName, value);
|
||||
}
|
||||
return functionReturnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service;
|
||||
|
||||
/**
|
||||
* 函数服务.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:30
|
||||
*/
|
||||
public interface FunctionService {
|
||||
|
||||
/**
|
||||
* 执行.
|
||||
*
|
||||
* @param functionName
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
String apply(String functionName, String value);
|
||||
|
||||
/**
|
||||
* 是否提前执行.
|
||||
*
|
||||
* @param functionName
|
||||
* @return
|
||||
*/
|
||||
boolean beforeFunction(String functionName);
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.model.LogRecordInfo;
|
||||
|
||||
/**
|
||||
* 日志记录.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:43
|
||||
*/
|
||||
public interface LogRecordService {
|
||||
|
||||
/**
|
||||
* 保存日志.
|
||||
*
|
||||
* @param logRecordInfo
|
||||
*/
|
||||
void record(LogRecordInfo logRecordInfo);
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.model.Operator;
|
||||
|
||||
/**
|
||||
* 获取操作人.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:46
|
||||
*/
|
||||
public interface OperatorGetService {
|
||||
|
||||
/**
|
||||
* 获取操作人.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Operator getUser();
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service;
|
||||
|
||||
/**
|
||||
* 函数解析.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:40
|
||||
*/
|
||||
public interface ParseFunction {
|
||||
|
||||
/**
|
||||
* 是否先执行.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
default boolean executeBefore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数名称.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String functionName();
|
||||
|
||||
/**
|
||||
* 执行.
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
String apply(String value);
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service.impl;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.service.FunctionService;
|
||||
import com.github.dynamic.threadpool.logrecord.service.ParseFunction;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 默认实现函数接口.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 21:54
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class DefaultFunctionServiceImpl implements FunctionService {
|
||||
|
||||
private final ParseFunctionFactory parseFunctionFactory;
|
||||
|
||||
@Override
|
||||
public String apply(String functionName, String value) {
|
||||
ParseFunction function = parseFunctionFactory.getFunction(functionName);
|
||||
if (function == null) {
|
||||
return value;
|
||||
}
|
||||
return function.apply(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beforeFunction(String functionName) {
|
||||
return parseFunctionFactory.isBeforeFunction(functionName);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service.impl;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.model.LogRecordInfo;
|
||||
import com.github.dynamic.threadpool.logrecord.service.LogRecordService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 默认实现日志存储.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 17:59
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultLogRecordServiceImpl implements LogRecordService {
|
||||
|
||||
@Override
|
||||
public void record(LogRecordInfo logRecordInfo) {
|
||||
log.info("Log print :: {}", logRecordInfo);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service.impl;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.model.Operator;
|
||||
import com.github.dynamic.threadpool.logrecord.service.OperatorGetService;
|
||||
|
||||
/**
|
||||
* 默认实现.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 17:58
|
||||
*/
|
||||
public class DefaultOperatorGetServiceImpl implements OperatorGetService {
|
||||
|
||||
@Override
|
||||
public Operator getUser() {
|
||||
return new Operator("994924");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service.impl;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.service.ParseFunction;
|
||||
|
||||
/**
|
||||
* 默认实现.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/24 17:57
|
||||
*/
|
||||
public class DefaultParseFunction implements ParseFunction {
|
||||
|
||||
@Override
|
||||
public boolean executeBefore() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String functionName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.github.dynamic.threadpool.logrecord.service.impl;
|
||||
|
||||
import com.github.dynamic.threadpool.logrecord.service.ParseFunction;
|
||||
import com.google.common.collect.Maps;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 函数解析工厂.
|
||||
*
|
||||
* @author chen.ma
|
||||
* @date 2021/10/23 22:39
|
||||
*/
|
||||
public class ParseFunctionFactory {
|
||||
|
||||
private Map<String, ParseFunction> allFunctionMap;
|
||||
|
||||
public ParseFunctionFactory(List<ParseFunction> parseFunctions) {
|
||||
if (CollectionUtils.isEmpty(parseFunctions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
allFunctionMap = Maps.newHashMap();
|
||||
for (ParseFunction parseFunction : parseFunctions) {
|
||||
if (StringUtils.isEmpty(parseFunction.functionName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
allFunctionMap.put(parseFunction.functionName(), parseFunction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取函数实例.
|
||||
*
|
||||
* @param functionName
|
||||
* @return
|
||||
*/
|
||||
public ParseFunction getFunction(String functionName) {
|
||||
return allFunctionMap.get(functionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否提前执行.
|
||||
*
|
||||
* @param functionName
|
||||
* @return
|
||||
*/
|
||||
public boolean isBeforeFunction(String functionName) {
|
||||
return allFunctionMap.get(functionName) != null && allFunctionMap.get(functionName).executeBefore();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.github.dynamic.threadpool.logrecord;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LogRecordToolApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in new issue