From e0009f364eb4c3af2f9d6740e23dd4a3b63976b3 Mon Sep 17 00:00:00 2001 From: "chen.ma" Date: Sun, 24 Oct 2021 22:40:27 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=80=9F=E9=89=B4=20@mouzt=E3=80=81?= =?UTF-8?q?@dadiyang=20=E4=B8=A4=E4=BD=8D=E5=A4=A7=E4=BD=AC=E5=BC=80?= =?UTF-8?q?=E6=BA=90=E6=A1=86=E6=9E=B6=E5=AE=9E=E7=8E=B0=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=B7=A5=E5=85=B7.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 31 ++- tools/log-record-tool/.gitignore | 33 +++ tools/log-record-tool/pom.xml | 60 +++++ .../logrecord/annotation/LogField.java | 18 ++ .../logrecord/annotation/LogRecord.java | 81 +++++++ .../logrecord/aop/LogRecordAspect.java | 206 ++++++++++++++++++ .../logrecord/compare/AbstractEquator.java | 186 ++++++++++++++++ .../threadpool/logrecord/compare/Equator.java | 31 +++ .../logrecord/compare/FieldInfo.java | 80 +++++++ .../logrecord/compare/GetterBaseEquator.java | 144 ++++++++++++ .../logrecord/config/LogRecordConfig.java | 70 ++++++ .../logrecord/context/LogRecordContext.java | 46 ++++ .../logrecord/enums/LogRecordTypeEnum.java | 21 ++ .../logrecord/model/LogRecordInfo.java | 50 +++++ .../logrecord/model/LogRecordOps.java | 32 +++ .../logrecord/model/MethodExecuteResult.java | 33 +++ .../threadpool/logrecord/model/Operator.java | 23 ++ .../parse/LogRecordEvaluationContext.java | 32 +++ .../parse/LogRecordExpressionEvaluator.java | 53 +++++ .../parse/LogRecordOperationSource.java | 71 ++++++ .../logrecord/parse/LogRecordValueParser.java | 107 +++++++++ .../logrecord/service/FunctionService.java | 28 +++ .../logrecord/service/LogRecordService.java | 20 ++ .../logrecord/service/OperatorGetService.java | 20 ++ .../logrecord/service/ParseFunction.java | 35 +++ .../impl/DefaultFunctionServiceImpl.java | 32 +++ .../impl/DefaultLogRecordServiceImpl.java | 21 ++ .../impl/DefaultOperatorGetServiceImpl.java | 19 ++ .../service/impl/DefaultParseFunction.java | 28 +++ .../service/impl/ParseFunctionFactory.java | 56 +++++ .../LogRecordToolApplicationTests.java | 11 + 31 files changed, 1677 insertions(+), 1 deletion(-) create mode 100644 tools/log-record-tool/.gitignore create mode 100644 tools/log-record-tool/pom.xml create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogField.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogRecord.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/aop/LogRecordAspect.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/AbstractEquator.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/Equator.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/FieldInfo.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/GetterBaseEquator.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/config/LogRecordConfig.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/context/LogRecordContext.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/enums/LogRecordTypeEnum.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordInfo.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordOps.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/MethodExecuteResult.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/Operator.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordEvaluationContext.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordExpressionEvaluator.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordOperationSource.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordValueParser.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/FunctionService.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/LogRecordService.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/OperatorGetService.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/ParseFunction.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultFunctionServiceImpl.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultLogRecordServiceImpl.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultOperatorGetServiceImpl.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultParseFunction.java create mode 100644 tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/ParseFunctionFactory.java create mode 100644 tools/log-record-tool/src/test/java/com/github/dynamic/threadpool/logrecord/LogRecordToolApplicationTests.java diff --git a/pom.xml b/pom.xml index d51e1c9b..67507cb1 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ pom ${project.artifactId} - https://github.com/acmenlt/dynamic-thread-pool + https://github.com/acmenlt/dynamic-threadpool 🔥 强大的动态线程池,附带监控报警功能(没有依赖任何中间件) @@ -19,6 +19,7 @@ config discovery example + tools dynamic-threadpool-spring-boot-starter @@ -32,6 +33,8 @@ 5.4.7 1.2.75 3.12.0 + 6.1.5.Final + 2.12.1 1.0.1 @@ -108,6 +111,12 @@ ${revision} + + com.github.dynamic-threadpool + log-record-tool + ${revision} + + com.baomidou mybatis-plus-boot-starter @@ -131,9 +140,29 @@ alibaba-dingtalk-service-sdk ${dingtalk-sdk.version} + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + chen.ma diff --git a/tools/log-record-tool/.gitignore b/tools/log-record-tool/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/tools/log-record-tool/.gitignore @@ -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/ diff --git a/tools/log-record-tool/pom.xml b/tools/log-record-tool/pom.xml new file mode 100644 index 00000000..b8ce38db --- /dev/null +++ b/tools/log-record-tool/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + com.github.dynamic-threadpool + tools + ${revision} + + + log-record-tool + jar + + ${project.artifactId} + + 操作日志记录工具类, 借鉴以下开源项目 + - https://github.com/mouzt/mzt-biz-log + - https://github.com/dadiyang/equator + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.aspectj + aspectjweaver + + + + org.projectlombok + lombok + + + + com.alibaba + transmittable-thread-local + + + + com.google.guava + guava + + + + org.hibernate.validator + hibernate-validator + + + + diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogField.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogField.java new file mode 100644 index 00000000..f6dadfa8 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogField.java @@ -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(); + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogRecord.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogRecord.java new file mode 100644 index 00000000..c1c2f767 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/annotation/LogRecord.java @@ -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 ""; + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/aop/LogRecordAspect.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/aop/LogRecordAspect.java new file mode 100644 index 00000000..5ff0fb73 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/aop/LogRecordAspect.java @@ -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 operations = Lists.newArrayList(); + Map functionNameAndReturnMap = Maps.newHashMap(); + MethodExecuteResult methodExecuteResult = new MethodExecuteResult(true); + + try { + operations = logRecordOperationSource.computeLogRecordOperations(method, targetClass); + List 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 getBeforeExecuteFunctionTemplate(Collection operations) { + List spElTemplates = new ArrayList(); + for (LogRecordOps operation : operations) { + // 执行之前的函数 + List templates = getSpElTemplates(operation, operation.getSuccessLogTemplate()); + if (!CollectionUtils.isEmpty(templates)) { + spElTemplates.addAll(templates); + } + } + + return spElTemplates; + } + + private List getSpElTemplates(LogRecordOps operation, String action) { + List 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 operations, + Class targetClass, boolean success, String errorMsg, Map functionNameAndReturnMap) { + for (LogRecordOps operation : operations) { + try { + String action = getActionContent(success, operation); + if (StringUtils.isEmpty(action)) { + // 没有日志内容则忽略 + continue; + } + // 获取需要解析的表达式 + List spElTemplates = getSpElTemplates(operation, action); + String operatorIdFromService = getOperatorIdFromServiceAndPutTemplate(operation, spElTemplates); + + Map 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 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 expressionValues) { + return StringUtils.isEmpty(condition) || StringUtils.endsWithIgnoreCase(expressionValues.get(condition), "true"); + } + + private String getRealOperatorId(LogRecordOps operation, String operatorIdFromService, Map expressionValues) { + return !StringUtils.isEmpty(operatorIdFromService) ? operatorIdFromService : expressionValues.get(operation.getOperatorId()); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/AbstractEquator.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/AbstractEquator.java new file mode 100644 index 00000000..484f6cc5 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/AbstractEquator.java @@ -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> WRAPPER = + Arrays + .asList( + Byte.class, + Short.class, + Integer.class, + Long.class, + Float.class, + Double.class, + Character.class, + Boolean.class, + String.class + ); + + private List includeFields; + + private List 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 includeFields, List excludeFields) { + this.includeFields = includeFields; + this.excludeFields = excludeFields; + } + + /** + * 指定包含或排除某些字段. + * + * @param includeFields 包含字段, 若为 null 或空集则不指定 + * @param excludeFields 排除字段, 若为 null 或空集则不指定 + * @param bothExistFieldOnly 是否只对比两个类都包含的字段, 默认为 true + */ + public AbstractEquator(List includeFields, List excludeFields, boolean bothExistFieldOnly) { + this.includeFields = includeFields; + this.excludeFields = excludeFields; + this.bothExistFieldOnly = bothExistFieldOnly; + } + + @Override + public boolean isEquals(Object first, Object second) { + List diff = getDiffFields(first, second); + return diff == null || diff.isEmpty(); + } + + @Override + public List getDiffFields(Object first, Object second) { + return null; + } + + /** + * 对比两个对象的指定属性是否相等, 默认为两个对象是否 equals. + *

+ * 子类可以通过覆盖此方法对某些特殊属性进行比对. + * + * @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 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 getAllFieldNames(Set firstFields, Set secondFields) { + Set 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; + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/Equator.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/Equator.java new file mode 100644 index 00000000..6f487a39 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/Equator.java @@ -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 getDiffFields(Object first, Object second); + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/FieldInfo.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/FieldInfo.java new file mode 100644 index 00000000..c928ae44 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/FieldInfo.java @@ -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); + } + + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/GetterBaseEquator.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/GetterBaseEquator.java new file mode 100644 index 00000000..45270b93 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/compare/GetterBaseEquator.java @@ -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 方法比对两个对象 + *

+ * 所有无参的 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, Map> CACHE = new ConcurrentHashMap<>(); + + public GetterBaseEquator(boolean bothExistFieldOnly) { + super(bothExistFieldOnly); + } + + public GetterBaseEquator(List includeFields, List excludeFields) { + super(includeFields, excludeFields); + } + + public GetterBaseEquator(List includeFields, List excludeFields, boolean bothExistFieldOnly) { + super(includeFields, excludeFields, bothExistFieldOnly); + } + + @Override + public List getDiffFields(Object first, Object second) { + if (first == null && second == null) { + return Collections.emptyList(); + } + // 先尝试判断是否为普通数据类型 + if (isSimpleField(first, second)) { + return compareSimpleField(first, second); + } + Set allFieldNames; + // 获取所有字段 + Map firstGetters = getAllGetters(first); + Map secondGetters = getAllGetters(second); + if (first == null) { + allFieldNames = secondGetters.keySet(); + } else if (second == null) { + allFieldNames = firstGetters.keySet(); + } else { + allFieldNames = getAllFieldNames(firstGetters.keySet(), secondGetters.keySet()); + } + List 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 getAllGetters(Object obj) { + if (obj == null) { + return Collections.emptyMap(); + } + return CACHE.computeIfAbsent(obj.getClass(), k -> { + Class clazz = obj.getClass(); + Map 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); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/config/LogRecordConfig.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/config/LogRecordConfig.java new file mode 100644 index 00000000..69223eed --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/config/LogRecordConfig.java @@ -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 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(); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/context/LogRecordContext.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/context/LogRecordContext.java new file mode 100644 index 00000000..ca908043 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/context/LogRecordContext.java @@ -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>> 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> mapStack = VARIABLE_MAP_STACK.get(); + if (mapStack == null) { + Stack> stack = new Stack<>(); + VARIABLE_MAP_STACK.set(stack); + } + + VARIABLE_MAP_STACK.get().push(Maps.newHashMap()); + } + + public static Map getVariables() { + Stack> mapStack = VARIABLE_MAP_STACK.get(); + return mapStack.peek(); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/enums/LogRecordTypeEnum.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/enums/LogRecordTypeEnum.java new file mode 100644 index 00000000..758b0da4 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/enums/LogRecordTypeEnum.java @@ -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 + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordInfo.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordInfo.java new file mode 100644 index 00000000..3d4491af --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordInfo.java @@ -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; + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordOps.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordOps.java new file mode 100644 index 00000000..e420b1fc --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/LogRecordOps.java @@ -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; + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/MethodExecuteResult.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/MethodExecuteResult.java new file mode 100644 index 00000000..f4f5c7b2 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/MethodExecuteResult.java @@ -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; + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/Operator.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/Operator.java new file mode 100644 index 00000000..f4c26dc1 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/model/Operator.java @@ -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; + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordEvaluationContext.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordEvaluationContext.java new file mode 100644 index 00000000..5f29404a --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordEvaluationContext.java @@ -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 variables = LogRecordContext.getVariables(); + if (variables != null && variables.size() > 0) { + for (Map.Entry entry : variables.entrySet()) { + setVariable(entry.getKey(), entry.getValue()); + } + } + + setVariable("_ret", ret); + setVariable("_errorMsg", errorMsg); + } +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordExpressionEvaluator.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordExpressionEvaluator.java new file mode 100644 index 00000000..40060b4d --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordExpressionEvaluator.java @@ -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 expressionCache = Maps.newConcurrentMap(); + + private final Map 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; + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordOperationSource.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordOperationSource.java new file mode 100644 index 00000000..9607debc --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordOperationSource.java @@ -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 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 parseLogRecordAnnotations(AnnotatedElement ae) { + Collection logRecordAnnotations = AnnotatedElementUtils.getAllMergedAnnotations(ae, LogRecord.class); + Collection 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."); + } + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordValueParser.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordValueParser.java new file mode 100644 index 00000000..bc26e584 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/parse/LogRecordValueParser.java @@ -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 processTemplate(Collection templates, Object ret, + Class targetClass, Method method, Object[] args, String errorMsg, + Map beforeFunctionNameAndReturnMap) { + Map 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 processBeforeExecuteFunctionTemplate(Collection templates, Class targetClass, Method method, Object[] args) { + Map 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 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; + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/FunctionService.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/FunctionService.java new file mode 100644 index 00000000..278284b1 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/FunctionService.java @@ -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); + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/LogRecordService.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/LogRecordService.java new file mode 100644 index 00000000..5f5461e9 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/LogRecordService.java @@ -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); + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/OperatorGetService.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/OperatorGetService.java new file mode 100644 index 00000000..cb1d297d --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/OperatorGetService.java @@ -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(); + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/ParseFunction.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/ParseFunction.java new file mode 100644 index 00000000..46d8c4e3 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/ParseFunction.java @@ -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); + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultFunctionServiceImpl.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultFunctionServiceImpl.java new file mode 100644 index 00000000..181c8d72 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultFunctionServiceImpl.java @@ -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); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultLogRecordServiceImpl.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultLogRecordServiceImpl.java new file mode 100644 index 00000000..174ec3ba --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultLogRecordServiceImpl.java @@ -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); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultOperatorGetServiceImpl.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultOperatorGetServiceImpl.java new file mode 100644 index 00000000..84a0a830 --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultOperatorGetServiceImpl.java @@ -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"); + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultParseFunction.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultParseFunction.java new file mode 100644 index 00000000..3f0236fd --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/DefaultParseFunction.java @@ -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; + } + +} diff --git a/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/ParseFunctionFactory.java b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/ParseFunctionFactory.java new file mode 100644 index 00000000..5b04405c --- /dev/null +++ b/tools/log-record-tool/src/main/java/com/github/dynamic/threadpool/logrecord/service/impl/ParseFunctionFactory.java @@ -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 allFunctionMap; + + public ParseFunctionFactory(List 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(); + } + +} diff --git a/tools/log-record-tool/src/test/java/com/github/dynamic/threadpool/logrecord/LogRecordToolApplicationTests.java b/tools/log-record-tool/src/test/java/com/github/dynamic/threadpool/logrecord/LogRecordToolApplicationTests.java new file mode 100644 index 00000000..58abdd12 --- /dev/null +++ b/tools/log-record-tool/src/test/java/com/github/dynamic/threadpool/logrecord/LogRecordToolApplicationTests.java @@ -0,0 +1,11 @@ +package com.github.dynamic.threadpool.logrecord; + +import org.junit.jupiter.api.Test; + +class LogRecordToolApplicationTests { + + @Test + void contextLoads() { + } + +}