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