Spring的spel表达式

背景

其实好久之前就听说过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表达式吗??其实这东西我们是用过的呀!!

引用

1、玩转Spring中强大的spel表达式!
2、一文掌握Spring的SpEL!

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring中,可以使用SpEL表达式绑定方法参数。具体步骤如下: 1. 在方法参数上添加@org.springframework.web.bind.annotation.RequestParam注解(或其他适当的注解),并指定SpEL表达式作为其属性值。 2. 在方法上添加@org.springframework.web.bind.annotation.GetMapping(或其他适当的注解),并设置其value属性来指定请求的URL。 3. 在应用程序上下文中配置一个DefaultSpELExpressionParser bean。 4. 在方法参数上添加@org.springframework.beans.factory.annotation.Value注解,并使用SpEL表达式引用应用程序上下文中的bean。 例如,以下代码展示了如何在Spring MVC中使用SpEL表达式绑定方法参数: ```java @RestController public class MyController { @GetMapping("/user/{id}") public User getUserById(@PathVariable("id") int id, @RequestParam("#{systemProperties['user.language']}") String language, @Value("#{myService.someProperty}") String someProperty) { // ... } } ``` 在上面的代码中,getUserById方法使用@PathVariable注解将URL路径中的{id}绑定到方法参数id上。然后,它使用@RequestParam注解将请求参数language绑定到方法参数language上,这里的SpEL表达式#{systemProperties['user.language']}将系统属性user.language的值作为参数传递给方法。最后,它使用@Value注解将应用程序上下文中名为myService的bean的someProperty属性绑定到方法参数someProperty上,这里的SpEL表达式#{myService.someProperty}将该属性的值作为参数传递给方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值