简介
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中:
- ${}是属性表达式,引用的是properties和yaml配置文件中的属性值
- #{}是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);
}
}