diff --git a/README.md b/README.md index 4c7dfec..40eea22 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > 参考 [mzt-biz-log](https://github.com/mouzt/mzt-biz-log) 实现的一款基于 spring aop 操作日志记录工具 支持自定义方法处理

- + diff --git a/operation-log/pom.xml b/operation-log-core/pom.xml similarity index 86% rename from operation-log/pom.xml rename to operation-log-core/pom.xml index a8484cf..9053f19 100644 --- a/operation-log/pom.xml +++ b/operation-log-core/pom.xml @@ -2,22 +2,24 @@ + 4.0.0 operation-log-parent cn.hangsman.operationlog 1.0.0 - 4.0.0 - - operation-log + operation-log-core 1.0.0 - org.springframework spring-context-support + + org.springframework + spring-aop + org.slf4j slf4j-api @@ -34,5 +36,4 @@ - \ No newline at end of file diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/OperationLog.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/OperationLog.java similarity index 91% rename from operation-log/src/main/java/cn/hangsman/operationlog/OperationLog.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/OperationLog.java index f26e84e..d8eae7f 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/OperationLog.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/OperationLog.java @@ -20,6 +20,6 @@ public class OperationLog { private String fail; private String detail; private String category; - private Date operatingTime; + private Date operationTime; } diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/Operator.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/Operator.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/Operator.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/Operator.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/annotation/OperationLog.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/annotation/OperationLog.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/annotation/OperationLog.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/annotation/OperationLog.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/annotation/OperationLogAnnotationParser.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/annotation/OperationLogAnnotationParser.java similarity index 54% rename from operation-log/src/main/java/cn/hangsman/operationlog/annotation/OperationLogAnnotationParser.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/annotation/OperationLogAnnotationParser.java index b4cb84b..c937ae6 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/annotation/OperationLogAnnotationParser.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/annotation/OperationLogAnnotationParser.java @@ -2,10 +2,13 @@ import cn.hangsman.operationlog.interceptor.OperationLogParam; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.StringUtils; import java.lang.reflect.AnnotatedElement; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.stream.Collectors; /** @@ -34,14 +37,25 @@ private Collection parseAnnotations(AnnotatedElement ae, bool if (ans.isEmpty()) { return null; } - return ans.stream().map(an -> OperationLogParam.builder() - .name(ae.toString()) - .content(an.content()) - .fail(an.fail()) - .category(an.category()) - .detail(an.detail()) - .condition(an.condition()) - .before(an.before()).build()).collect(Collectors.toCollection(ArrayList::new)); + return ans.stream().map(an -> { + Map beforeHandles = new HashMap<>(); + for (String template : an.before()) { + if (StringUtils.hasText(template)) { + int delimiterIndex = template.indexOf("="); + String variableName = template.substring(0, delimiterIndex); + String expressionStr = template.substring(delimiterIndex + 1); + beforeHandles.put(variableName, expressionStr); + } + } + return OperationLogParam.builder() + .name(ae.toString()) + .content(an.content()) + .fail(an.fail()) + .category(an.category()) + .detail(an.detail()) + .condition(an.condition()) + .before(beforeHandles).build(); + }).collect(Collectors.toCollection(ArrayList::new)); } } diff --git a/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/CachedExpressionEvaluator.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/CachedExpressionEvaluator.java new file mode 100644 index 0000000..efcaba4 --- /dev/null +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/CachedExpressionEvaluator.java @@ -0,0 +1,137 @@ +package cn.hangsman.operationlog.expression; + +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.common.TemplateAwareExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.Map; + +/** + * Shared utility class used to evaluate and cache SpEL expressions that + * are defined on {@link java.lang.reflect.AnnotatedElement}. + * + * @author Stephane Nicoll + * @see AnnotatedElementKey + * @since 4.2 + */ +public class CachedExpressionEvaluator { + + private final ExpressionParser parser; + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + /** + * Create a new instance with the specified {@link ExpressionParser}. + */ + protected CachedExpressionEvaluator(ExpressionParser parser) { + Assert.notNull(parser, "SpelExpressionParser must not be null"); + this.parser = parser; + } + + /** + * Create a new instance with a default {@link SpelExpressionParser}. + */ + protected CachedExpressionEvaluator() { + this(new SpelExpressionParser()); + } + + + /** + * Return the {@link TemplateAwareExpressionParser} to use. + */ + protected ExpressionParser getParser() { + return this.parser; + } + + /** + * Return a shared parameter name discoverer which caches data internally. + * + * @since 4.3 + */ + protected ParameterNameDiscoverer getParameterNameDiscoverer() { + return this.parameterNameDiscoverer; + } + + + /** + * Return the {@link Expression} for the specified SpEL value + *

Parse the expression if it hasn't been already. + * + * @param cache the cache to use + * @param elementKey the element on which the expression is defined + * @param expression the expression to parse + */ + protected Expression getExpression(Map cache, + AnnotatedElementKey elementKey, String expression) { + + ExpressionKey expressionKey = createKey(elementKey, expression); + Expression expr = cache.get(expressionKey); + if (expr == null) { + expr = getParser().parseExpression(expression); + cache.put(expressionKey, expr); + } + return expr; + } + + private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) { + return new ExpressionKey(elementKey, expression); + } + + + /** + * An expression key. + */ + protected static class ExpressionKey implements Comparable { + + private final AnnotatedElementKey element; + + private final String expression; + + protected ExpressionKey(AnnotatedElementKey element, String expression) { + Assert.notNull(element, "AnnotatedElementKey must not be null"); + Assert.notNull(expression, "Expression must not be null"); + this.element = element; + this.expression = expression; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ExpressionKey)) { + return false; + } + ExpressionKey otherKey = (ExpressionKey) other; + return (this.element.equals(otherKey.element) && + ObjectUtils.nullSafeEquals(this.expression, otherKey.expression)); + } + + @Override + public int hashCode() { + return this.element.hashCode() * 29 + this.expression.hashCode(); + } + + @Override + public String toString() { + return this.element + " with expression \"" + this.expression + "\""; + } + + @Override + public int compareTo(ExpressionKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0) { + result = this.expression.compareTo(other.expression); + } + return result; + } + } +} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelUtil.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/ExpressionUtil.java similarity index 98% rename from operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelUtil.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/expression/ExpressionUtil.java index 640f02c..c397773 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelUtil.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/ExpressionUtil.java @@ -1,4 +1,4 @@ -package cn.hangsman.operationlog.spel; +package cn.hangsman.operationlog.expression; import org.springframework.expression.ParseException; @@ -11,7 +11,7 @@ * @author hangsman * @since 1.0 */ -public class SpelUtil { +public class ExpressionUtil { private static boolean isSuffixHere(String expressionString, int pos, String suffix) { int suffixPosition = 0; diff --git a/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/OperationLogExpressionEvaluator.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/OperationLogExpressionEvaluator.java new file mode 100644 index 0000000..8bc3e3b --- /dev/null +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/OperationLogExpressionEvaluator.java @@ -0,0 +1,46 @@ +package cn.hangsman.operationlog.expression; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.TypedValue; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by 2022/1/16 9:20 + * + * @author hangsman + * @since 1.0 + */ +public class OperationLogExpressionEvaluator extends CachedExpressionEvaluator { + + private final Map expressionCache = new ConcurrentHashMap<>(64); + + public OperationLogExpressionEvaluator(ExpressionParser parser) { + super(parser); + } + + public EvaluationContext createEvaluationContext(Method method, Object[] arguments, BeanFactory beanFactory) { + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( + TypedValue.NULL, method, arguments, getParameterNameDiscoverer()); + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + return evaluationContext; + } + + public T parseExpression(String expressionStr, AnnotatedElementKey methodKey, EvaluationContext evaluationContext, Class desiredResultType) { + return getExpression(this.expressionCache, methodKey, expressionStr).getValue(evaluationContext, desiredResultType); + } + + public boolean condition(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return (Boolean.TRUE.equals(parseExpression(conditionExpression, methodKey, evalContext, Boolean.class))); + } +} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunction.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunction.java similarity index 64% rename from operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunction.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunction.java index f348bdf..bea4508 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunction.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunction.java @@ -1,13 +1,15 @@ -package cn.hangsman.operationlog.spel; +package cn.hangsman.operationlog.expression; /** - * Created by 2022/1/12 11:17 + * Created by 2022/1/16 9:50 * * @author hangsman * @since 1.0 */ public interface SpelFunction { + Object apply(Object value); String functionName(); + } diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/spel/FunctionExpression.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunctionExpression.java similarity index 94% rename from operation-log/src/main/java/cn/hangsman/operationlog/spel/FunctionExpression.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunctionExpression.java index c26c0c7..643851d 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/spel/FunctionExpression.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunctionExpression.java @@ -1,4 +1,4 @@ -package cn.hangsman.operationlog.spel; +package cn.hangsman.operationlog.expression; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; @@ -11,19 +11,19 @@ import java.util.Map; /** - * Created by 2022/1/12 11:47 + * Created by 2022/1/16 9:24 * * @author hangsman * @since 1.0 */ -public class FunctionExpression implements Expression { +public class SpelFunctionExpression implements Expression { private final String expressionStr; private final Expression[] expressions; private final SpelFunction function; - public FunctionExpression(String expressionStr, Expression[] expressions, - SpelFunction function) { + public SpelFunctionExpression(String expressionStr, Expression[] expressions, + SpelFunction function) { this.expressionStr = expressionStr; this.expressions = expressions; this.function = function; @@ -161,4 +161,5 @@ public void setValue(EvaluationContext context, Object value) throws EvaluationE public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { } + } diff --git a/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunctionExpressionParser.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunctionExpressionParser.java new file mode 100644 index 0000000..f6cc8d2 --- /dev/null +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/expression/SpelFunctionExpressionParser.java @@ -0,0 +1,273 @@ +package cn.hangsman.operationlog.expression; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.*; +import org.springframework.expression.common.ExpressionUtils; +import org.springframework.expression.common.TemplateAwareExpressionParser; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * Created by 2022/1/16 9:25 + * + * @author hangsman + * @since 1.0 + */ +public class SpelFunctionExpressionParser extends TemplateAwareExpressionParser { + + private static final Pattern VALID_FUNCTION_EXPRESSION_PATTERN = Pattern.compile(".*\\$.*?[(].*?[)].*"); + + private final ParserContext templateParserContext = new TemplateParserContext("{", "}"); + + private final ParserContext functionParserContext = new TemplateParserContext("(", ")"); + + private final SpelExpressionParser normalExpressionParser = new SpelExpressionParser(); + + private final Map functionMap = new HashMap<>(); + + public SpelFunctionExpressionParser(List functions) { + if (!CollectionUtils.isEmpty(functions)) { + for (SpelFunction parseFunction : functions) { + String functionName = parseFunction.functionName(); + Assert.hasLength(functionName, "functionName can not be empty!"); + functionMap.put(functionName, parseFunction); + } + } + } + + @Override + public Expression parseExpression(String expressionString) throws ParseException { + return this.parseExpression(expressionString, templateParserContext); + } + + @Override + protected Expression doParseExpression(String expressionString, ParserContext context) throws ParseException { + if (VALID_FUNCTION_EXPRESSION_PATTERN.matcher(expressionString).matches()) { + return doParseFunctionExpression(expressionString, functionParserContext); + } + return normalExpressionParser.parseExpression(expressionString); + } + + private Expression doParseFunctionExpression(String expressionString, ParserContext context) { + Map variableExpressionMap = getVariableExpressionMap(expressionString, context); + + String originExpressionString = expressionString; + // 将解析出来的方法替换成变量形式 + // #_ret != null ? $json(#_ret) : '' 将被替换成 #_ret != null ? #fun_uuid : '' + for (String key : variableExpressionMap.keySet()) { + Expression expression = variableExpressionMap.get(key); + expressionString = expressionString.replace(expression.getExpressionString(), "#" + key); + } + // 然后解析上面替换好的表达式 + Expression proxyExpression = doParseExpression(expressionString, null); + // ProxyExpression 会在getValue的时候先处理 variableExpressionMap 中的表达式 然后将返回值放入 EvaluationContext + return new ProxyExpression(originExpressionString, proxyExpression, variableExpressionMap); + } + + /** + * 提取方法中的变量 然后将变量解析成表达式 + */ + private Map getVariableExpressionMap(String expressionString, ParserContext context) { + String prefix = context.getExpressionPrefix(); + String suffix = context.getExpressionSuffix(); + Map variableExpressionMap = new HashMap<>(); + int startIdx = 0; + while (startIdx < expressionString.length()) { + int $Index = expressionString.indexOf("$", startIdx); + int prefixIndex = expressionString.indexOf(prefix, $Index); + if (prefixIndex >= startIdx) { + String functionName = expressionString.substring($Index + "$".length(), prefixIndex); + Assert.hasLength(functionName, "functionName can not be empty:" + expressionString); + int afterPrefixIndex = prefixIndex + prefix.length(); + int suffixIndex = ExpressionUtil.skipToCorrectEndSuffix(suffix, expressionString, afterPrefixIndex); + if (suffixIndex == -1) { + throw new ParseException(expressionString, prefixIndex, + "No ending suffix '" + suffix + "' for expression starting at character " + + prefixIndex + ": " + expressionString.substring(prefixIndex)); + } + if (suffixIndex == afterPrefixIndex) { + throw new ParseException(expressionString, prefixIndex, + "No expression defined within delimiter '" + prefix + suffix + + "' at character " + prefixIndex); + } + // 提取方法中的变量表达式 + String expr = expressionString.substring(prefixIndex + prefix.length(), suffixIndex); + if (expr.isEmpty()) { + throw new ParseException(expressionString, prefixIndex, + "No expression defined within delimiter '" + prefix + suffix + + "' at character " + prefixIndex); + } + List expressions = new ArrayList<>(); + // 处理变量表达式 多个变量以逗号分隔 $json(1,2,3.....) + for (String spel : expr.split(",")) { + expressions.add(doParseExpression(spel, null)); + } + String functionExpressionStr = expressionString.substring($Index, suffixIndex + suffix.length()); + SpelFunction function = getFunction(functionName); + Assert.notNull(function, "expression " + functionExpressionStr + " not find function :" + functionName); + SpelFunctionExpression functionExpression = + new SpelFunctionExpression(functionExpressionStr, expressions.toArray(new Expression[0]), function); + variableExpressionMap.put(generateVariableId(), functionExpression); + startIdx = suffixIndex + suffix.length(); + } else { + break; + } + } + return variableExpressionMap; + } + + private String generateVariableId() { + return "fun_" + UUID.randomUUID().toString().replace("-", ""); + } + + private SpelFunction getFunction(String functionName) { + return this.functionMap.get(functionName); + } + + private static class ProxyExpression implements Expression { + + private final String expressionString; + + private final Expression proxyExpression; + + private final Map variableExpressionMap; + + public ProxyExpression(String expressionString, Expression proxyExpression, Map variableExpressionMap) { + this.expressionString = expressionString; + this.proxyExpression = proxyExpression; + this.variableExpressionMap = variableExpressionMap; + } + + @Override + public String getExpressionString() { + return this.expressionString; + } + + @Override + public Object getValue(EvaluationContext context) throws EvaluationException { + for (String key : variableExpressionMap.keySet()) { + Expression expression = variableExpressionMap.get(key); + Object value = expression.getValue(context); + context.setVariable(key, value); + } + return proxyExpression.getValue(context); + } + + @Override + @SuppressWarnings("unchecked") + public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException { + Object result = getValue(context); + if (desiredResultType == null) { + return (T) result; + } else { + return ExpressionUtils.convertTypedValue( + context, new TypedValue(result), desiredResultType); + } + } + + @Override + public Object getValue() throws EvaluationException { + return null; + } + + @Override + public T getValue(Class desiredResultType) throws EvaluationException { + return null; + } + + @Override + public Object getValue(Object rootObject) throws EvaluationException { + return null; + } + + @Override + public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + return null; + } + + @Override + public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + return null; + } + + @Override + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) throws EvaluationException { + return null; + } + + @Override + public Class getValueType() throws EvaluationException { + return null; + } + + @Override + public Class getValueType(Object rootObject) throws EvaluationException { + return null; + } + + @Override + public Class getValueType(EvaluationContext context) throws EvaluationException { + return null; + } + + @Override + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + return null; + } + + @Override + public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + return null; + } + + @Override + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + return null; + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + return null; + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException { + return null; + } + + @Override + public boolean isWritable(Object rootObject) throws EvaluationException { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context) throws EvaluationException { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + return false; + } + + @Override + public void setValue(Object rootObject, Object value) throws EvaluationException { + + } + + @Override + public void setValue(EvaluationContext context, Object value) throws EvaluationException { + + } + + @Override + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + + } + } +} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/BeanFactoryOperationLogSourceAdvisor.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/BeanFactoryOperationLogSourceAdvisor.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/interceptor/BeanFactoryOperationLogSourceAdvisor.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/BeanFactoryOperationLogSourceAdvisor.java diff --git a/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogAspectSupport.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogAspectSupport.java new file mode 100644 index 0000000..9baf11f --- /dev/null +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogAspectSupport.java @@ -0,0 +1,253 @@ +package cn.hangsman.operationlog.interceptor; + +import cn.hangsman.operationlog.OperationLog; +import cn.hangsman.operationlog.expression.OperationLogExpressionEvaluator; +import cn.hangsman.operationlog.service.DefaultOperationLogRecorder; +import cn.hangsman.operationlog.service.DefaultOperatorService; +import cn.hangsman.operationlog.service.OperationLogRecorder; +import cn.hangsman.operationlog.service.OperatorService; +import lombok.Getter; +import lombok.Setter; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.*; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by 2022/1/14 22:16 + * + * @author hangsman + * @since 1.0 + */ +@Getter +@Setter +public class OperationLogAspectSupport implements BeanFactoryAware, SmartInitializingSingleton { + + private final Map metadataCache = new ConcurrentHashMap<>(512); + + private OperatorService operatorService = new DefaultOperatorService(); + + private OperationLogRecorder operationLogRecorder = new DefaultOperationLogRecorder(); + + private OperationLogSource operationSource; + + private OperationLogExpressionEvaluator evaluator; + + private BeanFactory beanFactory; + + protected OperationLogInvoker execute(final OperationLogInvoker invoker, Object target, Method method, Object[] args) { + Class targetClass = getTargetClass(target); + OperationLogSource operationSource = getOperationSource(); + Collection operations = operationSource.getLogOperations(method, targetClass); + if (!CollectionUtils.isEmpty(operations)) { + OperationLogParam operation = operations.iterator().next(); + LogOperationContext operationContext = createLogOperationContext(operation, method, args, target, targetClass); + if (operationContext.isConditionPassing()) { + operationContext.resolveBeforeHandle(); + boolean invokeSuccess = invoke(invoker, operationContext.evaluationContext); + recordLog(operationContext, invokeSuccess, new Date()); + } + } else { + invoker.invoke(); + } + return invoker; + } + + protected boolean invoke(OperationLogInvoker invoker, EvaluationContext evaluationContext) { + invoker.invoke(); + evaluationContext.setVariable("_ret", invoker.getRetValue()); + evaluationContext.setVariable("_errorMsg", invoker.getThrowable() != null ? invoker.getThrowable().getMessage() : ""); + return invoker.getThrowable() == null; + } + + protected void recordLog(LogOperationContext operationContext, boolean invokeSuccess, Date operationTime) { + OperationLogParam operation = operationContext.metadata.operation; + OperationLog.OperationLogBuilder builder = OperationLog.builder(); + builder.operator(this.operatorService.getOperator()); + builder.operationTime(operationTime); + builder.category(operation.category); + builder.detail(operationContext.parseTemplate(operation.detail)); + if (invokeSuccess) { + builder.content(operationContext.parseTemplate(operation.content)); + } else { + builder.fail(operationContext.parseTemplate(operation.fail)); + } + this.operationLogRecorder.record(builder.build()); + } + + private Class getTargetClass(Object target) { + return AopProxyUtils.ultimateTargetClass(target); + } + + protected LogOperationContext createLogOperationContext( + OperationLogParam operation, Method method, Object[] args, Object target, Class targetClass) { + LogOperationMetadata metadata = getLogOperationMetadata(operation, method, targetClass); + return new LogOperationContext(metadata, args, target); + } + + protected LogOperationMetadata getLogOperationMetadata(OperationLogParam operation, Method method, Class targetClass) { + LogOperationCacheKey cacheKey = new LogOperationCacheKey(operation, method, targetClass); + LogOperationMetadata metadata = this.metadataCache.get(cacheKey); + if (metadata == null) { + metadata = new LogOperationMetadata(operation, method, targetClass); + this.metadataCache.put(cacheKey, metadata); + } + return metadata; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public void afterSingletonsInstantiated() { + try { + setEvaluator(this.beanFactory.getBean(OperationLogExpressionEvaluator.class)); + setOperationLogRecorder(this.beanFactory.getBean(OperationLogRecorder.class)); + setOperatorService(this.beanFactory.getBean(OperatorService.class)); + } catch (NoUniqueBeanDefinitionException ex) { + throw new IllegalStateException("no unique bean of type.", ex); + } catch (NoSuchBeanDefinitionException ex) { + throw new IllegalStateException("no bean of type found. ", ex); + } + } + + protected static class LogOperationMetadata { + + private final OperationLogParam operation; + + private final Method method; + + private final Method targetMethod; + + private final AnnotatedElementKey methodKey; + + public LogOperationMetadata(OperationLogParam operation, Method method, Class targetClass) { + this.operation = operation; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.targetMethod = (!Proxy.isProxyClass(targetClass) ? + AopUtils.getMostSpecificMethod(method, targetClass) : this.method); + this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass); + } + } + + private static final class LogOperationCacheKey implements Comparable { + + private final OperationLogParam operation; + + private final AnnotatedElementKey methodCacheKey; + + private LogOperationCacheKey(OperationLogParam operationLogParam, Method method, Class targetClass) { + this.operation = operationLogParam; + this.methodCacheKey = new AnnotatedElementKey(method, targetClass); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LogOperationCacheKey)) { + return false; + } + LogOperationCacheKey otherKey = (LogOperationCacheKey) other; + return (this.operation.equals(otherKey.operation) && + this.methodCacheKey.equals(otherKey.methodCacheKey)); + } + + @Override + public int hashCode() { + return (this.operation.hashCode() * 31 + this.methodCacheKey.hashCode()); + } + + @Override + public String toString() { + return this.operation + " on " + this.methodCacheKey; + } + + @Override + public int compareTo(LogOperationCacheKey other) { + int result = this.operation.getName().compareTo(other.operation.getName()); + if (result == 0) { + result = this.methodCacheKey.compareTo(other.methodCacheKey); + } + return result; + } + } + + @Getter + protected class LogOperationContext { + + private final LogOperationMetadata metadata; + + private final Object[] args; + + private final Object target; + + private final EvaluationContext evaluationContext; + + private Boolean conditionPassing; + + + public LogOperationContext(LogOperationMetadata metadata, Object[] args, Object target) { + this.metadata = metadata; + this.args = extractArgs(metadata.method, args); + this.target = target; + this.evaluationContext = createEvaluationContext(metadata.targetMethod, args); + } + + private EvaluationContext createEvaluationContext(Method targetMethod, Object[] args) { + return evaluator.createEvaluationContext(targetMethod, args, beanFactory); + } + + private Object[] extractArgs(Method method, Object[] args) { + if (!method.isVarArgs()) { + return args; + } + Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]); + Object[] combinedArgs = new Object[args.length - 1 + varArgs.length]; + System.arraycopy(args, 0, combinedArgs, 0, args.length - 1); + System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length); + return combinedArgs; + } + + protected boolean isConditionPassing() { + if (this.conditionPassing == null) { + if (StringUtils.hasText(this.metadata.operation.getCondition())) { + this.conditionPassing = evaluator.condition(this.metadata.operation.getCondition(), + this.metadata.methodKey, evaluationContext); + } else { + this.conditionPassing = true; + } + } + return this.conditionPassing; + } + + protected void resolveBeforeHandle() { + metadata.operation.before.forEach((key, value) -> { + Object result = evaluator.parseExpression(value, metadata.methodKey, evaluationContext, Object.class); + evaluationContext.setVariable(key, result); + }); + } + + protected String parseTemplate(String template) { + return evaluator.parseExpression(template, metadata.methodKey, evaluationContext, String.class); + } + + } +} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInterceptor.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInterceptor.java similarity index 78% rename from operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInterceptor.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInterceptor.java index ddcf0e6..be76f45 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInterceptor.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInterceptor.java @@ -19,10 +19,10 @@ public Object invoke(MethodInvocation invocation) throws Throwable { OperationLogInvoker invoker = new OperationLogInvoker(invocation); Object target = invocation.getThis(); Assert.state(target != null, "Target must not be null"); - try { - return execute(invoker, target, method, invocation.getArguments()); - } catch (OperationLogInvoker.ThrowableWrapper th) { - throw th.getOriginal(); + invoker = execute(invoker, target, method, invocation.getArguments()); + if (invoker.getThrowable() != null) { + throw invoker.getThrowable(); } + return invoker.getRetValue(); } } diff --git a/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInvoker.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInvoker.java new file mode 100644 index 0000000..7761840 --- /dev/null +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInvoker.java @@ -0,0 +1,31 @@ +package cn.hangsman.operationlog.interceptor; + +import lombok.Getter; +import org.aopalliance.intercept.MethodInvocation; + +/** + * Created by 2022/1/14 22:13 + * + * @author hangsman + * @since 1.0 + */ +@Getter +class OperationLogInvoker { + + private final MethodInvocation invocation; + private Object retValue; + private Throwable throwable; + + public OperationLogInvoker(MethodInvocation invocation) { + this.invocation = invocation; + } + + public void invoke() { + try { + retValue = invocation.proceed(); + } catch (Throwable ex) { + throwable = ex; + } + } + +} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogParam.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogParam.java similarity index 86% rename from operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogParam.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogParam.java index 444d680..327be2f 100644 --- a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogParam.java +++ b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogParam.java @@ -3,6 +3,8 @@ import lombok.Builder; import lombok.Data; +import java.util.Map; + /** * Created by 2022/1/14 21:13 * @@ -25,6 +27,6 @@ public class OperationLogParam { String condition; - String[] before; + Map before; } diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSource.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSource.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSource.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSource.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSourcePointcut.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSourcePointcut.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSourcePointcut.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogSourcePointcut.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/service/DefaultOperationLogRecorder.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/service/DefaultOperationLogRecorder.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/service/DefaultOperationLogRecorder.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/service/DefaultOperationLogRecorder.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/service/DefaultOperatorService.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/service/DefaultOperatorService.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/service/DefaultOperatorService.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/service/DefaultOperatorService.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/service/OperationLogRecorder.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/service/OperationLogRecorder.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/service/OperationLogRecorder.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/service/OperationLogRecorder.java diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/service/OperatorService.java b/operation-log-core/src/main/java/cn/hangsman/operationlog/service/OperatorService.java similarity index 100% rename from operation-log/src/main/java/cn/hangsman/operationlog/service/OperatorService.java rename to operation-log-core/src/main/java/cn/hangsman/operationlog/service/OperatorService.java diff --git a/operation-log-spring-boot-autoconfigure/pom.xml b/operation-log-spring-boot-autoconfigure/pom.xml index 54ee01f..534c4b5 100644 --- a/operation-log-spring-boot-autoconfigure/pom.xml +++ b/operation-log-spring-boot-autoconfigure/pom.xml @@ -19,12 +19,7 @@ cn.hangsman.operationlog - operation-log - - - - org.springframework - spring-context + operation-log-core diff --git a/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/annotation/EnableOperationLog.java b/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/annotation/EnableOperationLog.java index 9ef55cf..da0e5a8 100644 --- a/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/annotation/EnableOperationLog.java +++ b/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/annotation/EnableOperationLog.java @@ -3,6 +3,7 @@ import cn.hangsman.operationlog.spring.boot.autoconfigure.OperationLogAutoConfiguration; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; import java.lang.annotation.*; @@ -22,4 +23,5 @@ boolean proxyTargetClass() default false; + int order() default Ordered.LOWEST_PRECEDENCE; } diff --git a/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogAutoConfiguration.java b/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogAutoConfiguration.java index 3272333..f16a749 100644 --- a/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogAutoConfiguration.java +++ b/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogAutoConfiguration.java @@ -1,9 +1,14 @@ package cn.hangsman.operationlog.spring.boot.autoconfigure; -import cn.hangsman.operationlog.spel.SpelFunction; -import cn.hangsman.operationlog.spel.SpelFunctionExpressionParser; -import cn.hangsman.operationlog.spel.SpelFunctionFactory; +import cn.hangsman.operationlog.expression.OperationLogExpressionEvaluator; +import cn.hangsman.operationlog.expression.SpelFunction; +import cn.hangsman.operationlog.expression.SpelFunctionExpressionParser; +import cn.hangsman.operationlog.service.DefaultOperationLogRecorder; +import cn.hangsman.operationlog.service.DefaultOperatorService; +import cn.hangsman.operationlog.service.OperationLogRecorder; +import cn.hangsman.operationlog.service.OperatorService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,12 +24,24 @@ public class OperationLogAutoConfiguration { @Bean - SpelFunctionExpressionParser spelFunctionExpressionParser() { - return new SpelFunctionExpressionParser(); + public SpelFunctionExpressionParser spelFunctionExpressionParser(@Autowired(required = false) List parseFunctions) { + return new SpelFunctionExpressionParser(parseFunctions); } @Bean - public SpelFunctionFactory spelFunctionFactory(@Autowired(required = false) List parseFunctions) { - return new SpelFunctionFactory(parseFunctions); + public OperationLogExpressionEvaluator operationLogExpressionEvaluator(SpelFunctionExpressionParser expressionParser) { + return new OperationLogExpressionEvaluator(expressionParser); + } + + @Bean + @ConditionalOnMissingBean(OperationLogRecorder.class) + public OperationLogRecorder operationLogRecorder() { + return new DefaultOperationLogRecorder(); + } + + @Bean + @ConditionalOnMissingBean(OperatorService.class) + public OperatorService operatorService() { + return new DefaultOperatorService(); } } diff --git a/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogProxyConfiguration.java b/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogProxyConfiguration.java index 8d21543..0547013 100644 --- a/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogProxyConfiguration.java +++ b/operation-log-spring-boot-autoconfigure/src/main/java/cn/hangsman/operationlog/spring/boot/autoconfigure/OperationLogProxyConfiguration.java @@ -5,7 +5,6 @@ import cn.hangsman.operationlog.interceptor.OperationLogSource; import cn.hangsman.operationlog.service.OperationLogRecorder; import cn.hangsman.operationlog.service.OperatorService; -import cn.hangsman.operationlog.spel.SpelFunctionExpressionParser; import cn.hangsman.operationlog.spring.boot.annotation.EnableOperationLog; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; @@ -22,45 +21,35 @@ * @author hangsman * @since 1.0 */ -@Configuration(proxyBeanMethods = false) +@Configuration @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class OperationLogProxyConfiguration implements ImportAware { protected AnnotationAttributes enableOperationLog; @Bean - public BeanFactoryOperationLogSourceAdvisor advisor(OperationLogInterceptor interceptor, OperationLogSource operationLogSource) { + public BeanFactoryOperationLogSourceAdvisor advisor() { BeanFactoryOperationLogSourceAdvisor advisor = new BeanFactoryOperationLogSourceAdvisor(); - advisor.setAdvice(interceptor); - advisor.setLogOperationSource(operationLogSource); + advisor.setAdvice(operationLogInterceptor()); + advisor.setLogOperationSource(logOperationSource()); + if (this.enableOperationLog != null) { + advisor.setOrder(this.enableOperationLog.getNumber("order")); + } return advisor; } - @Bean - public OperationLogInterceptor operationLogInterceptor(OperationLogSource operationLogSource, - SpelFunctionExpressionParser expressionParser, - @Autowired(required = false) OperationLogRecorder operationLogRecorder, - @Autowired(required = false) OperatorService operatorService) { + public OperationLogInterceptor operationLogInterceptor() { OperationLogInterceptor interceptor = new OperationLogInterceptor(); - interceptor.setExpressionParser(expressionParser); - interceptor.setOperationSource(operationLogSource); - if (operationLogRecorder != null) { - interceptor.setOperationLogRecorder(operationLogRecorder); - } - if (operatorService != null) { - interceptor.setOperatorService(operatorService); - } + interceptor.setOperationSource(logOperationSource()); return interceptor; } - @Bean public OperationLogSource logOperationSource() { return new OperationLogSource(); } - @Override public void setImportMetadata(AnnotationMetadata importMetadata) { this.enableOperationLog = AnnotationAttributes.fromMap( diff --git a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/domain/User.java b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/domain/User.java index ef31762..c829a79 100644 --- a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/domain/User.java +++ b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/domain/User.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Data; -import org.springframework.core.annotation.Order; /** * Created by 2022/1/14 17:23 diff --git a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/IUserService.java b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/IUserService.java index d5cbc9c..ed99445 100644 --- a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/IUserService.java +++ b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/IUserService.java @@ -1,6 +1,5 @@ package cn.hangsman.operationlog.test.service; -import cn.hangsman.operationlog.annotation.OperationLog; import cn.hangsman.operationlog.test.domain.User; /** diff --git a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/OperationLogRecordService.java b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/OperationLogRecordService.java new file mode 100644 index 0000000..1346b63 --- /dev/null +++ b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/service/OperationLogRecordService.java @@ -0,0 +1,26 @@ +package cn.hangsman.operationlog.test.service; + +import cn.hangsman.operationlog.OperationLog; +import cn.hangsman.operationlog.service.OperationLogRecorder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Created by 2022/1/16 15:47 + * + * @author hangsman + * @since 1.0 + */ +@Service +@Slf4j +public class OperationLogRecordService implements OperationLogRecorder { + + @Autowired + IUserService userService; + + @Override + public void record(OperationLog operationLog) { + log.info(operationLog.toString()); + } +} diff --git a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/JsonSpelFunction.java b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/JsonSpelFunction.java index f37b36e..59525c6 100644 --- a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/JsonSpelFunction.java +++ b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/JsonSpelFunction.java @@ -1,6 +1,7 @@ package cn.hangsman.operationlog.test.spel; -import cn.hangsman.operationlog.spel.SpelFunction; + +import cn.hangsman.operationlog.expression.SpelFunction; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; diff --git a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/UserNameSpelFunction.java b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/UserNameSpelFunction.java index ba9baf3..09c7b62 100644 --- a/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/UserNameSpelFunction.java +++ b/operation-log-test/src/main/java/cn/hangsman/operationlog/test/spel/UserNameSpelFunction.java @@ -1,6 +1,7 @@ package cn.hangsman.operationlog.test.spel; -import cn.hangsman.operationlog.spel.SpelFunction; + +import cn.hangsman.operationlog.expression.SpelFunction; import cn.hangsman.operationlog.test.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/operation-log-test/src/test/java/cn/hangsman/operationlog/test/OperationLogTest.java b/operation-log-test/src/test/java/cn/hangsman/operationlog/test/OperationLogTest.java index 2ab363e..69d6a35 100644 --- a/operation-log-test/src/test/java/cn/hangsman/operationlog/test/OperationLogTest.java +++ b/operation-log-test/src/test/java/cn/hangsman/operationlog/test/OperationLogTest.java @@ -1,6 +1,5 @@ package cn.hangsman.operationlog.test; -import cn.hangsman.operationlog.spring.boot.annotation.EnableOperationLog; import cn.hangsman.operationlog.test.domain.User; import cn.hangsman.operationlog.test.service.IUserService; import org.junit.Test; diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogAspectSupport.java b/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogAspectSupport.java deleted file mode 100644 index 68951de..0000000 --- a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogAspectSupport.java +++ /dev/null @@ -1,223 +0,0 @@ -package cn.hangsman.operationlog.interceptor; - -import cn.hangsman.operationlog.OperationLog; - -import cn.hangsman.operationlog.service.DefaultOperationLogRecorder; -import cn.hangsman.operationlog.service.DefaultOperatorService; -import cn.hangsman.operationlog.service.OperationLogRecorder; -import cn.hangsman.operationlog.service.OperatorService; -import cn.hangsman.operationlog.spel.SpelFunctionExpressionParser; -import org.springframework.aop.framework.AopProxyUtils; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.expression.AnnotatedElementKey; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Created by 2022/1/14 22:16 - * - * @author hangsman - * @since 1.0 - */ -public class OperationLogAspectSupport implements InitializingBean { - - private final Map metadataCache = new ConcurrentHashMap<>(512); - - private OperatorService operatorService = new DefaultOperatorService(); - - private OperationLogRecorder operationLogRecorder = new DefaultOperationLogRecorder(); - - private OperationLogSource operationSource; - - private SpelFunctionExpressionParser expressionParser; - - protected Object execute(OperationLogInvoker invoker, Object target, Method method, Object[] args) { - Class targetClass = getTargetClass(target); - OperationLogSource operationSource = getOperationSource(); - Collection operations = operationSource.getLogOperations(method, targetClass); - if (!CollectionUtils.isEmpty(operations)) { - OperationLogParam operation = operations.iterator().next(); - EvaluationContext evaluationContext = getExpressionParser().createEvaluationContext(method, args); - LogOperationMetadata metadata = getLogOperationMetadata(operation, method, targetClass); - recordLog(invoker, metadata, evaluationContext); - } else { - invoker.invoke(); - } - if (invoker.getThrowableWrapper() != null) { - throw invoker.getThrowableWrapper(); - } - return invoker.getRetValue(); - } - - private void recordLog(final OperationLogInvoker invoker, LogOperationMetadata metadata, EvaluationContext evaluationContext) { - for (String variableName : metadata.variableExpressionMap.keySet()) { - Expression expression = metadata.variableExpressionMap.get(variableName); - evaluationContext.setVariable(variableName, expression.getValue(evaluationContext)); - } - Date operationTime = new Date(); - invoker.invoke(); - evaluationContext.setVariable("_ret", invoker.getRetValue()); - OperationLogInvoker.ThrowableWrapper throwableWrapper = invoker.getThrowableWrapper(); - evaluationContext.setVariable("_errorMsg", throwableWrapper != null ? throwableWrapper.getMessage() : null); - if (isConditionPassing(metadata, evaluationContext)) { - OperationLog.OperationLogBuilder builder = OperationLog.builder(); - Map expressionMap = metadata.templateExpressionMap; - OperationLogParam operation = metadata.operation; - if (throwableWrapper == null) { - builder.content(expressionMap.get(operation.content).getValue(evaluationContext, String.class)); - } else { - builder.fail(expressionMap.get(operation.fail).getValue(evaluationContext, String.class)); - } - builder.detail(expressionMap.get(operation.detail).getValue(evaluationContext, String.class)); - builder.operator(getOperatorService().getOperator()); - builder.operatingTime(operationTime); - builder.category(metadata.operation.category); - this.operationLogRecorder.record(builder.build()); - } - } - - - private boolean isConditionPassing(LogOperationMetadata metadata, EvaluationContext evaluationContext) { - if (StringUtils.hasText(metadata.operation.condition)) { - Expression expression = metadata.templateExpressionMap.get(metadata.operation.condition); - Boolean expressionValue = expression.getValue(evaluationContext, Boolean.class); - return Boolean.TRUE.equals(expressionValue); - } - return true; - } - - private Class getTargetClass(Object target) { - return AopProxyUtils.ultimateTargetClass(target); - } - - protected LogOperationMetadata getLogOperationMetadata(OperationLogParam operation, Method method, Class targetClass) { - LogOperationCacheKey cacheKey = new LogOperationCacheKey(operation, method, targetClass); - LogOperationMetadata metadata = this.metadataCache.get(cacheKey); - if (metadata == null) { - Map variableExpressionMap = new HashMap<>(); - for (String template : operation.before) { - if (StringUtils.hasText(template)) { - int delimiterIndex = template.indexOf("="); - String variableName = template.substring(0, delimiterIndex); - String expressionStr = template.substring(delimiterIndex + 1); - Expression expression = getExpressionParser().parseExpression(expressionStr); - variableExpressionMap.put(variableName, expression); - } - } - Map templateExpressionMap = new HashMap<>(); - templateExpressionMap.put(operation.content, getExpressionParser().parseExpression(operation.content)); - templateExpressionMap.put(operation.fail, getExpressionParser().parseExpression(operation.fail)); - templateExpressionMap.put(operation.detail, getExpressionParser().parseExpression(operation.detail)); - templateExpressionMap.put(operation.condition, getExpressionParser().parseExpression(operation.condition)); - metadata = new LogOperationMetadata(operation, variableExpressionMap, templateExpressionMap); - this.metadataCache.put(cacheKey, metadata); - } - return metadata; - } - - public OperatorService getOperatorService() { - return operatorService; - } - - public void setOperatorService(OperatorService operatorService) { - this.operatorService = operatorService; - } - - public void setOperationLogRecorder(OperationLogRecorder operationLogRecorder) { - this.operationLogRecorder = operationLogRecorder; - } - - public OperationLogSource getOperationSource() { - return operationSource; - } - - public void setOperationSource(OperationLogSource operationSource) { - this.operationSource = operationSource; - } - - public SpelFunctionExpressionParser getExpressionParser() { - return expressionParser; - } - - public void setExpressionParser(SpelFunctionExpressionParser expressionParser) { - this.expressionParser = expressionParser; - } - - @Override - public void afterPropertiesSet() throws Exception { - Assert.notNull(expressionParser, "expressionParser can not be empty"); - Assert.notNull(operatorService, "expressionEvaluator can not be empty"); - Assert.notNull(operationLogRecorder, "OperationLogRecorder can not be empty"); - } - - - protected static class LogOperationMetadata { - - private final OperationLogParam operation; - - private final Map variableExpressionMap; - - private final Map templateExpressionMap; - - public LogOperationMetadata(OperationLogParam operation, Map variableExpressionMap, Map templateExpressionMap) { - this.operation = operation; - this.variableExpressionMap = variableExpressionMap; - this.templateExpressionMap = templateExpressionMap; - } - } - - private static final class LogOperationCacheKey implements Comparable { - - private final OperationLogParam operation; - - private final AnnotatedElementKey methodCacheKey; - - private LogOperationCacheKey(OperationLogParam operationLogParam, Method method, Class targetClass) { - this.operation = operationLogParam; - this.methodCacheKey = new AnnotatedElementKey(method, targetClass); - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (!(other instanceof LogOperationCacheKey)) { - return false; - } - LogOperationCacheKey otherKey = (LogOperationCacheKey) other; - return (this.operation.equals(otherKey.operation) && - this.methodCacheKey.equals(otherKey.methodCacheKey)); - } - - @Override - public int hashCode() { - return (this.operation.hashCode() * 31 + this.methodCacheKey.hashCode()); - } - - @Override - public String toString() { - return this.operation + " on " + this.methodCacheKey; - } - - @Override - public int compareTo(LogOperationCacheKey other) { - int result = this.operation.getName().compareTo(other.operation.getName()); - if (result == 0) { - result = this.methodCacheKey.compareTo(other.methodCacheKey); - } - return result; - } - } -} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInvoker.java b/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInvoker.java deleted file mode 100644 index 81f411b..0000000 --- a/operation-log/src/main/java/cn/hangsman/operationlog/interceptor/OperationLogInvoker.java +++ /dev/null @@ -1,50 +0,0 @@ -package cn.hangsman.operationlog.interceptor; - -import org.aopalliance.intercept.MethodInvocation; - -/** - * Created by 2022/1/14 22:13 - * - * @author hangsman - * @since 1.0 - */ -class OperationLogInvoker { - - private final MethodInvocation invocation; - private Object retValue; - private ThrowableWrapper throwableWrapper; - - public OperationLogInvoker(MethodInvocation invocation) { - this.invocation = invocation; - } - - public void invoke() throws ThrowableWrapper { - try { - retValue = invocation.proceed(); - } catch (Throwable ex) { - throwableWrapper = new ThrowableWrapper(ex); - } - } - - public Object getRetValue() { - return retValue; - } - - public ThrowableWrapper getThrowableWrapper() { - return throwableWrapper; - } - - static class ThrowableWrapper extends RuntimeException { - - private final Throwable original; - - public ThrowableWrapper(Throwable original) { - super(original.getMessage(), original); - this.original = original; - } - - public Throwable getOriginal() { - return this.original; - } - } -} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/spel/FunctionExpressionProxy.java b/operation-log/src/main/java/cn/hangsman/operationlog/spel/FunctionExpressionProxy.java deleted file mode 100644 index 7e54882..0000000 --- a/operation-log/src/main/java/cn/hangsman/operationlog/spel/FunctionExpressionProxy.java +++ /dev/null @@ -1,157 +0,0 @@ -package cn.hangsman.operationlog.spel; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.EvaluationException; -import org.springframework.expression.Expression; -import org.springframework.expression.TypedValue; -import org.springframework.expression.common.ExpressionUtils; - -import java.util.Map; - -/** - * Created by 2022/1/12 15:42 - * - * @author hangsman - * @since 1.0 - */ -public class FunctionExpressionProxy implements Expression { - - private final String expressionString; - - private final Expression proxyExpression; - private final Map variableExpressionMap; - - public FunctionExpressionProxy(String expressionString, Expression proxyExpression, Map variableExpressionMap) { - this.expressionString = expressionString; - this.proxyExpression = proxyExpression; - this.variableExpressionMap = variableExpressionMap; - } - - @Override - public String getExpressionString() { - return this.expressionString; - } - - @Override - public Object getValue(EvaluationContext context) throws EvaluationException { - for (String key : variableExpressionMap.keySet()) { - Expression expression = variableExpressionMap.get(key); - Object value = expression.getValue(context); - context.setVariable(key, value); - } - return proxyExpression.getValue(context); - } - - @Override - @SuppressWarnings("unchecked") - public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException { - Object result = getValue(context); - if (desiredResultType == null) { - return (T) result; - } else { - return ExpressionUtils.convertTypedValue( - context, new TypedValue(result), desiredResultType); - } - } - - @Override - public Object getValue() throws EvaluationException { - return null; - } - - @Override - public T getValue(Class desiredResultType) throws EvaluationException { - return null; - } - - @Override - public Object getValue(Object rootObject) throws EvaluationException { - return null; - } - - @Override - public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { - return null; - } - - @Override - public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { - return null; - } - - @Override - public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) throws EvaluationException { - return null; - } - - @Override - public Class getValueType() throws EvaluationException { - return null; - } - - @Override - public Class getValueType(Object rootObject) throws EvaluationException { - return null; - } - - @Override - public Class getValueType(EvaluationContext context) throws EvaluationException { - return null; - } - - @Override - public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { - return null; - } - - @Override - public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { - return null; - } - - @Override - public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { - return null; - } - - @Override - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { - return null; - } - - @Override - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException { - return null; - } - - @Override - public boolean isWritable(Object rootObject) throws EvaluationException { - return false; - } - - @Override - public boolean isWritable(EvaluationContext context) throws EvaluationException { - return false; - } - - @Override - public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { - return false; - } - - @Override - public void setValue(Object rootObject, Object value) throws EvaluationException { - - } - - @Override - public void setValue(EvaluationContext context, Object value) throws EvaluationException { - - } - - @Override - public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { - - } -} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunctionExpressionParser.java b/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunctionExpressionParser.java deleted file mode 100644 index b1db132..0000000 --- a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunctionExpressionParser.java +++ /dev/null @@ -1,151 +0,0 @@ -package cn.hangsman.operationlog.spel; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.*; -import org.springframework.context.expression.BeanFactoryResolver; -import org.springframework.context.expression.MethodBasedEvaluationContext; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.expression.*; -import org.springframework.expression.common.LiteralExpression; -import org.springframework.expression.common.TemplateAwareExpressionParser; -import org.springframework.expression.common.TemplateParserContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -import java.lang.reflect.Method; -import java.util.*; - -/** - * Created by 2022/1/12 9:58 - * - * @author hangsman - * @since 1.0 - */ -@Slf4j -public class SpelFunctionExpressionParser extends TemplateAwareExpressionParser implements SmartInitializingSingleton, BeanFactoryAware { - - private static final String FUNCTION_VARIABLE_PREFIX = "$"; - - private final LiteralExpression EMPTY_EXPRESSION = new LiteralExpression(""); - - private final ParserContext functionParserContext = new TemplateParserContext("(", ")"); - - private final ParserContext templateParserContext = new TemplateParserContext("{", "}"); - - private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); - - private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - - private SpelFunctionFactory functionFactory; - - private BeanFactory beanFactory; - - public EvaluationContext createEvaluationContext(Method method, Object[] arguments) { - MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( - TypedValue.NULL, method, arguments, this.parameterNameDiscoverer); - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } - return evaluationContext; - } - - @Override - public Expression parseExpression(String expressionString) throws ParseException { - if (!StringUtils.hasText(expressionString)) { - return EMPTY_EXPRESSION; - } - try { - return this.parseExpression(expressionString, templateParserContext); - } catch (Exception e) { - log.error("parse expression error : {}", expressionString); - throw e; - } - } - - @Override - protected Expression doParseExpression(String expressionString, ParserContext context) throws ParseException { - if (expressionString.contains(FUNCTION_VARIABLE_PREFIX)) { - return doParseFunctionExpression(expressionString, functionParserContext); - } else { - return spelExpressionParser.parseExpression(expressionString); - } - } - - private Expression doParseFunctionExpression(String expressionString, ParserContext context) { - String prefix = context.getExpressionPrefix(); - String suffix = context.getExpressionSuffix(); - Map variableExpressionMap = new HashMap<>(); - int startIdx = 0; - while (startIdx < expressionString.length()) { - int $Index = expressionString.indexOf(FUNCTION_VARIABLE_PREFIX, startIdx); - int prefixIndex = expressionString.indexOf(prefix, $Index); - if (prefixIndex >= startIdx) { - String functionName = expressionString.substring($Index + FUNCTION_VARIABLE_PREFIX.length(), prefixIndex); - Assert.hasLength(functionName, "functionName can not be empty:" + expressionString); - int afterPrefixIndex = prefixIndex + prefix.length(); - int suffixIndex = SpelUtil.skipToCorrectEndSuffix(suffix, expressionString, afterPrefixIndex); - if (suffixIndex == -1) { - throw new ParseException(expressionString, prefixIndex, - "No ending suffix '" + suffix + "' for expression starting at character " + - prefixIndex + ": " + expressionString.substring(prefixIndex)); - } - if (suffixIndex == afterPrefixIndex) { - throw new ParseException(expressionString, prefixIndex, - "No expression defined within delimiter '" + prefix + suffix + - "' at character " + prefixIndex); - } - String expr = expressionString.substring(prefixIndex + prefix.length(), suffixIndex); - if (expr.isEmpty()) { - throw new ParseException(expressionString, prefixIndex, - "No expression defined within delimiter '" + prefix + suffix + - "' at character " + prefixIndex); - } - List expressions = new ArrayList<>(); - for (String spel : expr.split(",")) { - expressions.add(doParseExpression(spel, null)); - } - String expressionStr = FUNCTION_VARIABLE_PREFIX + functionName + prefix + expr + suffix; - SpelFunction function = functionFactory.getFunction(functionName); - Assert.notNull(function, "could not find function :" + functionName); - FunctionExpression functionExpression = new FunctionExpression(expressionStr, expressions.toArray(new Expression[0]), function); - String variableId = UUID.randomUUID().toString().replace("-", ""); - variableExpressionMap.put("function_" + variableId, functionExpression); - startIdx = suffixIndex + suffix.length(); - } else { - break; - } - } - String originExpressionString = expressionString; - for (String key : variableExpressionMap.keySet()) { - Expression expression = variableExpressionMap.get(key); - expressionString = expressionString.replace(expression.getExpressionString(), "#" + key); - } - Expression proxyExpression = doParseExpression(expressionString, null); - return new FunctionExpressionProxy(originExpressionString, proxyExpression, variableExpressionMap); - } - - @Override - public void afterSingletonsInstantiated() { - try { - setFunctionFactory(this.beanFactory.getBean(SpelFunctionFactory.class)); - } catch (NoUniqueBeanDefinitionException ex) { - throw new IllegalStateException("no unique bean of type " + - "SpelFunctionFactory found. Mark one as primary or declare a specific SpelFunctionFactory to use.", ex); - } catch (NoSuchBeanDefinitionException ex) { - throw new IllegalStateException("no bean of type SpelFunctionFactory found. " + - "Register a SpelFunctionFactory bean or remove the @EnableOperationLog annotation from your configuration.", ex); - } - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - public void setFunctionFactory(SpelFunctionFactory functionFactory) { - this.functionFactory = functionFactory; - } -} diff --git a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunctionFactory.java b/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunctionFactory.java deleted file mode 100644 index 51496a2..0000000 --- a/operation-log/src/main/java/cn/hangsman/operationlog/spel/SpelFunctionFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.hangsman.operationlog.spel; - -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Created by 2022/1/12 11:16 - * - * @author hangsman - * @since 1.0 - */ -public class SpelFunctionFactory { - - private final Map functionMap = new HashMap<>(); - - public SpelFunctionFactory(List functions) { - if (!CollectionUtils.isEmpty(functions)) { - for (SpelFunction parseFunction : functions) { - String functionName = parseFunction.functionName(); - Assert.hasLength(functionName, "functionName can not be empty!"); - functionMap.put(functionName, parseFunction); - } - } - } - - public SpelFunction getFunction(String functionName) { - return this.functionMap.get(functionName); - } - - -} diff --git a/pom.xml b/pom.xml index 51b82cb..31b7a58 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ operation-log-parent 1.0.0 - operation-log + operation-log-core operation-log-spring-boot-autoconfigure operation-log-spring-boot-starter operation-log-test @@ -67,7 +67,7 @@ cn.hangsman.operationlog - operation-log + operation-log-core 1.0.0