基于AOP+SPEL 自定义日志组件

一、编写背景

         1. 日常开发中,我们在查看业务日志的时候,会发现需要在一堆与业务无关的系统组件输出的日志中去找我们为特定业务功能所打印的关键日志信息,为了快速查找业务日志,我们可以采用在配置logback.xml日志的时候,将业务日志和系统日志分别输出到不同的文件中

        2. 我们在编写代码的时候,我们通常把打印日志的代码嵌入到业务代码中,如果后续要对日志内容修改的时候,修改起来会变得很繁琐,为了解决这一问题,我们可以将一些内容相同的日志抽取出来,以注解的形式添加在底层调用方法上,这样每次只要调用该方法对应的日志就会输出到响应的日志文件中

例如:

        a). 通常我们会将日志打印代码写在业务代码中

//业务日志输出到businessLog.log文件中    
private final Logger businessLog = LoggerFactory.getLogger("businessLog");

    @Override
//    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(LogRecord logRecord) {

        record(logRecord.getMessage());
    }

    @Override
    public void record(String msg) {
        if (businessLog.isInfoEnabled()) {
            businessLog.info("{}", msg);
        }

        //业务代码
        //TODO
    }

        b). 以下是我们修改一个菜单的业务代码,日志打印嵌套在业务代码中,controller层调用menu的service层修改某个菜单信息

	/**
     * 修改
     * @param sysMenuDTO
     * @return
     */
    @PostMapping("/update")
    public ResponseEntity<?> update(@RequestBody SysMenuDTO sysMenuDTO) {
        if(log.isInfoEnabled()) {
            log.info("update SysMenu start....");
			log.info("request param:{}", JSON.toJSONString(sysMenuDTO));
        }

        SysMenu oldMenu = this.sysMenuService.get(sysMenuDTO.getMenuId());
        log.info("{}修改了菜单【{}】", UserUtil.getUserName(), oldMenu.getMenuName());

		try{
            final int count = this.sysMenuService.update(sysMenuDTO);
            log.info("修改后的菜单名为【{}】,业务流水号:{}", sysMenuDTO.getMenuName(), SysHeadUtil.getSeqNo());
            return CommonResultUtil.success("更新成功", count);
        
        } catch (Exception e) {
            log.error("error:", e);
            throw e;
        }

    }

        c). 日志打印与业务代码分离的方式

                controller 层

	/**
     * 修改
     * @param sysMenuDTO
     * @return
     */
    @PostMapping("/update")
    @LogRecord(content = "update SysMenu start...\nrequest param:#{T(com.alibaba.fastjson.JSON).toJSONString(#sysMenuDTO)}", bizNo="")
    public ResponseEntity<?> update(@RequestBody SysMenuDTO sysMenuDTO) {
		try{
            return CommonResultUtil.success("更新成功", this.sysMenuService.update(sysMenuDTO));
        } catch (Exception e) {
            log.error("error:", e);
            throw e;
        }

    }

                 service interface,我们通过注解的方式,编写固定的日志模板,任何调用该接口修改菜单方法的代码都不需要在业务代码中嵌入日志打印代码

	/**
	* 修改
	* @param sysMenu 修改请求体DTO
	* @return 成功更新个数
	**/
	@LogRecord(content = "修改了菜单【#{@{getOldMenuName(#sysMenuDTO.menuId)}}】," +
			"修改后的菜单名为【#{#sysMenuDTO.menuName}}】",
			operator = "#{T(com.lkyl.UserUtil).getUserName()}",
			bizNo = "#{T(com.lkyl.SysHeadUtil).getSeqNo()}")
	int update(SysMenuDTO sysMenuDTO);

二、技术实现

        接下来我们来讨论,如何从技术层面实现通过注解打印日志这一功能

        首先我们需要了解面向切面编程知识,也就是AOP(aspect oriented programming),以及了解SPEL(spring expression language)知识

        1. 使用AOP

        Advisor 是 Spring AOP 对 Advice 和 Pointcut 的抽象,可以理解为“执行通知者”,一个 Pointcut (一般对应方法)和用于“增强”它的 Advice 共同组成这个方法的一个 Advisor

        1.1  我们自定义一个@LogRecord注解,这个注解作用在方法上,对应的类需要注入到spring的bean容器中

        1.2 @LogRecord

package com.lkyl.oceanframework.log.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord {
    //日志模板
    String content();

    String fail() default "";
    //操作员,默认为空,则调用默认实现的operatorService
    String operator() default "";
    //业务流水号
    String bizNo();

    String category() default "";

    String detail() default "";

    String condition() default "";
}

   1.3  注:摘自美团技术文档,链接请至文章末尾查看。

        业务中的 AOP 逻辑大部分是使用 @Aspect 注解实现的,但是基于注解的 AOP 在 Spring boot 1.5 中兼容性是有问题的,组件为了兼容 Spring boot1.5 的版本我们手工实现 Spring 的 AOP 逻辑。

                切面选择 AbstractBeanFactoryPointcutAdvisor 实现,切点是通过 StaticMethodMatcherPointcut 匹配包含 LogRecord 注解的方法。通过实现 MethodInterceptor 接口实现操作日志的增强逻辑。

        1.4  定义一个Advisor

        本例中的advisor继承了AbstractBeanFactoryPointcutAdvisor抽象类 ,该抽象类基于提供的 adviceBeanName 从容器中获取 Advice,在向beanFactory工厂注入实例时,可以指定adviceName,当执行具体的切面代码时,AbstractBeanFactoryPointcutAdvisor会从beanFactory工厂通过adviceName获取具体的bean对像,如果手动set了advice,那么在执行advice逻辑时会优先获取手动set的advice,本例中注入advisor的时候set了一个advice。

   在下面的advisor中指定了它的pointCut

package com.lkyl.oceanframework.log.adviser;

import com.lkyl.oceanframework.log.parser.LogRecordOperationSource;
import com.lkyl.oceanframework.log.pointcut.LogRecordPointcut;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor;

/**
 * TODO
 *
 * @version 1.0
 * @author: nicholas
 * @createTime: 2022年05月28日 12:47
 */
public class BeanFactoryLogRecordAdvisor extends AbstractBeanFactoryPointcutAdvisor {

    private LogRecordOperationSource logRecordOperationSource;
    @Override
    public Pointcut getPointcut() {
        LogRecordPointcut logRecordPointcut = new LogRecordPointcut();
        logRecordPointcut.setLogRecordOperationSource(logRecordOperationSource);
        return logRecordPointcut;
    }

    public void setLogRecordOperationSource(LogRecordOperationSource logRecordOperationSource) {
        this.logRecordOperationSource = logRecordOperationSource;
    }
}

         1.5 定义一个PointCut 对于point我们继承了StaticMethodMatcherPointcut 

        静态方法切点:org.springframework.aop.support.StaticMethodMatcerPointcut是静态方法切点的抽象基类,默认情况下它匹配所有的类。StaticMethodMatcherPointcut包括两个主要的子类 分别是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者提供简单字符串匹配方法前面,而后者使用正则表达式匹配方法前面。 动态方法切点:org.springframework.aop.support.DynamicMethodMatcerPointcut是动态方法切点的抽象基类,默认情况下它匹配所有的类,DynamicMethodMatcerPointcut已经过时, 可以使用DefaultPointcutAdvisor和DynamicMethodMatcherPointcut动态方法代替

        

package com.lkyl.oceanframework.log.pointcut;

import com.lkyl.oceanframework.log.parser.LogRecordOperationSource;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.lang.NonNull;
import org.springframework.util.CollectionUtils;

import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * @author nicholas
 */
public class LogRecordPointcut extends StaticMethodMatcherPointcut implements Serializable {
    // LogRecord的解析类
    private LogRecordOperationSource logRecordOperationSource;

    @Override
    public boolean matches(@NonNull Method method, @NonNull Class<?> targetClass) {
          // 解析 这个 method 上有没有 @LogRecord 注解,有的话会解析出来注解上的各个参数
        return !CollectionUtils.isEmpty(logRecordOperationSource.computeLogRecordOperations(method, targetClass));
    }

    public void setLogRecordOperationSource(LogRecordOperationSource logRecordOperationSource) {
        this.logRecordOperationSource = logRecordOperationSource;
    }
}

`        1.6  定义一个Advice增强器

package com.lkyl.oceanframework.log.interceptor;

import com.lkyl.oceanframework.common.utils.exception.CommonException;
import com.lkyl.oceanframework.log.context.LogRecordContext;
import com.lkyl.oceanframework.log.enums.LogRecordEnum;
import com.lkyl.oceanframework.log.evaluator.LogRecordExpressionEvaluator;
import com.lkyl.oceanframework.log.options.LogRecordOps;
import com.lkyl.oceanframework.log.parser.LogRecordOperationSource;
import com.lkyl.oceanframework.log.result.MethodExecuteResult;
import com.lkyl.oceanframework.log.service.IFunctionService;
import com.lkyl.oceanframework.log.service.ILogRecordService;
import com.lkyl.oceanframework.log.service.IOperatorGetService;
import com.lkyl.oceanframework.log.spelExt.ops.SelfFunctionReference;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.CompositeStringExpression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.stream.Stream;

import static org.springframework.aop.support.AopUtils.getTargetClass;

/**
 * TODO
 *
 * @version 1.0
 * @author: nicholas
 * @createTime: 2022年05月28日 13:50
 */
@Slf4j
public class LogRecordInterceptor implements MethodInterceptor , BeanFactoryAware {

    private LogRecordExpressionEvaluator expressionEvaluator;

    private LogRecordOperationSource logRecordOperationSource;

    @Resource
    private IFunctionService iFunctionService;

    private BeanFactory beanFactory;

    private String tenantId;

    @Resource
    private ILogRecordService iLogRecordService;

    @Resource
    private IOperatorGetService iOperatorGetService;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        // 记录日志
        return execute(invocation, invocation.getThis(), method, invocation.getArguments());
    }

    private Object execute(MethodInvocation invoker, Object target, Method method, Object[] args) throws Throwable {
        Class<?> targetClass = getTargetClass(target);
        AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(method, targetClass);
        LogRecordContext.putEmptySpan();
        expressionEvaluator = new LogRecordExpressionEvaluator();
        EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, target,
                null, null, beanFactory);
        Object ret = null;
        MethodExecuteResult methodExecuteResult = new MethodExecuteResult(true, null, "");

        Collection<LogRecordOps> operations = new ArrayList<>();
        Map<String, String> functionNameAndReturnMap = new HashMap<>();
        try {
            operations = logRecordOperationSource.computeLogRecordOperations(method, targetClass);
            List<Expression> selfFunExp = getBeforeExecuteFunctionTemplate(operations);
            //业务逻辑执行前的自定义函数解析
            functionNameAndReturnMap = processBeforeExecuteFunctionTemplate(selfFunExp, annotatedElementKey, evaluationContext);
        } catch (Exception e) {
            log.error("log record parse before function exception", e);
        }
        try {
            ret = invoker.proceed();
        } catch (Exception e) {
            methodExecuteResult = new MethodExecuteResult(false, e, e.getMessage());
        }
        try {
            if (!CollectionUtils.isEmpty(operations)) {
                recordExecute(annotatedElementKey, ret, operations, methodExecuteResult.isSuccess(), methodExecuteResult.getErrorMsg(),
                        functionNameAndReturnMap, evaluationContext);
            }
        } catch (Exception t) {
            //记录日志错误不要影响业务
            log.error("log record parse exception", t);
        } finally {
            LogRecordContext.clear();
        }
        if (methodExecuteResult.throwable != null) {
            throw methodExecuteResult.throwable;
        }
        return ret;
    }

    public void setTenantId(String tenantId) {
        this.tenantId = tenantId;
    }

    public void  setLogRecordOperationSource(LogRecordOperationSource logRecordOperationSource) {
        this.logRecordOperationSource = logRecordOperationSource;
    }

    public void setFunctionService(IFunctionService iFunctionService) {
        this.iFunctionService = iFunctionService;
    }

    //记录日志
    private void recordExecute(AnnotatedElementKey annotatedElementKey, Object ret, Collection<LogRecordOps> operations,
                               boolean isSuccess, String errorMsg, Map<String, String> functionNameAndReturnMap,
                               EvaluationContext evaluationContext) {
        evaluationContext.setVariable("_ret", ret);
        if (!isSuccess) {
            evaluationContext.setVariable("_errorMsg", errorMsg);
        }
        //get operator name
        String userName;
        Optional<LogRecordOps> operator = operations.stream().takeWhile(ops -> StringUtils.equals(LogRecordEnum.OPERATOR.getAttrName(),
                ops.getLogRecordEnum().getAttrName())).findFirst();
        if(operator.isPresent()) {
            userName = this.expressionEvaluator.parseExpression(operator.get().getExpressString(), annotatedElementKey, evaluationContext);
        } else {
            userName = iOperatorGetService.getUser().getOperatorName();
        }
        //content
        StringBuilder contentStr = new StringBuilder(userName);
        Optional<LogRecordOps> content = operations.stream().takeWhile(ops -> StringUtils.equals(LogRecordEnum.CONTENT.getAttrName(),
                ops.getLogRecordEnum().getAttrName())).findFirst();
        if (!content.isPresent()) {
           throw new CommonException("parse @LogRecord content error, content is blank");
        }
         if (content.get().getExpression() instanceof CompositeStringExpression) {
             Stream.of(((CompositeStringExpression) content.get().getExpression()).getExpressions()).forEach(exp -> {
                 if (exp instanceof LiteralExpression) {
                     contentStr.append(((LiteralExpression) exp).getValue());
                 }
                 else {
                     String cachedValue = null;
                     if(((SpelExpression) exp).getAST() instanceof SelfFunctionReference ) {
                         cachedValue = functionNameAndReturnMap.
                                 get(((SelfFunctionReference) ((SpelExpression) exp).getAST()).getSelfFunctionName());
                     }
                     contentStr.append(cachedValue == null ? expressionEvaluator.parseExpression(exp.getExpressionString(),
                             annotatedElementKey, evaluationContext) : cachedValue);
                 }
             });
         } else {
             contentStr.append(expressionEvaluator.parseExpression(content.get().getExpressString(), annotatedElementKey, evaluationContext));
         }
        Optional<LogRecordOps> bizNo = operations.stream().takeWhile(ops -> StringUtils.equals(LogRecordEnum.BIZ_NO.getAttrName(),
                ops.getLogRecordEnum().getAttrName())).findFirst();
         if(bizNo.isPresent()) {
             contentStr.append(expressionEvaluator.parseExpression(bizNo.get().getExpressString(), annotatedElementKey, evaluationContext));
         }


//        LogRecord logRecord = new LogRecord(Logger.getLogger(LogRecordInterceptor.class.getName()).getLevel(), contentStr.toString());
        iLogRecordService.record(contentStr.toString());
    }

    /**
     * 目前支持第一层自定义函数提取
     * @param operations    注解中的content, operator,bizNo封装类
     * @return  预执行的自定义函数对应的expression
     */
    private List<Expression> getBeforeExecuteFunctionTemplate(Collection<LogRecordOps> operations) {
        List<Expression> selfFunExeBeforeExp = new ArrayList<>();
        operations.stream().filter(ops-> (ops.getExpression() instanceof CompositeStringExpression)
                || (ops.getExpression() instanceof SpelExpression)).map(LogRecordOps::getExpression).forEach(exp -> {
            if (exp instanceof CompositeStringExpression) {
                Stream.of(((CompositeStringExpression) exp).getExpressions()).forEach(subItem->{
                    if((subItem instanceof SpelExpression)
                            && (((SpelExpression) subItem).getAST() instanceof SelfFunctionReference)) {
                        selfFunExeBeforeExp.add(subItem);
                    }
                });
            } else if(((SpelExpression)exp).getAST() instanceof SelfFunctionReference){
                selfFunExeBeforeExp.add(exp);
            }
        });
        return selfFunExeBeforeExp;
    }

    private Map<String, String> processBeforeExecuteFunctionTemplate(List<Expression> selfFunExp,
                                                                     AnnotatedElementKey annotatedElementKey,
                                                                     EvaluationContext evaluationContext) {
        Map<String, String> selfNameValueMap = new HashMap<>(3);
        selfFunExp.stream().forEach(exp->{
            String funName = ((SelfFunctionReference)(((SpelExpression)exp).getAST())).getSelfFunctionName();
            if (iFunctionService.beforeFunction(funName)) {
                selfNameValueMap.put(funName,
                        expressionEvaluator.parseExpression(exp.getExpressionString(), annotatedElementKey, evaluationContext));
            }
        });
        return selfNameValueMap;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }


}

2.  SPEL

        运行时将对象的变量进行解析及操作,支持大部分基本的java语言语法,同时支持静态方法及非静态方法的调用,也可以对bean的变量及方法进行操作,支持模板化表达式解析,我们在本例中就会用到模板化表达式解析,对SPEL不了解的同学建议先参考链接:https://blog.csdn.net/qq_21579619/article/details/123078778https://blog.csdn.net/qq_21579619/article/details/123078778

在模板化的表达式中,具体的spel表达式由#{}来定位,我们知道SPEL表达式的解析依赖于Context,对于spel表达式中出现的变量、方法等会去context获取,所以第一步我们需要有一个context,第二步我们对现有的spel解析类型中新增自定义函数解析类型,第三步实现解析类parser

        2.1 接下来我们先来看一下自定义函数的使用然后再进行三部曲的讲解,我们需要SPEL表达式中嵌入我们自定义的函数,输出我们想要的日志内容,比如,我们的方法参数只有一个变量String code的码值,但是在日志输出的时候我们想要看到的是它的中文含义,这时我们就需要自定义一个函数,专门做中文转义,目前spring表达式提供的方法及bean的解析不能更有效的实现我们自定义函数解析日志的需求,所以后面我们会对spel进行一定的扩展以便支持对我们自己定义的函数的解析

        如下是在不使用自定义函数的日志解析

@LogRecord(content = "【#{#code}】对应的级别已被调整为:【#{#level}】", bizNo = "#{T(com.tc.HeadUtil).getSeqNo()}")
public int updateLevel(String code, String level) {

    //更新了code对应的级别
    ...
}

       我们的日志打印出来看到的是包含码值的日志信息,不是很直观

        2.2  我们定义了一个函数getNameByCode(String code), 在模板解析时,会将code对应的中文输出,如下

@LogRecord(content = "【#{@{getNameByCode(#code)}}】对应的级别已被调整为:【#{#level}】", bizNo = "#{T(com.tc.HeadUtil).getSeqNo()}")
public int updateLevel(String code, String level) {

    //更新了code对应的级别
    ...
}

        2.3 我们可以使用TemplateParserContext在日志文本中嵌入我们的SPEL表达式

                2.3.1 可以自定义spel表达式前缀和后缀,默认前缀为“#{”,后缀为“}”

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.expression.common;

import org.springframework.expression.ParserContext;

public class TemplateParserContext implements ParserContext {
    private final String expressionPrefix;
    private final String expressionSuffix;

    public TemplateParserContext() {
        this("#{", "}");
    }

    public TemplateParserContext(String expressionPrefix, String expressionSuffix) {
        this.expressionPrefix = expressionPrefix;
        this.expressionSuffix = expressionSuffix;
    }

    public final boolean isTemplate() {
        return true;
    }

    public final String getExpressionPrefix() {
        return this.expressionPrefix;
    }

    public final String getExpressionSuffix() {
        return this.expressionSuffix;
    }
}

        3. 本例中解析SPEL对应的Context,SelfFunction,Parser实现

                3.1 定义一个Context,因为我们的@LogRecord注解是作用在方法上的,所以下面的context继承了spring spel中的MethodBasedEvaluationContext,MethodBasedEvaluationContext是在StandardEvaluationContext的基础上支持当前方法参数的直接调用

package com.lkyl.oceanframework.log.context;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.ParameterNameDiscoverer;

import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author nicholas
 */
public class LogRecordEvaluationContext extends MethodBasedEvaluationContext {

    public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments,
                                      ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg, BeanFactory beanFactory) {
       //把方法的参数都放到 SpEL 解析的 RootObject 中
       super(rootObject, method, arguments, parameterNameDiscoverer);
       //设置beanFactory
        setBeanResolver(new BeanFactoryResolver(beanFactory));
       //把 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);
    }
}

                3.2 实现自定义函数功能

                UML类图                        

                  首先我们定义一个IParseFunction接口如下,每一个自定义函数包含接口名称functionName,及处理方法apply(),为了更具灵活性可以使用方法executeBefore()控制自定义函数的执行顺序,如业务方法执行前解析,还是业务方法执行完后解析

package com.lkyl.oceanframework.log.function;

import com.lkyl.oceanframework.log.enums.ParseFunctionEnum;

public interface IParseFunction<T>{

  default boolean executeBefore(){
    return false;
  }

  default String functionName(){
    return ParseFunctionEnum.DEFAULT_FUNCTION.getFunctionName();
  }

  String apply(T value);
}

         然后,我们使用工厂设计模式来将所有的自定义函数装入工厂中,如下

package com.lkyl.oceanframework.log.factory;

import com.lkyl.oceanframework.common.utils.utils.CollectionUtils;
import com.lkyl.oceanframework.common.utils.utils.ObjectUtils;
import com.lkyl.oceanframework.log.function.IParseFunction;
import org.apache.commons.lang3.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author nicholas
 */
public class ParseFunctionFactory {
  private Map<String, IParseFunction> allFunctionMap;

  public ParseFunctionFactory(List<IParseFunction> parseFunctions) {
    if (CollectionUtils.isEmpty(parseFunctions)) {
      return;
    }
    allFunctionMap = new HashMap<>();
    parseFunctions.stream().filter(e->StringUtils.isNotBlank(e.functionName())).forEach(iParseFunction -> {
      if(ObjectUtils.isNotEmpty(allFunctionMap.get(iParseFunction.functionName()))) {
        throw new RuntimeException("create parseFunctionFactory error, duplicate functionName found: " + iParseFunction.functionName() + ".");
      }
      allFunctionMap.put(iParseFunction.functionName(), iParseFunction);
    });
  }

  public IParseFunction getFunction(String functionName) {
    return allFunctionMap.get(functionName);
  }

  public boolean isBeforeFunction(String functionName) {
    return allFunctionMap.get(functionName) != null && allFunctionMap.get(functionName).executeBefore();
  }
}

        接着我们定义一个执行自定义函数的统一调用service,如下

        

package com.lkyl.oceanframework.log.service;


/**
 * @author nicholas
 */
public interface IFunctionService {

     String apply(String functionName, Object value);

     boolean beforeFunction(String functionName);
}

        

package com.lkyl.oceanframework.log.service.impl;

import com.lkyl.oceanframework.log.factory.ParseFunctionFactory;
import com.lkyl.oceanframework.log.function.IParseFunction;
import com.lkyl.oceanframework.log.service.IFunctionService;

/**
 * @author nicholas
 */
public class DefaultFunctionServiceImpl implements IFunctionService {

  private final ParseFunctionFactory parseFunctionFactory;

  public DefaultFunctionServiceImpl(ParseFunctionFactory parseFunctionFactory) {
    this.parseFunctionFactory = parseFunctionFactory;
  }

  @Override
  public String apply(String functionName, Object value) {
    IParseFunction function = parseFunctionFactory.getFunction(functionName);
    if (function == null) {
      return value.toString();
    }
    return function.apply(value);
  }

  @Override
  public boolean beforeFunction(String functionName) {
    return parseFunctionFactory.isBeforeFunction(functionName);
  }
}

            3.3 定义一个Parser,扩展spel支持selfFunction

                首先介绍下SPEL解析过程,例如  

    @Test
    public void test() {
        // 1 定义解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 此处未具体set rootObject,方法getRandom()作为举例用
        String str = "1 + #getRandom(#arg1, #arg2, #arg3) = 2";
        // 指定表达式
        Expression exp = parser.parseExpression(str);
        //输出为true
        System.out.println(exp.getValue(context));
    }

                spel默认的InternalSpelExpressionParser在解析表达式字符串时,首先识别“1”为Int类型节点,作为根节点,然后识别“+”为运算符类型节点,并将左边的Int类型节点作为其左叶子节点,紧接着识别“#getRandom(#arg1, #arg2, #arg3)”,getRandom被作为一个方法类型节点,其三个参数作为它的叶子节点,而getRandom方法类型节点被作为“+”运算符类型节点的右叶子节点,以此类推,将上述str字符串解析为森林树状结构

                具体解析过程需要阅读源码了解,下面是我们的Parser实现类,它继承了TemplateAwareExpressionParser,TemplateAwareExpressionParser作为模板化spel解析类的抽象类,它将具体对spel字符串的解析方法doParseExpression()

交给了子类去实现

                实现parser类之前,我们需要在SPEL的节点枚举类型中添加我们的自定义函数枚举类型,如下在TokenKind中添加SELF_FUNCTION_REF("@{"),在SPEL表达式中,我们的自定义函数都由@{}定位,当然这里也可以用其他的字符,接着在Tokenizer中添加对我们自定义定位符的识别isTwoCharToken(TokenKind.SELF_FUNCTION_REF如果是“@{”就将其作为自定义函数类型pushPairToken(TokenKind.SELF_FUNCTION_REF)放入List<Token>中

                以下是Parser,及SelfFunctionReference

package com.lkyl.oceanframework.log.spelExt.ops;

import com.lkyl.oceanframework.log.factory.ParseFunctionFactory;
import com.lkyl.oceanframework.log.function.IParseFunction;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.ast.SpelNodeImpl;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;

import java.util.Map;
import java.util.StringJoiner;

import static org.springframework.expression.spel.SpelMessage.FUNCTION_NOT_DEFINED;

/**
 * TODO
 *
 * @version 1.0
 * @author: nicholas
 * @createTime: 2022年06月28日 23:00
 */
public class SelfFunctionReference extends SpelNodeImpl {

    private String selfFunctionName;

    @Nullable
    private volatile IParseFunction iParseFunction;

    private Object [] args;

    public String getSelfFunctionName() {
        return this.selfFunctionName;
    }

    public SelfFunctionReference(String selfFunctionName, int startPos, int endPos, SpelNodeImpl... arguments) {
        super(startPos, endPos, arguments);
        this.selfFunctionName = selfFunctionName;
    }

    @Override
    public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        try {
            Object beanFactory = state.getEvaluationContext().getBeanResolver().resolve(state.getEvaluationContext(), "parseFunctionFactory");
            if(beanFactory != null && beanFactory instanceof ParseFunctionFactory) {
                IParseFunction iParseFunction = ((ParseFunctionFactory) beanFactory).getFunction(this.selfFunctionName);
                if(iParseFunction != null) {
                    Object [] args = getArguments(state);
                    if(args.length != 1) {
                        throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, args.length, ReflectionUtils.findMethod(iParseFunction.getClass(), "apply").getParameterCount());
                    }
                    this.iParseFunction = iParseFunction;
                    this.args = args;
                    Object resultValue = iParseFunction.apply(args[0]);
                    return new TypedValue(resultValue);
                }
            }
            throw new SpelEvaluationException(FUNCTION_NOT_DEFINED, this.selfFunctionName);
        } catch (EvaluationException e) {
            throw e;
        } catch (AccessException e) {
            throw new EvaluationException("resolve beanFactory error!");
        }


    }

    @Override
    public String toStringAST() {
        StringJoiner sj = new StringJoiner(",", "(", ")");
        for (int i = 0; i < getChildCount(); i++) {
            sj.add(getChild(i).toStringAST());
        }
        return "@{" + this.selfFunctionName + sj.toString() + "}";
    }

    /**
     * Compute the arguments to the function, they are the children of this expression node.
     * @return an array of argument values for the function call
     */
    private Object[] getArguments(ExpressionState state) throws EvaluationException {
        // Compute arguments to the function
        Object[] arguments = new Object[getChildCount()];
        for (int i = 0; i < arguments.length; i++) {
            arguments[i] = this.children[i].getValueInternal(state).getValue();
        }
        return arguments;
    }

    @Override
    public boolean isCompilable() {

        return this.iParseFunction != null && args.length > 0;
    }
}

         Parser


/**
 * TODO
 *
 * @version 1.0
 * @author: nicholas
 * @createTime: 2022年06月28日 22:51
 */
public class LogRecordValueParser extends TemplateAwareExpressionParser {

    //省略....

    //startNode
    // : parenExpr | literal
    //	    | type
    //	    | methodOrProperty
    //	    | functionOrVar
    //	    | projection
    //	    | selection
    //	    | firstSelection
    //	    | lastSelection
    //	    | indexer
    //	    | constructor
    @Nullable
    private SpelNodeImpl eatStartNode() {
        if (maybeEatLiteral()) {
            return pop();
        }
        else if (maybeEatParenExpression()) {
            return pop();
        }
        else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstructorReference() ||
                maybeEatMethodOrProperty(false) || maybeEatFunctionOrVar()) {
            return pop();
        }
        else if (maybeEatSelfFunctionReference()) {//因为定位符是@{},而bean的定位符是@,所以要放到maybeEatBeanReference前面
            return pop();
        }
        else if (maybeEatBeanReference()) {
            return pop();
        }
        else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) {
            return pop();
        }
        else if (maybeEatInlineListOrMap()) {
            return pop();
        }
        else {
            return null;
        }
    }

    //自定义函数
    // parse: @{selfFunctionName}
    // quoted if dotted
    private boolean maybeEatSelfFunctionReference() {
        if (peekToken(TokenKind.SELF_FUNCTION_REF)) {
            Token selfFunRefToken = takeToken();
            Token funNameToken = null;
            String funName = null;
            if (peekToken(TokenKind.IDENTIFIER)) {
                funNameToken = eatToken(TokenKind.IDENTIFIER);
                funName = funNameToken.stringValue();
            }
            else {
                throw internalException(funNameToken.startPos, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED);
            }
            SpelNodeImpl[] args = maybeEatMethodArgs();
            eatToken(TokenKind.RCURLY);
            this.constructedNodes.push(new SelfFunctionReference(funName, funNameToken.startPos, funNameToken.endPos, args));
            return true;
        }
        return false;
    }

//省略....
}

三、与springboot整合

        1.1 @EnableLogRecord,组件的starter

package com.lkyl.oceanframework.log.annotation;

import com.lkyl.oceanframework.log.selector.LogRecordConfigureSelector;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * @author nicholas
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LogRecordConfigureSelector.class)
public @interface EnableLogRecord {

    String tenantId();

    AdviceMode mode() default AdviceMode.PROXY;
}

         1.2 selector

package com.lkyl.oceanframework.log.selector;

import com.lkyl.oceanframework.log.config.LogRecordProxyAutoConfiguration;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * TODO
 *
 * @version 1.0
 * @author: nicholas
 * @createTime: 2022年05月29日 23:09
 */
public class LogRecordConfigureSelector implements ImportSelector {


    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{LogRecordProxyAutoConfiguration.class.getName()};
    }
}

        1.3 AutoConfig

package com.lkyl.oceanframework.log.config;

import com.lkyl.oceanframework.log.adviser.BeanFactoryLogRecordAdvisor;
import com.lkyl.oceanframework.log.annotation.EnableLogRecord;
import com.lkyl.oceanframework.log.factory.ParseFunctionFactory;
import com.lkyl.oceanframework.log.function.IParseFunction;
import com.lkyl.oceanframework.log.function.impl.DefaultParseFunction;
import com.lkyl.oceanframework.log.interceptor.LogRecordInterceptor;
import com.lkyl.oceanframework.log.parser.LogRecordOperationSource;
import com.lkyl.oceanframework.log.service.IFunctionService;
import com.lkyl.oceanframework.log.service.ILogRecordService;
import com.lkyl.oceanframework.log.service.impl.DefaultFunctionServiceImpl;
import com.lkyl.oceanframework.log.service.impl.DefaultLogRecordServiceImpl;
import lombok.extern.slf4j.Slf4j;
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.*;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

import java.util.List;

@Configuration
@Slf4j
public class LogRecordProxyAutoConfiguration implements ImportAware {

  private AnnotationAttributes enableLogRecord;


  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public LogRecordOperationSource logRecordOperationSource() {
    return new LogRecordOperationSource();
  }

  @Bean
  @ConditionalOnMissingBean(IFunctionService.class)
  public IFunctionService functionService(ParseFunctionFactory parseFunctionFactory) {
    return new DefaultFunctionServiceImpl(parseFunctionFactory);
  }

  @Bean
  public ParseFunctionFactory parseFunctionFactory(@Autowired List<IParseFunction> parseFunctions) {
    return new ParseFunctionFactory(parseFunctions);
  }

  @Bean
  @ConditionalOnMissingBean(IParseFunction.class)
  public DefaultParseFunction parseFunction() {
    return new DefaultParseFunction();
  }


  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public BeanFactoryLogRecordAdvisor logRecordAdvisor(IFunctionService functionService) {
    BeanFactoryLogRecordAdvisor advisor =
            new BeanFactoryLogRecordAdvisor();
    advisor.setLogRecordOperationSource(logRecordOperationSource());
    advisor.setAdvice(logRecordInterceptor(functionService));
    return advisor;
  }

  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public LogRecordInterceptor logRecordInterceptor(IFunctionService functionService) {
    LogRecordInterceptor interceptor = new LogRecordInterceptor();
    interceptor.setLogRecordOperationSource(logRecordOperationSource());
    interceptor.setTenantId(enableLogRecord.getString("tenantId"));
    interceptor.setFunctionService(functionService);
    return interceptor;
  }

  @Bean
  @ConditionalOnMissingBean(ILogRecordService.class)
  @Role(BeanDefinition.ROLE_APPLICATION)
  public ILogRecordService recordService() {
    return new DefaultLogRecordServiceImpl();
  }

  @Override
  public void setImportMetadata(AnnotationMetadata importMetadata) {
    this.enableLogRecord = AnnotationAttributes.fromMap(
            importMetadata.getAnnotationAttributes(EnableLogRecord.class.getName(), false));
    if (this.enableLogRecord == null) {
      log.info("@EnableLogRecord is not present on importing class");
    }
  }
}

完!

github链接:项目地址

美团技术文档:美团技术文档链接

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值