Spring Expressjion Language(SpEL)

Spring Expressjion Language(SpEL)

做毕业设计的时候,系统需要记录用户的操作日志,并持久化存储。日志具体的内容大概是xx用户在xx事件对xx做了xx修改,记录的内容比较详细,日志的内容和动态参数有关,并且实现方式不能对业务代码有侵入性,很容易想到切面+注解的方式,但仅靠注解无法实现动态参数的记录,当时对于SpEL不太熟悉,并且操作日志的功能不在毕设要求内,只是简单地对每个接口进行了固定的日志记录,如擦操作人、时间、添加了xxx、删除了xxx等信息,后来想到SpringSecurity使用SpEL实现了动态权限的控制,那么也应该可以实现动态参数的日志记录,所以有了这篇笔记。

Spel官方文档

SpringEL表达式,简单来说就是可以在运行时解析一定格式的字符串,当作代码来执行。即可以计算出表达式的值,也可以通过表达式设置对象的属性。

SpEL相关接口和类

EL表达式的执行流程包含以下几个步骤。

  1. 配置参数(SpelParserConfigration),如null值得处理,集合大小是否自动增长等,创建解析器(SpelExpressionParser)。
  2. 解析上下文配置(ParserContext),如模板解析。
  3. 求值上下文配置(EvaluationContext),可以设置相关变量、根对象、对象属性的访问权限等
  4. 解析表达式并求值。

以下展示了基本用法

SpelParserConfiguration configuration = new SpelParserConfiguration(true, true);
SpelExpressionParser parser = new SpelExpressionParser(configuration);
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("demo", new Demo("d23", "sdf"));
context.setVariable("a", 100);
Demo root = new Demo("root", "666666");
System.out.println(parser.parseExpression("我是#{#demo.username},我今年#{#a}岁 #{password}",
        new TemplateParserContext()).getValue(context, root));

SpEL语法

Spring官方文档中对所有的语法进行了介绍,这里进行简单总结

  1. 支持从字符串直接构造列表、哈希表。
  2. 支持各种运算操作符,四则运算、位运算,Java语言支持的都可以使用,且支持"obj.?property","obj.!property"等运算符。
  3. 函数调用。调用对象的方法。
    • 将对象设置到上下文中或者根对象,调用其成员方法。
    • 通过类的全限定类名调用其静态方法,如"T(java.Math).max(4, 5)"
  4. 访问Bean使用"@beanName",访问FactoryBean使用"&名称"。
  5. 支持对集合进行筛选,“list.?[value<10]”

更多语法等可以查看官方文档或者Spel表达式

SpEL记录日志

以修改某用户信息为例,需要记录的日志形式为 xx将张三(id)调整到了李四(id)的位置,以及时间、IP等信息。

  • 定义注解,Spel表达式写在该注解上
  • 定义切面,织入所有有该注解的方法
    • 通过切面获取方法的所有参数对象
    • 通过方法获取参数名称
    • 将参数名称、参数对象对应设置到求值上下文中
    • 求解表达式
  • 将日志持久化

主要分为以上三部,其中关键在于切面的执行逻辑。对于我们来说,既要获取旧值,也要拿到新值。旧值无非两种方法。

  1. 前端携带
  2. 数据库查询
    虽说都可以解决问题,但从逻辑上、合理性考虑,应当从数据库查询。
    新值直接通过Controller层的参数获取,由于不能对业务代码有任何的侵入性,所有的逻辑都需要在切面中完成。下面代码为解析并对表达式求值的过程。
if (!StringUtils.isEmptyOrBlank(logRecord.spel())) {
    SpelExpressionParser parser = new SpelExpressionParser();
    /* 属性只读 */
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    /* 解析参数 */
    for (Map.Entry<String, Object> entry : resolveParams(method, pjp).entrySet()) {
        context.setVariable(entry.getKey(), entry.getValue());
    }

    log.info(parser.parseExpression(logRecord.spel()).getValue(context,String.class));
}

参数的解析如下所示

private Map<String, Object> resolveParams(Method method, ProceedingJoinPoint pjp) {
    Parameter[] parameters = method.getParameters();
    Map<String, Object> map = new HashMap<>();
    Object[] args = pjp.getArgs();
    for (int i = 0; i < parameters.length; i++){
        map.put(parameters[i].getName(), args[i]);
    }
    return map;
}

上述代码对应的Controller接口

@OperLogRecord(spel = "'体测编号:' + #formdata.msId + ' ' + #formdata.msName")
    @Operation(operationId = Authority.PtMs.MS_UPDATE, summary = "修改体测信息")
    @PutMapping
    public ApiResponse<Boolean> updateMeasurement(
            @RequestBody @Validated(Update.class) PtMeasurementFormdata formdata) {
        return ApiResponse.success(ptMeasurementService.updateMeasurement(formdata));
    }

可以看到表达式中使用了大量的字符串拼接,Spring默认实现了模板上下文的解析方式,表达式可以写成"体测编号:#{#formdata.msId} #{#formdata.msName}",使用TemplateParserContext。

上述表达式并未求出旧值,如需要求出旧值可以使用函数,例如"体测:#{@xxxService.selectById(#formdata.msId)} #{#formdata.msName}",访问Bean使用@。

但是还有一个问题,这种方式查出来的结果为对象,无法满足 xxx(id)把生日从xxx修改为xxx这样的日志记录,也不能在表达式中写两遍一样的函数,目前没有好的解决办法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值