likes
comments
collection
share

认识SpEL表达式

作者站长头像
站长
· 阅读数 70

前言

最近项目接入苞米豆的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,通过上下文对象结合表达式来计算最后的结果。

简单使用

  1. 创建一个解析器对象
  2. 通过解析器对象解析表达式为Expression对象
  3. 再通过Expression对象的相应方法进行计算求值
public static void main(String[] args) {
  // 1.
  ExpressionParser parser = new SpelExpressionParser();
  // 2.
  Expression expression = parser.parseExpression("'Hello' + 'World'");
  // 3.
  expression.getValue(context);
}

进行一些简单的运算 解析表达式为Expression对象,Spel可以进行字符串的拼接、求和、大小比较以及布尔类型的判断等等。

public static void main(String[] args) {
  ExpressionParser parser = new SpelExpressionParser();
  parser.parseExpression("'Hello' + 'World'").getValue(String.class);
  parser.parseExpression("1+2").getValue();
  parser.parseExpression("2>1 and (!true)").getValue();
}

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

public static void main(String[] args) {
  ExpressionParser parser = new SpelExpressionParser();
  ParserContext context = 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, context);
}

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

实际应用

​ 如果平时有使用Spring框架应该都会有用到,某些配置项,比如url地址,或者第三方接口的开发者id等都可以通过@Value注解的方式从应用的配置文件中获取,而这种方式就是使用SpEL来进行解析后读取到对应数据。

public class UserFacade {
  	
    @Value("#{user.value}")
    private String value;
}

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

  1. 拥有管理员权限可查看任何用户信息,否则只能查看自己的信息
public class UserController {

    // 1. 
    @PreAuthorize("hasAuth('ROLE_ADMIN'))
    @PostMapping("/getUserById/{userId}")
    public Result<SysUser> getUserById(String userId) {
        // ...
    }
}

重构

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

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

  • ControllerMethodLog注解:用于标记对应方法的相关信息,入库后便于排查问题。
  • LogParams注解:参数列表注解,内部value是一个数组,用于存放需要进行解析的参数列表。
  • LogParam注解:参数注解,用于映射请求参数中的某个属性值需要映射到入库对象的某个属性上。
public class BasicController {

    @ControllerMethodLog(description = "测试保存请求日志")
    @LogParams(value={
            @LogParam(logField="id",objField="#userInfo.id")
    })
    public RestResponse<UserInfoVo> test(@RequestBody UserInfo userInfo){
        return null;
    }
}

AOP切面类:进入切面业务逻辑后首先设置状态为正常,在异常捕获逻辑中标记异常状态。由于需要同时记录请求的返回成功与否,在finally中进行具体的接口请求日志记录,在实际场景中,应将此块内容进行异步化。

@Aspect
@Component
public class OperationTestLogAspect {

   @Pointcut("@annotation(cn.com.xiaocainiaoya.annotation.ControllerMethodLog)")
   public void operationLog() {
   }

   @Around("operationLog()")
   public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      OperationLog opLog = new OperationLog();
      opLog.setStatus(1);

      Object thing = null;
      try {
         // 
         thing = joinPoint.proceed();
         return thing;
      } catch (Throwable e) {
         opLog.setStatus(0);
         opLog.setResult(e.getMessage());
         throw e;
      } finally {
         insertOperationLog(opLog, joinPoint, thing);
      }
   }
}

核心逻辑在此,在请求数据入库之前,需要通过spEl结合标记的注解对请求参数取值,通过MethodBasedEvaluationContext构建解析器ExpressionParser的上下文, 底层逻辑也是通过ParameterNameDiscoverer反射获取对应的属性值,然后再通过反射,将获取到的属性值赋值给入库对象。

private void insertOperationLog(OperationLog opLog, ProceedingJoinPoint joinPoint, Object thing) {
   // ... 
   for(int i = 0; i < params.length; i++){
      // 重点在这
      EvaluationContext context = new MethodBasedEvaluationContext((Object) null, signature.getMethod(), joinPoint.getArgs(), NAME_DISCOVERER);
      String value = (String)PARSER.parseExpression(params[i].objField()).getValue(context);
      ReflectUtil.setFieldValue(opLog, params[i].logField(), value);
   }
   // 插入逻辑
}

总结

​ 在实际的应用场景中,通过SpeL表达式来获取对应变量的场景还有很多,使用Spel来获取对应的变量值,可以提高整体代码结构的灵活性、易用性等。比如上述的分布式锁通过接口参数设置锁名称、日志中通过接口参数设置日志相关参数等等。

转载自:https://juejin.cn/post/7060326049673904141
评论
请登录