认识SpEL表达式

认识SpEL表达式

前言

最近项目接入苞米豆的lock4j用于分布式的锁控制,良好的控制在多台服务器下请求分流导致的数据重复问题,使用上也比较简单,在需要分布式锁的方法上添加一个@Lock4j注解并添加相应的参数即可,在使用中发现其中有一个属性keys = {"#userId", "#user.sex"},并且支持自定义重写分布式锁键的生成策略。在好奇心的驱使下,查看了默认实现的分布式锁键生成策略是通过SpEL的方式解析参数信息。

SpEL概述

Spring表达式语言的全拼为Spring Expression Language,缩写为SpEL。并且SpEL属于spring-core模块,不直接与Spring绑定,是一个独立模块,不依赖于其他模块,可以单独使用。

核心接口

  1. 解析器ExpressionParser,用于将字符串表达式转换为Expression表达式对象。
  2. 表达式Expression,最后通过它的getValute方法对表达式进行计算取值。
  3. 上下文EvaluationContext,通过上下文对象结合表达式来计算最后的结果。

简单使用

public static void main(String[] args) {
  // 创建解析器
  ExpressionParser parser = new SpelExpressionParser();
  // 解析表达式为Expression对象
  Expression expression = parser.parseExpression("'Hello' + 'World'");
  // 计算求值
  System.out.println(expression.getValue(context));
}

进行一些简单的运算

public static void main(String[] args) {
  // 创建解析器
  ExpressionParser parser = new SpelExpressionParser();
  // 解析表达式为Expression对象
  // 进行字符串的拼接
  System.out.println(parser.parseExpression("'Hello' + 'World'").getValue(String.class));
  // 简单的运算
  System.out.println(parser.parseExpression("1+2").getValue());
  // 简单的比较
  System.out.println(parser.parseExpression("1>2").getValue());
  // 稍微复杂一点的比较
  System.out.println(parser.parseExpression("2>1 and (!true)").getValue());
}

通过ParseContext对象设置自定义的解析规则:这里设置表达式的解析前缀为#{解析后缀为},最后通过表达式对象expression.getValue()获取到表达式中的值。

public static void main(String[] args) {
  ExpressionParser parser = new SpelExpressionParser();
  ParserContext parserContext = new ParserContext() {
    @Override
    public boolean isTemplate() {
      return true;
    }

    @Override
    public String getExpressionPrefix() {
      return "#{";
    }

    @Override
    public String getExpressionSuffix() {
      return "}";
    }
  };
  String template = "#{'Hello'}#{'World!'}";
  Expression expression = parser.parseExpression(template, parserContext);
  System.out.println(expression.getValue());
}

还有很多不同的取值方式,比如参数(上下文)是个对象,获取这个对象中的某个属性;或者参数是一个List获取某一个索引值;又或者是一个Map对象,根据某个Key获取对应的值等等。

实际应用

​ 如果平时有使用Spring框架应该都会有用到比如@Value注解,就是通过SpEL方式进行赋值。

public class UserFacade {
		
  	// 获取字符串tom 
    @Value("#{'tom'}")
    private String name;
  	
  	// 获取bean对象的属性
    @Value("#{user.value}")
    private String value;
}

在比如接触过Spring Security或者Shiro等身份验证和授权的框架中,对不同的角色有不同的接口权限,会使用到如下场景,其中对@PreAuthorize("hasAuthority('ROLE_DMIN'))hasAuthority('ROLE_ADMIN')就是通过SpEL进行参数解析后,对当前用户的角色进行校验。

@RestController
@RequestMapping("/admin/user")
public class UserController {

    /**
     * 拥有管理员权限可查看任何用户信息,否则只能查看自己的信息
     */
    @PreAuthorize("hasAuthority('ROLE_ADMIN'))
    @PostMapping("/getUserById/{userId}")
    public Result<List<SysUser>> getUserById(String userId) {
        return new Result<>(userFacade.getUserById(userId));
    }
}

重构

​ 之前在项目中记录系统中一些敏感接口的请求日志信息,采用的是AOP的方式,在请求进入控制层之前拦截进入AOP的切面方法,但是记录的日志部分关键信息需要从请求的参数中获取,在之前的实现中是通过约定一种表达式,对应列表ListMapbean对象的取值是自实现,且仅仅支持二级取值,确实在使用上有很大的缺陷。这种场景下,就可以使用SpEL进行方法参数解析,省了重复造轮子的过程,且使用上更为灵活。

SpEL结合AOP重构请求日志保存,这边只做简单的通过SpEL方式进行对象等取值处理,不考虑具体实际场景中的复杂业务逻辑。

/**
 * 测试控制层
 * @Author: xiaocainiaoya
 * @Date: 2021/04/20 23:06:06
 **/
@RestController
@RequestMapping("basic")
@Api(tags = "测试")
public class BasicVersionController {

    @ApiOperation(value="测试",notes="测试")
    @PostMapping("test")
    @ControllerMethodLog(name = "测试保存请求日志", description = "测试保存请求日志")
    @LogAssistParams(value={
            @LogAssistParam(logField="projectName",objField="#projectInfo.id"),
            @LogAssistParam(logField="id",objField="#projectInfo.projectName")
    })
    public RestResponse<ProjectInfo> test(@RequestBody ProjectInfo projectInfo){
        return null;
    }
}

AOP切面类

/**
 * 拦截日志
 *
 * @Author: xiaocainiaoya
 * @Date: 2021/04/20 23:08:28
 **/
@Aspect
@Component
@Slf4j
public class OperationTestLogAspect {

   @Autowired
   private OperationLogFacade operationLogFacade;

   /**
   * 此处的切点是注解的方式
   */
   @Pointcut("@annotation(cn.com.xiaocainiaoya.annotation.ControllerMethodLog)")
   public void operationLog() {
   }

   @Around("operationLog()")
   public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      OperationLog operationLog = new OperationLog();
      operationLog.setStatus(1);// 默认调用成功,异常时修改为调用失败

      Object thing = null;
      try {
         // 执行切入方法内容
         thing = joinPoint.proceed();
         operationLog.setOperEndTime(DateTime.now().toJdkDate());
         return thing;
      } catch (Throwable e) {
         log.error(e.getMessage(), e);
         operationLog.setStatus(0);//发生异常时定义为调用失败
         operationLog.setResultContext(e.getMessage());
         throw e;
      } finally {
         insertOperationLog(operationLog, joinPoint, thing);
      }
   }

   private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

   private static final ExpressionParser PARSER = new SpelExpressionParser();

   /**
   *   插入操作日志
   *
   * @Author: xiaocainiaoya
   * @Date: 2021/04/20 23:11:28
   * @param operationLog 日志基础信息
   * @param joinPoint 拦截切入点信息
   * @param thing 拦截函数返回值
   * @return:
   **/
   private void insertOperationLog(OperationLog operationLog, ProceedingJoinPoint joinPoint, Object thing) {
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      ControllerMethodLog methodAnnotation = signature.getMethod().getAnnotation(ControllerMethodLog.class);
      Api typeAnnotation = (Api) signature.getDeclaringType().getAnnotation(Api.class);
      //注释不完整不进行日志记录操作
      if (methodAnnotation == null || typeAnnotation == null) {
         return;
      }
      LogAssistParams logAssistParams = signature.getMethod().getAnnotation(LogAssistParams.class);
      if(methodAnnotation == null){
         return ;
      }
      LogAssistParam[] assistParams = logAssistParams.value();
      if(ObjectUtil.isNull(assistParams) || assistParams.length == 0){
         return ;
      }
      for(int i = 0; i < assistParams.length; i++){
         /**
          * 重点在这,通过MethodBasedEvaluationContext构建解析器ExpressionParser的上下文, 底层逻辑也是通过ParameterNameDiscoverer反射获取对应的属性值
          */
         EvaluationContext context = new MethodBasedEvaluationContext((Object) null, signature.getMethod(), joinPoint.getArgs(), NAME_DISCOVERER);
         String value = (String)PARSER.parseExpression(assistParams[i].objField()).getValue(context);
         ReflectUtil.setFieldValue(operationLog, assistParams[i].logField(), value);
      }
      operationLogFacade.insertSelective(operationLog);
   }
}

博客地址:https://xiaocainiaoya.github.io/

联系方式:xiaocainiaoya@foxmail.com

扫码

posted @ 2021-04-26 21:38  不懂技术的小菜鸟~  阅读(2089)  评论(0编辑  收藏  举报