feature: 借鉴 @mouzt、@dadiyang 两位大佬开源框架实现操作日志工具.

pull/161/head
chen.ma 3 years ago
parent 06b42fa1b1
commit e0009f364e

@ -9,7 +9,7 @@
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<url>https://github.com/acmenlt/dynamic-thread-pool</url>
<url>https://github.com/acmenlt/dynamic-threadpool</url>
<description>🔥 强大的动态线程池,附带监控报警功能(没有依赖任何中间件)</description>
<modules>
@ -19,6 +19,7 @@
<module>config</module>
<module>discovery</module>
<module>example</module>
<module>tools</module>
<module>dynamic-threadpool-spring-boot-starter</module>
</modules>
@ -32,6 +33,8 @@
<hutool-core.version>5.4.7</hutool-core.version>
<fastjson.version>1.2.75</fastjson.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<hibernate-validator.version>6.1.5.Final</hibernate-validator.version>
<transmittable-thread-local.version>2.12.1</transmittable-thread-local.version>
<dingtalk-sdk.version>1.0.1</dingtalk-sdk.version>
@ -108,6 +111,12 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.github.dynamic-threadpool</groupId>
<artifactId>log-record-tool</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
@ -131,9 +140,29 @@
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>${dingtalk-sdk.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${transmittable-thread-local.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>chen.ma</name>

@ -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…
Cancel
Save