认识SpEL表达式
前言
最近项目接入苞米豆的lock4j
用于分布式的锁控制,良好的控制在多台服务器下请求分流导致的数据重复问题,使用上也比较简单,在需要分布式锁的方法上添加一个@Lock4j
注解并添加相应的参数即可,在使用中发现其中有一个属性keys = {"#userId", "#user.sex"}
,并且支持自定义重写分布式锁键的生成策略。在好奇心的驱使下,查看了默认实现的分布式锁键生成策略是通过SpEL
的方式解析参数信息。
SpEL概述
Spring
表达式语言的全拼为Spring Expression Language
,缩写为SpEL
。并且SpEL
属于spring-core
模块,不直接与Spring
绑定,是一个独立模块,不依赖于其他模块,可以单独使用。
核心接口
- 解析器
ExpressionParser
,用于将字符串表达式转换为Expression
表达式对象。
- 表达式
Expression
,最后通过它的getValute
方法对表达式进行计算取值。
- 上下文
EvaluationContext
,通过上下文对象结合表达式来计算最后的结果。
简单使用
1 2 3 4 5 6 7 8
| public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); 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(); 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 { @Value("#{'tom'}") private String name; @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
的切面方法,但是记录的日志部分关键信息需要从请求的参数中获取,在之前的实现中是通过约定一种表达式,对应列表List
、Map
、bean
对象的取值是自实现,且仅仅支持二级取值,确实在使用上有很大的缺陷。这种场景下,就可以使用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
|
@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
|
@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();
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++){
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