認識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