Spring - SpEL

1 概述

Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”。在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。

SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用。

2 使用场景

  1. Bean 的香瓜属性的配置
  2. 结合 AOP 完成业务系统的日志记录

3 功能概览

SpEL支持如下表达式:

一、基本表达式: 字面量表达式、关系,逻辑与算数运算表达式、字符串连接及截取表达式、三目运算及Elivis表达式、正则表达式、括号优先级表达式;

二、类相关表达式: 类类型表达式、类实例化、instanceof表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean引用;

三、集合相关表达式: 内联List、内联数组、集合,字典访问、列表,字典,数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义;

四、其他表达式:模板表达式。

注:SpEL表达式中的关键字是不区分大小写的

4 基本使用

4.1 使用步骤

1)创建解析器:SpEL使用ExpressionParser接口表示解析器,提供 SpelExpressionParser默认实现;
2)解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为Expression对象。
3)构造上下文:准备比如变量定义等等表达式需要的上下文数据。
4)求值:通过Expression接口的getValue方法根据上下文获得表达式值。

示例:

public class SpelTest {
    @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));
    }
}

4.2 原理

一、表达式: 表达式是表达式语言的核心,所以表达式语言都是围绕表达式进行的,从我们角度来看是“干什么”;

二、解析器: 用于将字符串表达式解析为表达式对象,从我们角度来看是“谁来干”;

三、上下文: 表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等,从我们角度看是“在哪干”;

四、根对象及活动上下文对象: 根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象,从我们角度看是“对谁干”

4.3 基本语法

4.3.1 字面量表达式

SpEL支持的字面量包括:字符串、数字类型(int、long、float、double)、布尔类型、null类型。


image.png

4.3.2 算数运算表达式

image.png

4.3.3 关系表达式

等于(==)、不等于(!=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=),区间(between)运算。

如parser.parseExpression("1>2").getValue(boolean.class);将返回false;

而parser.parseExpression("1 between {1, 2}").getValue(boolean.class);将返回true。

between运算符右边操作数必须是列表类型,且只能包含2个元素。第一个元素为开始,第二个元素为结束,区间运算是包含边界值的,即 xxx>=list.get(0) && xxx<=list.get(1)。

SpEL同样提供了等价的“EQ” 、“NE”、 “GT”、“GE”、 “LT” 、“LE”来表示等于、不等于、大于、大于等于、小于、小于等于,不区分大小写。

4.3.4 逻辑表达式

且(and或者&&)、或(or或者||)、非(!或NOT)。

public void test4() {
    ExpressionParser parser = new SpelExpressionParser();

    boolean result1 = parser.parseExpression("2>1 and (!true or !false)").getValue(boolean.class);
    boolean result2 = parser.parseExpression("2>1 && (!true || !false)").getValue(boolean.class);

    boolean result3 = parser.parseExpression("2>1 and (NOT true or NOT false)").getValue(boolean.class);
    boolean result4 = parser.parseExpression("2>1 && (NOT true || NOT false)").getValue(boolean.class);
}

4.3.5 变量定义及引用

变量定义通过EvaluationContext接口的setVariable(variableName, value)方法定义;在表达式中使用"#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);
}

输出:
路人甲java
Spring系列
我是root对象
我是root对象

4.3.6 对象属性及安全导航表达式

对象属性获取非常简单,即使用如“a.property.property”这种点缀式获取,SpEL对于属性名首字母是不区分大小写的;SpEL还引入了Groovy语言中的安全导航运算符“(对象|属性)?.属性”,用来避免“?.”前边的表达式为null时抛出空指针异常,而是返回null;修改对象属性值则可以通过赋值表达式或Expression接口的setValue方法修改。

4.3.7 Bean 引用

SpEL支持使用“@”符号来引用Bean,在引用Bean时需要使用BeanResolver接口实现来查找Bean,Spring提供BeanFactoryResolver实现。
示例:

public void test6() {
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    User user = new User();
    Car car = new Car();
    car.setName("保时捷");
    user.setCar(car);
    factory.registerSingleton("user", user);

    StandardEvaluationContext context = new StandardEvaluationContext();
    context.setBeanResolver(new BeanFactoryResolver(factory));

    ExpressionParser parser = new SpelExpressionParser();
    User userBean = parser.parseExpression("@user").getValue(context, User.class);
    System.out.println(userBean);
    System.out.println(userBean == factory.getBean("user"));
}

4.3.8 集合相关表达式

1. 内联List

从Spring3.0.4开始支持内联List,使用{表达式,……}定义内联List,如“{1,2,3}”将返回一个整型的ArrayList,而“{}”将返回空的List,对于字面量表达式列表,SpEL会使用java.util.Collections.unmodifiableList方法将列表设置为不可修改。

public void test7() {
    ExpressionParser parser = new SpelExpressionParser();
    //将返回不可修改的空List
    List<Integer> result2 = parser.parseExpression("{}").getValue(List.class);
    //对于字面量列表也将返回不可修改的List
    List<Integer> result1 = parser.parseExpression("{1,2,3}").getValue(List.class);
    Assert.assertEquals(new Integer(1), result1.get(0));
    try {
        result1.set(0, 2);
    } catch (Exception e) {
        e.printStackTrace();
    }
    //对于列表中只要有一个不是字面量表达式,将只返回原始List,
    //不会进行不可修改处理
    String expression3 = "{{1+2,2+4},{3,4+4}}";
    List<List<Integer>> result3 = parser.parseExpression(expression3).getValue(List.class);
    result3.get(0).set(0, 1);
    System.out.println(result3);
    //声明二维数组并初始化
    int[] result4 = parser.parseExpression("new int[2]{1,2}").getValue(int[].class);
    System.out.println(result4[1]);
    //定义一维数组并初始化
    int[] result5 = parser.parseExpression("new int[1]").getValue(int[].class);
    System.out.println(result5[0]);
}

输出:

java.lang.UnsupportedOperationException
 at java.util.Collections$UnmodifiableList.set(Collections.java:1311)
 at com.javacode2018.spel.SpelTest.test7(SpelTest.java:315)
[[1, 6], [3, 8]]
2
0

2. 集合,字典元素访问

SpEL目前支持所有集合类型和字典类型的元素访问,使用“集合[索引]”访问集合元素,使用“map[key]”访问字典元素;

//SpEL内联List访问  
int result1 = parser.parseExpression("{1,2,3}[0]").getValue(int.class);  

//SpEL目前支持所有集合类型的访问  
Collection<Integer> collection = new HashSet<Integer>();  
collection.add(1);  
collection.add(2);  

EvaluationContext context2 = new StandardEvaluationContext();  
context2.setVariable("collection", collection);  
int result2 = parser.parseExpression("#collection[1]").getValue(context2, int.class);  

//SpEL对Map字典元素访问的支持  
Map<String, Integer> map = new HashMap<String, Integer>();  
map.put("a", 1);  

EvaluationContext context3 = new StandardEvaluationContext();  
context3.setVariable("map", map);  
int result3 = parser.parseExpression("#map['a']").getValue(context3, int.class);  

3. 列表、字典、数组元素修改

可以使用赋值表达式或Expression接口的setValue方法修改;

@Test
public void test8() {
    ExpressionParser parser = new SpelExpressionParser();

    //修改list元素值
    List<Integer> list = new ArrayList<Integer>();
    list.add(1);
    list.add(2);

    EvaluationContext context1 = new StandardEvaluationContext();
    context1.setVariable("collection", list);
    parser.parseExpression("#collection[1]").setValue(context1, 4);
    int result1 = parser.parseExpression("#collection[1]").getValue(context1, int.class);
    System.out.println(result1);

    //修改map元素值
    Map<String, Integer> map = new HashMap<String, Integer>();
    map.put("a", 1);
    EvaluationContext context2 = new StandardEvaluationContext();
    context2.setVariable("map", map);
    parser.parseExpression("#map['a']").setValue(context2, 4);
    Integer result2 = parser.parseExpression("#map['a']").getValue(context2, int.class);
    System.out.println(result2);
}
输出:
4
4

5 示例 -- 操作日志

1. controller

@TenantLog(module = "custer", description = "get ...", content = "parameter: id = #{ #customer?.id }, "
      + "accountId = #{ #customer?.accountId}, orderNumber = #{ #customer.orderNumber }, des = #{ #customer.description }")
  @RequestMapping(value = "/create", method = RequestMethod.POST)
  public CustomerTicket createCustomerTicket(@RequestBody CustomerTicket customer) {

    log.info("customer is 888888 {}", customer.toString());

    CustomerTicket customerTicket = new CustomerTicket();

    customerTicket.setId(customer.getId());

    customerTicket.setAccountId(customer.getAccountId());

    customerTicket.setOrderNumber(customer.getOrderNumber());

    customerTicket.setDescription(customer.getDescription());

    customerTicket.setCreateTime(new Date());

    return customerTicket;
  }


  @TenantLog(module = "custer", description = "get ...", content = "parameter: #{ #customer.toString() }")
  @RequestMapping(value = "/create1", method = RequestMethod.POST)
  public CustomerTicket createCustomerTicket1(@RequestBody CustomerTicket customer) {

    log.info("customer is 888888 {}", customer.toString());

    CustomerTicket customerTicket = new CustomerTicket();

    customerTicket.setId(customer.getId());

    customerTicket.setAccountId(customer.getAccountId());

    customerTicket.setOrderNumber(customer.getOrderNumber());

    customerTicket.setDescription(customer.getDescription());

    customerTicket.setCreateTime(new Date());

    return customerTicket;
  }

2. AOP + SpEL

@Slf4j
@Aspect
@Component
public class TenantLogOperateAspect {


  // 需要被SpEl解析的模板前缀和后缀 {{ expression  }}
  public static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext("#{", "}");

  // SpEL解析器
  public static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();


  @Pointcut("@annotation(com.chenjunjie.webpro.aop.TenantLog)")
  public void tenantLogPointcut() {
  }

  @Around("tenantLogPointcut()")
  public Object addAspect(ProceedingJoinPoint joinPoint) throws Throwable {


    MethodSignature signature = (MethodSignature) joinPoint.getSignature();

    // 参数
    Object[] args = joinPoint.getArgs();

    // 参数名称
    String[] parameterNames = signature.getParameterNames();

    // 目标方法
    Method targetMethod = signature.getMethod();

    TenantLog operationLog = targetMethod.getAnnotation(TenantLog.class);


    // request
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

    try {
      /**
       * SpEL解析的上下文,把 HandlerMethod 的形参都添加到上下文中,并且使用参数名称作为KEY
       */
      EvaluationContext evaluationContext = new StandardEvaluationContext();
      for (int i = 0; i < args.length; i ++) {
        evaluationContext.setVariable(parameterNames[i], args[i]);
      }

      String logContent = EXPRESSION_PARSER.parseExpression(operationLog.content(), TEMPLATE_PARSER_CONTEXT).getValue(evaluationContext, String.class);

      // TODO 异步存储日志

      System.out.println("**************************");
      log.info("operationLog={}", logContent);
      System.out.println("**************************");
      
      // 执行方法
      Object proceed = joinPoint.proceed();

      return proceed;


    } catch (Exception e) {
      log.error("操作日志SpEL表达式解析异常: {}", e.getMessage());
    }

    return null;
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容