0%

认识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,通过上下文对象结合表达式来计算最后的结果。

简单使用

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

进行一些简单的运算

1
2
3
4
5
6
7
8
9
10
11
12
13
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()获取到表达式中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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方式进行赋值。

1
2
3
4
5
6
7
8
9
10
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进行参数解析后,对当前用户的角色进行校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
@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方式进行对象等取值处理,不考虑具体实际场景中的复杂业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 测试控制层
* @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="#bidProjectInfo.id"),
@LogAssistParam(logField="id",objField="#bidProjectInfo.projectName")
})
public RestResponse<BidPackageInvitationVo> test(@RequestBody ProjectInfo projectInfo){
return null;
}
}

AOP切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* 拦截日志
*
* @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

扫码

-------------本文结束感谢您的阅读-------------