4.2.2 解析逻辑
模板解析Spring 3 中提供了一个非常强大的功能:SpEL,SpEL 在 Spring 产品中是作为表达式求值的核心基础模块,它本身是可以脱离 Spring 独立使用的。举个例子:
public static void main(String[] args) { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("#root.purchaseName"); Order order = new Order(); order.setPurchaseName("张三"); System.out.println(expression.getValue(order)); }
这个方法将打印 “张三”。LogRecord 解析的类图如下:
解析核心类:LogRecordValueParser
里面封装了自定义函数和 SpEL 解析类 LogRecordExpressionEvaluator
。
public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator { private Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64); private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64); public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { return getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class); } }
LogRecordExpressionEvaluator
继承自 CachedExpressionEvaluator
类,这个类里面有两个 Map,一个是 expressionCache 一个是 targetMethodCache。在上面的例子中可以看到,SpEL 会解析成一个 Expression 表达式,然后根据传入的 Object 获取到对应的值,所以 expressionCache 是为了缓存方法、表达式和 SpEL 的 Expression 的对应关系,让方法注解上添加的 SpEL 表达式只解析一次。下面的 targetMethodCache 是为了缓存传入到 Expression 表达式的 Object。核心的解析逻辑是上面最后一行代码。
getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class);
getExpression
方法会从 expressionCache 中获取到 @LogRecordAnnotation 注解上的表达式的解析 Expression 的实例,然后调用 getValue
方法,getValue
传入一个 evalContext 就是类似上面例子中的 order 对象。其中 Context 的实现将会在下文介绍。日志上下文实现下面的例子把变量放到了 LogRecordContext 中,然后 SpEL 表达式就可以顺利的解析方法上不存在的参数了,通过上面的 SpEL 的例子可以看出,要把方法的参数和 LogRecordContext 中的变量都放到 SpEL 的 getValue
方法的 Object 中才可以顺利的解析表达式的值。下面看看如何实现:
@LogRecord(content = "修改了订单的配送员:从“{deveryUser{#oldDeliveryUserId}}”, 修改到“{deveryUser{#request.getUserId()}}”", bizNo="#request.getDeliveryOrderNo()") public void modifyAddress(updateDeliveryRequest request){ // 查询出原来的地址是什么 LogRecordContext.putVariable("oldDeliveryUserId", DeliveryService.queryOldDeliveryUserId(request.getDeliveryOrderNo())); // 更新派送信息 电话,收件人、地址 doUpdate(request); }
在 LogRecordValueParser 中创建了一个 EvaluationContext,用来给 SpEL 解析方法参数和 Context 中的变量。相关代码如下:
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass, ret, errorMsg, beanFactory);
在解析的时候调用 getValue
方法传入的参数 evalContext,就是上面这个 EvaluationContext 对象。下面是 LogRecordEvaluationContext 对象的继承体系:
LogRecordEvaluationContext 做了三个事情:
- 把方法的参数都放到 SpEL 解析的 RootObject 中。
- 把 LogRecordContext 中的变量都放到 RootObject 中。
- 把方法的返回值和 ErrorMsg 都放到 RootObject 中。
LogRecordEvaluationContext 的代码如下:
public class LogRecordEvaluationContext extends MethodBasedEvaluationContext { public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments, ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg) { //把方法的参数都放到 SpEL 解析的 RootObject 中 super(rootObject, method, arguments, parameterNameDiscoverer); //把 LogRecordContext 中的变量都放到 RootObject 中 Map<String, Object> variables = LogRecordContext.getVariables(); if (variables != null && variables.size() > 0) { for (Map.Entry<String, Object> entry : variables.entrySet()) { setVariable(entry.getKey(), entry.getValue()); } } //把方法的返回值和 ErrorMsg 都放到 RootObject 中 setVariable("_ret", ret); setVariable("_errorMsg", errorMsg); } }
下面是 LogRecordContext 的实现,这个类里面通过一个 ThreadLocal 变量保持了一个栈,栈里面是个 Map,Map 对应了变量的名称和变量的值。
public class LogRecordContext { private static final InheritableThreadLocal<Stack<Map<String, Object>>> variableMapStack = new InheritableThreadLocal<>(); //其他省略.... }
上面使用了 InheritableThreadLocal,所以在线程池的场景下使用 LogRecordContext 会出现问题,如果支持线程池可以使用阿里巴巴开源的 TTL 框架。那这里为什么不直接设置一个 ThreadLocal<Map<String, Object>> 对象,而是要设置一个 Stack 结构呢?我们看一下这么做的原因是什么。
@LogRecord(content = "修改了订单的配送员:从“{deveryUser{#oldDeliveryUserId}}”, 修改到“{deveryUser{#request.getUserId()}}”", bizNo="#request.getDeliveryOrderNo()") public void modifyAddress(updateDeliveryRequest request){ // 查询出原来的地址是什么 LogRecordContext.putVariable("oldDeliveryUserId", DeliveryService.queryOldDeliveryUserId(request.getDeliveryOrderNo())); // 更新派送信息 电话,收件人、地址 doUpdate(request); }
上面代码的执行流程如下:
看起来没有什么问题,但是使用 LogRecordAnnotation 的方法里面嵌套了另一个使用 LogRecordAnnotation 方法的时候,流程就变成下面的形式:
可以看到,当方法二执行了释放变量后,继续执行方法一的 logRecord 逻辑,此时解析的时候 ThreadLocal<Map<String, Object>>的 Map 已经被释放掉,所以方法一就获取不到对应的变量了。方法一和方法二共用一个变量 Map 还有个问题是:如果方法二设置了和方法一相同的变量两个方法的变量就会被相互覆盖。所以最终 LogRecordContext 的变量的生命周期需要是下面的形式:
LogRecordContext 每执行一个方法都会压栈一个 Map,方法执行完之后会 Pop 掉这个 Map,从而避免变量共享和覆盖问题。默认操作人逻辑在 LogRecordInterceptor 中 IOperatorGetService 接口,这个接口可以获取到当前的用户。下面是接口的定义:
public interface IOperatorGetService { /** * 可以在里面外部的获取当前登陆的用户,比如 UserContext.getCurrentUser() * * @return 转换成Operator返回 */ Operator getUser(); }
下面给出了从用户上下文中获取用户的例子:
public class DefaultOperatorGetServiceImpl implements IOperatorGetService { @Override public Operator getUser() { //UserUtils 是获取用户上下文的方法 return Optional.ofNullable(UserUtils.getUser()) .map(a -> new Operator(a.getName(), a.getLogin())) .orElseThrow(()->new IllegalArgumentException("user is null")); } }
组件在解析 operator 的时候,就判断注解上的 operator 是否是空,如果注解上没有指定,我们就从 IOperatorGetService 的 getUser 方法获取了。如果都获取不到,就会报错。
String realOperatorId = ""; if (StringUtils.isEmpty(operatorId)) { if (operatorGetService.getUser() == null || StringUtils.isEmpty(operatorGetService.getUser().getOperatorId())) { throw new IllegalArgumentException("user is null"); } realOperatorId = operatorGetService.getUser().getOperatorId(); } else { spElTemplates = Lists.newArrayList(bizKey, bizNo, action, operatorId, detail); }
自定义函数逻辑自定义函数的类图如下:
下面是 IParseFunction 的接口定义:executeBefore
函数代表了自定义函数是否在业务代码执行之前解析,上面提到的查询修改之前的内容。
public interface IParseFunction { default boolean executeBefore(){ return false; } String functionName(); String apply(String value); }
ParseFunctionFactory 的代码比较简单,它的功能是把所有的 IParseFunction 注入到函数工厂中。
public class ParseFunctionFactory { private Map<String, IParseFunction> allFunctionMap; public ParseFunctionFactory(List<IParseFunction> parseFunctions) { if (CollectionUtils.isEmpty(parseFunctions)) { return; } allFunctionMap = new HashMap<>(); for (IParseFunction parseFunction : parseFunctions) { if (StringUtils.isEmpty(parseFunction.functionName())) { continue; } allFunctionMap.put(parseFunction.functionName(), parseFunction); } } public IParseFunction getFunction(String functionName) { return allFunctionMap.get(functionName); } public boolean isBeforeFunction(String functionName) { return allFunctionMap.get(functionName) != null && allFunctionMap.get(functionName).executeBefore(); } }
DefaultFunctionServiceImpl 的逻辑就是根据传入的函数名称 functionName 找到对应的 IParseFunction,然后把参数传入到 IParseFunction 的 apply
方法上最后返回函数的值。
public class DefaultFunctionServiceImpl implements IFunctionService { private final ParseFunctionFactory parseFunctionFactory; public DefaultFunctionServiceImpl(ParseFunctionFactory parseFunctionFactory) { this.parseFunctionFactory = parseFunctionFactory; } @Override public String apply(String functionName, String value) { IParseFunction function = parseFunctionFactory.getFunction(functionName); if (function == null) { return value; } return function.apply(value); } @Override public boolean beforeFunction(String functionName) { return parseFunctionFactory.isBeforeFunction(functionName); } }