基于Spring EL表达式处理业务表达式

文章介绍了如何在Spring框架中使用SpringEL进行动态表达式定制,特别是在@Value注解和XML配置中的应用,以及如何利用它来处理复杂的业务场景,如动态文件命名。此外,还展示了如何在表达式中引用Spring容器中的Bean和实例方法。
摘要由CSDN通过智能技术生成

简介

SpringEL是Spring表达式,功能非常强大。

我们可以能在@Value、@Cachable、自定义注解中的应用EL表达式,当然这些不是本文的重点。

本文的重点是如何基于SpringEL来定制业务表达式,以便于简化我们工作,减少自己去处理解析表达式的复杂逻辑。

一个简单的场景:

例如,业务希望文件名是动态的,根据实际的年、月、日、季以及其他的业务数据来生成。

因为有非常多的文件类型,业务希望通过表达式根据实际业务数据生成最终文件名。

如果仅仅是简单的年月日,自己写个简单的表达式解析器问题也不大,但是还有根据实际业务数据会有很多表达式,这种倒不是说不能写,但是复杂度就会比较高,测试起来就非常复杂,很难覆盖到所有的场景情况。

这时就可以基于SpringEL来定制开发。

Spring EL的常见应用

@Value

//注入操作系统属性
@Valule("#{systemProperties['os.name']}")
private String os;
//注入表达式结果
@Valule("#{T(java.lang.Math).random()*100}")
private Double randomNumber;
//注入其他bean属性
@Valule("#{myBean.name}")
private String name;

// 属性表达式,在配置文件找app.count属性注入
@Value("${app.count}")
private Integer count;

// EL表达式中包含$属性表达式
@Value("#{T(Integer).parseInt('${config.num:10}')}")
private Integer num;

这里要注意:#和$

Spring中:

  1. ${}是属性表达式,引用的是properties和yaml配置文件中的属性值
  2. #{}是EL表达式默认的模板

${}属性表达式在应用启动时就会被解析,在配置加载的时候进行替换,
#{}EL表达式是运行时解析

${}属性表达式比#{}EL表达式先执行,所以可以在EL表达式中包含${}属性表达式

处理注解

最常见的就是缓存、分布式锁等注解,可以根据方法的注解、结合实际调用的参数,计算缓存的key和分布式锁的key。

通过支持SpringEL,在设置的时候可以更加灵活。

@Aspect
@Component
public class KeyAspect {
    
    @Around("@annotation(disLock)")
    public Object around(ProceedingJoinPoint joinPoint, DisLcok disLock) throws Throwable {
        EvaluationContext context = new StandardEvaluationContext();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();
        String[] parametersNames = new DefaultParameterNameDiscoverer().getParameterNames(signature.getMethod());
        for (int i = 0; i < args.length; i++) {
            context.setVariable(parametersNames[i], args[i]);
        }

        // 计算key
        String lockKey = new SpelExpressionParser().parseExpression(disLock.key()).getValue(context, String.class);
        // 处理缓存、锁等逻辑
        return joinPoint.proceed();
    }
}

xml中使用

<bean id="myBean" class="vip.meet.MyBean">
    <property name="name" value="allen" />
    <property name="id" value="10" />
</bean>

<bean id="customerBean" class="vip.meet.CustomerBean">
    <property name="item" value="#{myBean}" />
    <property name="itemName" value="#{myBean.name}" />
</bean>

Spring EL表达式

这里就不详解EL的各种用法了,只是介绍一下注意事项和Spring EL能完成那些操作。

基本表达式

@Test
public void basic() {
    ExpressionParser parser = new SpelExpressionParser();
    // String操作
    System.out.println(parser.parseExpression("'Hello World'.concat('!')").getValue(String.class));

    // 运算符
    System.out.println(parser.parseExpression("10+2-3*4/2").getValue(Integer.class));

    // 访问静态变量 T表示类型,如果没有指定包,默认是java.lang下的类
    System.out.println(parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class));
    // 和上一个等价
    System.out.println(parser.parseExpression("T(java.lang.Integer).MAX_VALUE").getValue(int.class));

    // 访问静态方法
    System.out.println(parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class));
    
    // 逻辑表达式
    System.out.println(parser.parseExpression("2>1 and (!true or !false)").getValue(boolean.class));
}

模板

@Test
public void templateWorld(){
    ExpressionParser parser = new SpelExpressionParser();
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("bj", "北京");
    context.setVariable("cd", "成都");
    context.setVariable("sh", "上海");
    String template = "Hello #{#bj},你好 #{#cd},Hi #{#sh}";
    Expression expression = parser.parseExpression(template, new TemplateParserContext());
    System.out.println(expression.getValue(context, String.class));
}

其中#{}是TemplateParserContext模板指定的表达式,表达式中的#表示引用变量。

没有指定ParserContext,就不能解析多个,只能解析单个变量,如:

@Test
public void templateDefault(){
    ExpressionParser parser = new SpelExpressionParser();
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("bj", "北京");
    Expression expression = parser.parseExpression("#bj");
    System.out.println(expression.getValue(context, String.class));
}

函数表达式

EL中还有一个非常有用的就是可以引用函数。

public class SpringELFunctionTest {
    public static Integer add(Integer x, Integer y) {
            return x + y;
    }

    @Test
    public void functionTest() throws NoSuchMethodException {
        String exp = "#{ #add(4,5)}";
        StandardEvaluationContext context = new StandardEvaluationContext();
        Method add = SpringELFunctionTest.class.getDeclaredMethod("add", Integer.class, Integer.class);
        context.registerFunction("add", add);
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(exp, new TemplateParserContext());
        System.out.println(expression.getValue(context, Integer.class));
    }
}

还可以带参数:

@Data
private static class Param{
    private Date birthday;
    private Integer id;
    private String name;
}

private static class Fun{
    public String getParam(Param param){
        return "hello " + param.toString();
    }
}

@Test
public void assignTest() {
    String exp = "#{#fun.getParam(#param)} 啊哈娘子";
    StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
    Fun fun = new Fun();
    evaluationContext.setVariable("fun", fun);
    Param param = new Param();
    param.setId(1);
    param.setBirthday(new Date());
    param.setName("tim");
    evaluationContext.setVariable("param", param);
    ExpressionParser parser = new SpelExpressionParser();
    TemplateParserContext parserContext = new TemplateParserContext();
    Expression expression = parser.parseExpression(exp, parserContext);
    System.out.println(expression.getValue(evaluationContext, String.class));
}

Spring EL定制

Spring因为${}是属性表达式,所以,EL的表达式默认是#{}

如果,给业务用的表达式我们希望是${},该怎么做呢?

例如:

表达式:

${name}-${year}年${month}月${day}日-${quarter}季报.xlsx

计算之后得到类似:阿宝基金-2024年$12月31日-4季报.xlsx

一看,简单指定TemplateParserContext就可以,那么下面的方式可以行吗?

@Test
public void templateDefault(){
    ExpressionParser parser = new SpelExpressionParser();
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("name", "阿宝基金");
    context.setVariable("year", "2024");
    context.setVariable("month", "12");
    context.setVariable("day", "31");
    context.setVariable("quarter", "4");
    String template = "${name}-${year}年${month}月${day}日-${quarter}季报.xlsx";
    TemplateParserContext parserContext = new TemplateParserContext("${","}");
    Expression expression = parser.parseExpression(template, parserContext);
    System.out.println(expression.getValue(context, String.class));
}

答案是,不行,表达式应该如下:

String template = "${#name}-${#year}年${#month}月${#day}日-${#quarter}季报.xlsx";

上面的表达式,看起来不够简化,我就希望使用下面这个表达式,怎么办?

String template = "${name}-${year}年${month}月${day}日-${quarter}季报.xlsx";

可以利用StandardEvaluationContext的root对象。

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Map;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomParam {

    private Integer year;
    private Integer month;
    private Integer day;
    private Integer quarter;
    private String name;

    private Map<String,Object> paramMap;

    public static Integer staticMethod(){
        System.out.println("执行staticMethod");
        return 12;
    }

    public String instanceMethodParam(String... params){
        StringBuilder sb = new StringBuilder();
        for(String p : params){
            sb.append(p).append("@");
        }
        return sb.toString();
    }
}
@Test
public void templateRoot(){
    ExpressionParser parser = new SpelExpressionParser();
    HashMap<String, Object> map = new HashMap<>();
    map.put("p1","pa");
    map.put("p2","pb");
    CustomParam param = CustomParam.builder()
            .name("阿宝基金")
            .year(2024)
            .month(12)
            .day(31)
            .quarter(4)
            .paramMap(map)
            .build();
    EvaluationContext context = new StandardEvaluationContext(param);
    String template = "${name}-${year}年${month}月${day}日-${quarter}季报.xlsx";
    TemplateParserContext parserContext = new TemplateParserContext("${","}");

    Expression expression = parser.parseExpression(template, parserContext);
    // 阿宝基金-2024年12月31日-4季报.xlsx
    System.out.println(expression.getValue(context, String.class));

    template = "function-${staticMethod()}-${instanceMethodParam('Hello','Hi','World')}.xlsx";
    expression = parser.parseExpression(template, parserContext);
    // function-12-Hello@Hi@World@.xlsx
    System.out.println(expression.getValue(context, String.class));

    template = "map-${paramMap['p1']}-${paramMap['p2']}.xlsx";
    expression = parser.parseExpression(template, parserContext);
    // map-pa-pb.xlsx
    System.out.println(expression.getValue(context, String.class));
}

重点在:EvaluationContext context = new StandardEvaluationContext(param);

StandardEvaluationContext的表达式,没有#默认是找root的对象的对应属性,

例如:

${name}就等价于${#root.name}

前面没有设置StandardEvaluationContext的root,root为空,所以:

${name}-${year}年${month}月${day}日-${quarter}季报.xlsx表达式自然有问题。

设置了root对象,能找到对应属性,自然就没问题了。

我们可以看到,不仅仅可以访问对应的属性,可以访问对应的实例方法和静态方法。

设置map,可以提供参数的灵活性。

引用Spring的bean

在Spring EL还可以引用Spring容器中的Bean,通过@,Spring中所有的类和方法都可以引用,非常灵活。

import lombok.Getter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
@Getter
public class ApplicationContextHolder implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public <T> T getBean(Class<T> clazz){
       return applicationContext.getBean(clazz);
    }
}
import org.springframework.stereotype.Service;

@Service(value="elService")
public class ELService {

    public String service(){
        return "el service";
    }
}
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import vip.meet.common.spring.ApplicationContextHolder;

@SpringBootTest
public class SpringELBeanTest {

    @Resource
    private ApplicationContextHolder applicationContextHolder;

    @Test
    public void bean() {
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        BeanFactoryResolver beanResolver = new BeanFactoryResolver(applicationContextHolder.getApplicationContext());
        context.setBeanResolver(beanResolver);
        String result = parser.parseExpression("@elService.service()")
                .getValue(context, String.class);
        System.out.println(result);
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值