背景
其实好久之前就听说过spel表达式了,碍于太难了,一下子看不懂;所以就一直没有去了解和学习。今天学习项目的时候看到了别人有使用,因此必须狠狠拿下这个知识点。
作用
主要作用:在运行时构建复杂表达式、存取对象图属性、对象方法调用等。给静态Java语言增加了动态功能。
基础概念
要学spel表达式,要先掌握3个重要的概念。
1、ExpressionParser(解析器):用于解析sepl表达式。
2、Expression(表达式):ExpressionParser解析表达式后产生的表达式。
3、EvaluationContext(上下文):主要存储一些内容/对象。用于给表达式获取对应的属性。
总结:定义解析器去解析表达式,然后到对应的上下文中获取属性。
第三点不是必须要有的。
简单案例
@Test
public void test1() {
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
}
1)创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现;
2)解析表达式:使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象。
3)构造上下文:准备比如变量定义等等表达式需要的上下文数据。
4)求值:通过Expression接口的getValue方法根据上下文获得表达式值。
字面量表达式
SpEL支持的字面量包括:字符串、数字类型(int、long、float、double)、布尔类型、null类型。
@Test
public void test2() {
ExpressionParser parser = new SpelExpressionParser();
String str1 = parser.parseExpression("'Hello World!'").getValue(String.class);
int int1 = parser.parseExpression("1").getValue(Integer.class);
long long1 = parser.parseExpression("-1L").getValue(long.class);
float float1 = parser.parseExpression("1.1").getValue(Float.class);
double double1 = parser.parseExpression("1.1E+2").getValue(double.class);
int hex1 = parser.parseExpression("0xa").getValue(Integer.class);
long hex2 = parser.parseExpression("0xaL").getValue(long.class);
boolean true1 = parser.parseExpression("true").getValue(boolean.class);
boolean false1 = parser.parseExpression("false").getValue(boolean.class);
Object null1 = parser.parseExpression("null").getValue(Object.class);
System.out.println("str1=" + str1);
System.out.println("int1=" + int1);
System.out.println("long1=" + long1);
System.out.println("float1=" + float1);
System.out.println("double1=" + double1);
System.out.println("hex1=" + hex1);
System.out.println("hex2=" + hex2);
System.out.println("true1=" + true1);
System.out.println("false1=" + false1);
System.out.println("null1=" + null1);
}
输出
str1=Hello World!
int1=1
long1=-1
float1=1.1
double1=110.0
hex1=10
hex2=10
true1=true
false1=false
null1=null
同事还支持
1、关系表达式(>、=、<)
2、逻辑表达式(&、| 、 !)
3、字符串连接及截取表达式(“‘Hello World!’[0]”将返回“H”)
4、三目运算
5、正则表达式
6、括号优先级表达式
获取上下文中的变量
对于变量而言,我们可以通过EvaluationContext的setVariable()方法进行设置,然后在表达式中使用时通过“#varName”的形式进行使用。如下示例中我们就给EvaluationContext设置了一个名为“user”的变量,然后在表达式中通过“#user”来使用该变量。
@Test
public void test14() {
Object user = new Object() {
public String getName() {
return "abc";
}
};
EvaluationContext context = new StandardEvaluationContext();
//1、设置变量
context.setVariable("user", user);
ExpressionParser parser = new SpelExpressionParser();
//2、表达式中以#varName的形式使用变量
Expression expression = parser.parseExpression("#user.name");
//3、在获取表达式对应的值时传入包含对应变量定义的EvaluationContext
String userName = expression.getValue(context, String.class);
//表达式中使用变量,并在获取值时传递包含对应变量定义的EvaluationContext。
Assert.assertTrue(userName.equals("abc"));
}
类类型表达式(访问静态方法或属性)
使用“T(Type)”来表示java.lang.Class实例,“Type”必须是类全限定名,“java.lang”包除外,即该包下的类可以不指定包名;使用类类型表达式可以进行访问类静态方法及类静态字段。(通过该表达式可以获取到项目上下文中的对象)
具体使用方法如下:
@Test
public void testClassTypeExpression() {
ExpressionParser parser = new SpelExpressionParser();
//java.lang包类访问
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(result1);
//其他包类访问
String expression2 = "T(com.javacode2018.spel.SpelTest)";
Class<SpelTest> value = parser.parseExpression(expression2).getValue(Class.class);
System.out.println(value == SpelTest.class);
//类静态字段访问
int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
System.out.println(result3 == Integer.MAX_VALUE);
//类静态方法调用
int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
System.out.println(result4);
}
类实例化
类实例化同样使用java关键字“new”,类名必须是全限定名,但java.lang包内的类型除外,如String、Integer。
@Test
public void testConstructorExpression() {
ExpressionParser parser = new SpelExpressionParser();
String result1 = parser.parseExpression("new String('路人甲java')").getValue(String.class);
System.out.println(result1);
Date result2 = parser.parseExpression("new java.util.Date()").getValue(Date.class);
System.out.println(result2);
}
#root
#root在表达式中永远都指向对应EvaluationContext的rootObject对象。在如下示例中#root就指向了对应的user对象。
@Test
public void test14_1() {
Object user = new Object() {
public String getName() {
return "abc";
}
};
EvaluationContext context = new StandardEvaluationContext(user);
ExpressionParser parser = new SpelExpressionParser();
Assert.assertTrue(parser.parseExpression("#root.name").getValue(context).equals("abc"));
}
#this
#this永远指向当前对象,其通常用于集合类型,表示集合中的一个元素。如下示例中我们就使用了#this表示当前元素以选出奇数作为一个新的List进行返回。
@Test
public void test14_2() {
ExpressionParser parser = new SpelExpressionParser();
List<Integer> intList = (List<Integer>)parser.parseExpression("{1,2,3,4,5,6}").getValue();
EvaluationContext context = new StandardEvaluationContext(intList);
//从List中选出为奇数的元素作为一个List进行返回,1、3、5。
List<Integer> oddList = (List<Integer>)parser.parseExpression("#root.?[#this%2==1]").getValue(context);
for (Integer odd : oddList) {
Assert.assertTrue(odd%2 == 1);
}
}
变量定义及引用
在表达式中使用"#variableName"引用;除了引用自定义变量,SpE还允许引用根对象及当前上下文对象,使用"#root"引用根对象,使用"#this"引用当前上下文对象;
@Test
public void testVariableExpression() {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", "路人甲java");
context.setVariable("lesson", "Spring系列");
//获取name变量,lesson变量
String name = parser.parseExpression("#name").getValue(context, String.class);
System.out.println(name);
String lesson = parser.parseExpression("#lesson").getValue(context, String.class);
System.out.println(lesson);
//StandardEvaluationContext构造器传入root对象,可以通过#root来访问root对象
context = new StandardEvaluationContext("我是root对象");
String rootObj = parser.parseExpression("#root").getValue(context, String.class);
System.out.println(rootObj);
//#this用来访问当前上线文中的对象
String thisObj = parser.parseExpression("#this").getValue(context, String.class);
System.out.println(thisObj);
}
因为用法太多了,就不一一介绍了。具体的文章将会在文章底部。
项目实战(AOP+spel表达式)
频控注解
@Repeatable(FrequencyControlContainer.class)//可重复
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControl {
/**
* key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做频控,就自己指定
*
* @return key的前缀
*/
String prefixKey() default "";
/**
* springEl 表达式,target=EL必填
*
* @return 表达式
*/
String spEl() default "";
/**
* 频控时间范围,默认单位秒
*
* @return 时间范围
*/
int time();
/**
* 频控时间单位,默认秒
*
* @return 单位
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 单位时间内最大访问次数
*
* @return 次数
*/
int count();
}
注解对应的切面
@Around("@annotation(com.abin.mallchat.common.common.annotation.FrequencyControl)||@annotation(com.abin.mallchat.common.common.annotation.FrequencyControlContainer)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class);
Map<String, FrequencyControl> keyMap = new HashMap<>();
for (int i = 0; i < annotationsByType.length; i++) {
FrequencyControl frequencyControl = annotationsByType[i];
String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? SpElUtils.getMethodKey(method) + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个)
String key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl());
keyMap.put(prefix + ":" + key, frequencyControl);
}
//批量获取redis统计的值
ArrayList<String> keyList = new ArrayList<>(keyMap.keySet());
List<Integer> countList = RedisUtils.mget(keyList, Integer.class);
for (int i = 0; i < keyList.size(); i++) {
String key = keyList.get(i);
Integer count = countList.get(i);
FrequencyControl frequencyControl = keyMap.get(key);
if (Objects.nonNull(count) && count >= frequencyControl.count()) {//频率超过了
log.warn("frequencyControl limit key:{},count:{}", key, count);
throw new BusinessException(CommonErrorEnum.FREQUENCY_LIMIT);
}
}
try {
return joinPoint.proceed();
} finally {
//不管成功还是失败,都增加次数
keyMap.forEach((k, v) -> {
RedisUtils.inc(k, v.time(), v.unit());
});
}
}
spel工具类
public class SpElUtils {
private static final ExpressionParser parser = new SpelExpressionParser();
private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public static String parseSpEl(Method method, Object[] args, String spEl) {
String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名
EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
}
Expression expression = parser.parseExpression(spEl);
return expression.getValue(context, String.class);
}
public static String getMethodKey(Method method) {
return method.getDeclaringClass() + "#" + method.getName();
}
}
controller
这里是获取普通的成员变量。
@GetMapping("/public/msg/page")
@ApiOperation("消息列表")
@FrequencyControl(time = 120, count = 20, target = FrequencyControl.Target.EL,spEl = "# request.roomId")
public ApiResult<CursorPageBaseResp<ChatMessageResp>> getMsgPage(@Valid ChatMessagePageReq request) {
//一些业务代码
}
如果获取的是静态的方法/变量的话。要通过前面谈到的“类类型表达式”的方式来获取。(代码中的就是上下文对象)
@FrequencyControl(time = 100, count = 5, spEl = "T(com.abin.mallchat.common.common.utils.RequestHolder).get().getIp()")
public void handleLoginReq(Channel channel) {
// 业务代码
}
后头想想
我们在对象中通过@Value注解去获取配置文件的属性,不就是通过spel表达式吗??其实这东西我们是用过的呀!!