Spring SpEL表达式

Spring Expression Language(简称 SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于 Unified EL,但提供了额外的特性,最显著的是方法调用和基本的字符串模板功能。最初使用SpEL表达式时,我总是错以为SpEL表达式的语法和Java一致,事实上二者相去甚远,本文会详细介绍SpEL表达式的工作原理以及语法。

前言

表达式语言(Expression Language),或称 EL 表达式,简称 EL,是 Java 中的一种特殊的通用编程语言,借鉴于 JavaScript 和 XPath。主要作用是在 Java Web 应用程序嵌入到网页(如 JSP)中,用以访问页面的上下文以及不同作用域中的对象,取得对象属性的值,或执行简单的运算或判断操作。EL 在得到某个数据时,会自动进行数据类型的转换。

Java 中已经有多种 EL 表达式语言,如 OGNL、MVEL 和 JBoss EL,为什么还需要 SpEL 呢?创建 SpEL 的目的是为 Spring 社区提供一种支持良好的表达式语言,可以跨 Spring 产品组合中的所有产品使用。它的语言特性是由 Spring 项目组合中的项目需求驱动的,包括一些插件工具工具需求。SpEL 基于一个与技术无关的 API,它允许集成其他表达式语言实现。

虽然 SpEL 的初衷是为了只是 Spring 框架的表达式语言,但是它并没有和 Spring 强绑定。SpEL 可以独立于 Spring 的其它模块被使用,它只需要用户创建一些 SpEL 表达式的基本对象(如语言解析器),就可以进行表达式的计算。

 

SpEL表达式计算

我们前面说过,SpEL是一种表达式语言,可以脱离Spring容器使用。下面我们用一个例子展示怎么使用SpEL表达式,可以看到计算SpEL表达式的流程主要有两步:

  1. 解析字符串表达式为Expression类型的数据;
  2. 计算Expression的值。
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); 
String message = (String) exp.getValue();//The value of the message variable is 'Hello World'.

表达式解析ExpressionParser

ExpressionParser用于把一个字符串类型的表达式转换为Spring可以用于计算的Expression。主要包含语法分析和词法分析两部分,解析后的表达式就已经可以真正进行计算了。

public interface ExpressionParser {
    Expression parseExpression(String var1) throws ParseException;

    Expression parseExpression(String var1, ParserContext var2) throws ParseException;
}

表达式上下文EvaluationContext

EvaluationContext接口用于计算表达式以解析属性、方法或字段并帮助执行类型转换。在前面的例子中,我们并没有使用表达式的上下文,下面的例子中是用来表达式上下文。可以看到,我们在计算表达式parser.parseExpression("booleanList[0]").setValue(context, simple, "false");时,我们需要访问Simple的booleanList并把字符串”false”转为Bool类型,这些工作就是由表达式上下文完成的。

class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

Simple simple = new Simple();
simple.booleanList.add(true);

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b is false
Boolean b = simple.booleanList.get(0);

Spring 提供了两种类型的表达式上下文:

  • SimpleEvaluationContext:它不包括 Java类型引用,构造函数和bean引用。它还要求明确选择对表达式中属性和方法的支持级别。默认情况下,create()静态工厂方法只启用对属性的读取访问。您还可以获取构建器以配置所需的确切支持级别,并将目标作为以下一项或几项的组合:PropertyAccessor仅自定义(不反射)/用于只读访问的数据绑定属性/用于读取和写入的数据绑定属性
  • 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

表达式解析的配置

集合相关的表达式解析配置

把字符串解析为对应的语法是一个很复杂的过程,Spring的解析过程允许用户添加配置解析配置,并提供了对应的配置类:SpelParserConfiguration。通过这个类,我们可以配置SpEL中的List自增或者元素不存在的时候new一个对象等。下面的例子展示了如何配置Parse允许自增和控制自动初始化。

class Demo {
    public List<String> list;
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression("list[3]");

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String

编译模式配置

springf4.1新增了基本的表达式编译器。通常情况下我们会通过解释的方法去执行表达式,这具有很大的动态灵活性,但性能会比较差。如果表达式被调用的频率不高,这就没什么问题,但是当性能比较重要时,就需要通过编译来提升性能了。

表达式的编译模式允许为表达式生成class文件,从而提升表达式的执行效率。在计算表达式时,编译器会生成一个Java类,该类的运行结果和表达式相同,但是效率会比解释表达式高很多。在第一次运行表达式之前,是无法确定表达式的输入和返回值的,所以编译器需要收集表达式解释执行期间的执行信息。例如,它不知道表达式中的属性引用的类型,但是在第一个解释执行中,它会发现它是什么。当然,如果各种表达式元素的类型随着时间的推移而变化,会导致之前推断的结果失效。因此,编译最适合类型信息不会在重复计算中更改的表达式。

我们通过以下表达式来评估编译模式表达式的性能优势,迭代执行50000次下列的表达式,编译模式耗时3ms,解释模式耗时75ms。

someArray[0].someProperty.someOtherProperty < 0.1

我们可以参照以下示例配置表达式的编译模式:

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
    this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);

SpEL 表达式功能

简单文本表达式

简单文本表达式是指以字符串书写的简单文本表达式,如下文的代码中,我们有一个文本表达式”1+2”,Spring 会解析这个表达式为语法树,然后计算返回 3。

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

Bool 类型及其运算

pEl 支持 Bool 类型及其运算,计算布尔值时候允许我们使用某些字符来代替对应的符号,如 ge(>=)、gt(>)、lt(<)、le(<=)、eq(==)、ne(!=)、div(/)、mod(%)、not(!),而且它们都是大小写不敏感的。使用时中间要以空格分开,示例如下:

ExpressionParser parser = new SpelExpressionParser();
Assert.assertTrue(parser.parseExpression("1 lt 2").getValue(boolean.class));//1<2
Assert.assertTrue(parser.parseExpression("1 le 2").getValue(boolean.class));//1<=2
Assert.assertTrue(parser.parseExpression("2 gt 1").getValue(boolean.class));//2>1
Assert.assertTrue(parser.parseExpression("2 ge 1").getValue(boolean.class));//2>=1
Assert.assertTrue(parser.parseExpression("1 ne 2").getValue(boolean.class));//1!=2
Assert.assertTrue(parser.parseExpression("not false").getValue(boolean.class));//!false

正则表达式

SpEl 也支持使用正则表达式,其中对应的关键字为 match。如下示例中即在表达式中使用了正则表达式,表示 123 是否匹配正则表达式“\d{3}”。

ExpressionParser parser = new SpelExpressionParser();
Assert.assertTrue(parser.parseExpression("123 matches '\\d{3}'").getValue(Boolean.class));//正则匹配三位数字

类表达式

SpEL 支持获取 java 类的信息,如下的示例中,表达式中使用 instanceof 关键字,以检测对象是否是特定类型的示例。

ExpressionParser parser = new SpelExpressionParser();
Assert.assertTrue(parser.parseExpression("'123' instanceof T(String)").getValue(Boolean.class));//检测字符串是否是String的实例。

访问数组和集合

在 SpEl 中我们可以通过索引的形式访问 List 或 Array 的某一个元素,对应的索引是从 0 开始的,以“list[index]”的形式出现。

访问List示例

@Test
public void test() {
    Object user = new Object() {
        public List<String> getInterests() {
            List<String> interests = Arrays.asList(new String[] {"BasketBall", "FootBall"});
            return interests;
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    Assert.assertTrue(parser.parseExpression("interests[0]").getValue(user, String.class).equals("BasketBall"));
    Assert.assertTrue(parser.parseExpression("interests[1]").getValue(user, String.class).equals("FootBall"));
}

访问数组示例

@Test
public void test() {
    Object user = new Object() {
        public String[] getInterests() {
            return new String[] {"BasketBall", "FootBall"};
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    Assert.assertTrue(parser.parseExpression("interests[0]").getValue(user, String.class).equals("BasketBall"));
    Assert.assertTrue(parser.parseExpression("interests[1]").getValue(user, String.class).equals("FootBall"));
}

访问Map集合

对于 Map 而言,则是通过类似于“map[key]”的形式访问对应的元素的。示例如下

@Test
public void test() {
    Object user = new Object() {
        public Map<String, String> getInterests() {
            Map<String, String> interests = new HashMap<String, String>();
            interests.put("key1", "BasketBall");
            interests.put("key2", "FootBall");
            return interests;
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    Assert.assertTrue(parser.parseExpression("interests['key1']").getValue(user, String.class).equals("BasketBall"));
    Assert.assertTrue(parser.parseExpression("interests['key2']").getValue(user, String.class).equals("FootBall"));
}

集合构造

构造List

在 SpEl 中可以使用“{e1,e2,e3}”的形式来构造一个 List,如下示例中我们就构造了一个 List。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    List<Integer> intList = (List<Integer>)parser.parseExpression("{1,2,3,4,5,6}").getValue();
    int index = 0;
    for (Integer i : intList) {
        Assert.assertTrue(i == ++index);
    }
}

构造Map

Map 是可以 key-value 的形式存在的,在 SpEl 中如果需要构造一个 Map,则可以使用“{key1:value1,key2:value2}”这样的形式进行定义,即使用大括号包起来,然后 key 和 value 之间以冒号“:”分隔构成一个 Entry,多个 Entry 之间以逗号分隔。如下示例中我们就构建了一个 key 为 String,value 为 Long 类型的 Map。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    Map<String, Long> map = (Map<String, Long>)parser.parseExpression("{'key1':1L,'key2':2L}").getValue();
    Assert.assertTrue(map.get("key1").equals(1L));
    Assert.assertTrue(map.get("key2").equals(2L));
}

集合选择

SpEl 允许将集合中的某些元素选出组成一个新的集合进行返回,这就是所谓的集合选择。

List集合选择

假设我们有一个 List,其包含 1-9 共 9 个数字,通过集合选择的功能,我们可以选出其中的奇数组成一个新的 List 进行返回,即 1、3、5、7、9。集合的选择使用的语法是“collection.?[condition]”,condition 中直接使用的属性、方法等都是针对于集合中的元素来的。如下示例中我们的 user 对象的 getInterests()方法返回包含三个元素的 List,然后我们通过 endsWith(‘Ball’)筛选出以 Ball 结尾的元素组成一个新的 List。

@Test
public void test() {
    Object user = new Object() {
        public List<String> getInterests() {
            List<String> interests = new ArrayList<String>();
            interests.add("BasketBall");
            interests.add("FootBall");
            interests.add("Movie");
            return interests;
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    List<String> interests = (List<String>)parser.parseExpression("interests.?[endsWith('Ball')]").getValue(user);
    Assert.assertTrue(interests.size() == 2);
    Assert.assertTrue(interests.get(0).equals("BasketBall"));
    Assert.assertTrue(interests.get(1).equals("FootBall"));
}

Map集合选择

对于 Map 的选择而言,其中的 condition 中直接使用的属性和方法针对的主体都是 Map 的 Entry。如下示例中我们通过条件 value.endsWith(‘Ball’)选出 Map 中 value 以 Ball 结尾的 Entry 组成一个新的 Map 进行返回,对应的条件相当于 Entry.getValue().endsWith(“Ball”)。

@Test
public void test() {
    Object user = new Object() {
        public Map<String, String> getInterests() {
            Map<String, String> interests = new HashMap<String, String>();
            interests.put("key1", "BasketBall");
            interests.put("key2", "FootBall");
            interests.put("key3", "Movie");
            return interests;
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    Map<String, String> interests = (Map<String, String>)parser.parseExpression("interests.?[value.endsWith('Ball')]").getValue(user);
    Assert.assertTrue(interests.size() == 2);
    Assert.assertTrue(interests.get("key1").equals("BasketBall"));
    Assert.assertTrue(interests.get("key2").equals("FootBall"));
}

集合投影

集合投影的意思是将集合中每个元素的某部分内容的组成一个新的集合进行返回。集合投影的语法是“collection.![projectionExpression]”

List集合投影

集合投影的意思是将集合中每个元素的某部分内容的组成一个新的集合进行返回。集合投影的语法是“collection.![projectionExpression]”,其中 projectionExpression 中直接使用的属性和方法都是针对于 collection 中的每个元素而言的,对于 List 而言其就表示 List 中的每个元素,对于 Map 而言,其就表示 Map 中的每个 Entry。在如下示例中我们就将 List 中的每一个元素调用 endsWith()方法后的结果组成一个新的 List 进行返回。

@Test
public void test() {
    Object user = new Object() {
        public List<String> getInterests() {
            List<String> interests = new ArrayList<String>();
            interests.add("BasketBall");
            interests.add("FootBall");
            interests.add("Movie");
            return interests;
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    List<Boolean> interests = (List<Boolean>)parser.parseExpression("interests.![endsWith('Ball')]").getValue(user);
    Assert.assertTrue(interests.size() == 3);
    Assert.assertTrue(interests.get(0).equals(true));
    Assert.assertTrue(interests.get(1).equals(true));
    Assert.assertTrue(interests.get(2).equals(false));
}

Map集合投影

Map 进行投影的结果是一个 List。如下示例中我们就将一个 Map 的 value 投影为一个 List,对应 List 中元素的顺序是不定的。

@Test
public void test() {
    Object user = new Object() {
        public Map<String, String> getInterests() {
            Map<String, String> interests = new HashMap<String, String>();
            interests.put("key1", "BasketBall");
            interests.put("key2", "FootBall");
            interests.put("key3", "Movie");
            return interests;
        }
    };
    ExpressionParser parser = new SpelExpressionParser();
    List<String> interests = (List<String>)parser.parseExpression("interests.![value]").getValue(user);
    Assert.assertTrue(interests.size() == 3);
    for (String interest : interests) {
        Assert.assertTrue(interest.equals("BasketBall") || interest.equals("FootBall") || interest.equals("Movie"));
    }
}

访问实例方法

在 SpEl 表达式中我们也可以直接访问对象的方法。在下述示例中我们就直接在 SpEl 中访问了字符串的 length()方法

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    //直接访问String的length()方法。
    Assert.assertTrue(parser.parseExpression("'abc'.length()").getValue().equals(3));
}

关系运算符

常见的关系运算符有>=、>、<、<=、==、!=、!等,SpEL 支持在表达式中使用这些运算符,也允许我们使用某些字符来代替对应的运算符号,如 ge(>=)、gt(>)、lt(<)、le(<=)、eq(==)、ne(!=)、div(/)、mod(%)、not(!),而且它们都是大小写不敏感的。使用时中间要以空格分开,示例如下。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    Assert.assertTrue(parser.parseExpression("1 < 2").getValue(boolean.class));//1 lt 2
    Assert.assertTrue(parser.parseExpression("1 le 2").getValue(boolean.class));//1<=2
    Assert.assertTrue(parser.parseExpression("2 gt 1").getValue(boolean.class));//2>1
    Assert.assertTrue(parser.parseExpression("2 ge 1").getValue(boolean.class));//2>=1
    Assert.assertTrue(parser.parseExpression("1 ne 2").getValue(boolean.class));//1!=2
    Assert.assertTrue(parser.parseExpression("not false").getValue(boolean.class));//!false
}

赋值操作

SpEl 也支持给表达式赋值,其是通过 Expression 的 setValue()方法进行的,在赋值时需要指定 rootObject 或对应的 EvaluationContext。示例如下:

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    Date d = new java.util.Date();
    //设日期为1号
    parser.parseExpression("date").setValue(d, 1);
    int date = (Integer)parser.parseExpression("date").getValue(d);
    Assert.assertTrue(date == 1);
}

List集合赋值

支持 List、Map 等的赋值。对于 List 和 Array 而言,在进行赋值时是通过元素的索引进行的,且对应的索引必须是存在的。如下示例中我们就将 list 的第一个元素由 0 设置为了 1。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    List<Integer> list = new ArrayList<Integer>(1);
    list.add(0);//添加一个元素0
    EvaluationContext context = new StandardEvaluationContext();
    //添加变量以方便表达式访问
    context.setVariable("list", list);
    //设置第一个元素的值为1
    parser.parseExpression("#list[0]").setValue(context, 1);
    int first = (Integer)parser.parseExpression("#list[0]").getValue(context);
    Assert.assertTrue(first == 1);
}

Map集合赋值

对于 Map 的赋值而言是通过 key 进行的,对应的 key 在 Map 中可以不存在。如下示例就是对 Map 的赋值。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    Map<String, Integer> map = new HashMap<String, Integer>();
    EvaluationContext context = new StandardEvaluationContext();
    //添加变量以方便表达式访问
    context.setVariable("map", map);
    //设置第一个元素的值为1
    parser.parseExpression("#map['key1']").setValue(context, 1);
    int first = (Integer)parser.parseExpression("#map['key1']").getValue(context);
    Assert.assertTrue(first == 1);
}

调用构造方法

SpEL 可以使用 new 运算符调用构造函数。除了基元类型(int、float 等)和字符串之外,其他类型都应该使用完全限定的类名。下面的示例演示如何使用新的运算符来调用构造函数:

@Test
public void test() {
    Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);
    Assert.notNull(einstein);
}

引用其它Bean

SpEL 可以通过表达式引用其它 bean 定义的属性。如下示例中我们就通过表达式引用了名为 hello 的 bean 的 key 属性。

<bean id="hello" class="com.app.Hello">
    <property name="key" value="abc"/>
</bean>

<bean id="world" class="com.app.World">
    <property name="key" value="#{hello.key}"/>
</bean>

三目运算

SpEl 也支持在表达式中使用三目运算符,形式为“exp?trueVal:falseVal”,即如果 exp 的值为 true 则返回 trueVal,否则返回 falseVal。

@Test
public void  test() {
    ExpressionParser parser = new SpelExpressionParser();
    Assert.assertTrue(parser.parseExpression("1>2 ? 1 : 2").getValue(int.class) == 2);//1跟2之间的较大者为2。
    Assert.assertTrue(parser.parseExpression("1<2 ? 2 : 1").getValue(int.class) == 2);//1跟2之间的较大者为2。
}

设置和使用变量

SpEL 支持设置变量并在表达式中访问,可以通过 EvaluationContext 的 setVariable()方法进行设置,然后在表达式中使用时,通过“#varName”的形式进行使用。如下示例中我们就给 EvaluationContext 设置了一个名为“user”的变量,然后在表达式中通过“#user”来使用该变量。 

@Test
public void test() {
    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"));
}

表达式模板

SpEL 还支持在解析表达式时将其当做一个字符串模板进行解析,即可以在表达式中混合普通的文本和特定的表达式块,然后在解析的时候将解析将对其中的表达式块进行计算,以实现模板功能。此功能需要我们在解析表达式时传入一个特定的 ParserContext,其可以影响 SpEl 表达式的解析,对应的模板功能应该传递一个 TemplateParserContext。这样 Spring 在解析对应的 SpEl 表达式时将会把其当做一个模板,然后对其中“#{exp}”形式的表达式进行计算。如下示例就是表达式模板的一个简单用法,其中使用#{}包起来的表达式会被当做一个普通的 SpEl 表达式进行计算以得出当前的年份,再进行替换,所以所得结果将是“the year is 2014”。

@Test
public void test() {
    //the year is 2014
    String expressionStr = "the year is #{T(java.util.Calendar).getInstance().get(T(java.util.Calendar).YEAR)}";
    ExpressionParser parser = new SpelExpressionParser();
    Expression expression = parser.parseExpression(expressionStr, new TemplateParserContext());
    Assert.assertTrue(expression.getValue().equals("the year is 2014"));
}

注册方法

StandardEvaluationContext 允许我们在其中注册方法,然后在表达式中使用对应的方法。注册的方法必须是一个 static 类型的公有方法。注册方法是通过 StandardEvaluationContext 的 registerFunction(funName,method)方法进行,其中第一个参数表示需要在表达式中使用的方法名称,第二个参数表示需要注册的 java.lang.reflect.Method。在表达式中我们可以使用类似于“#funName(params…)”的形式来使用对应的方法。如下示例中我们就通过 StandardEvaluationContext 注册了一个名叫 plusTen 的方法。

static class MathUtils {
    public static int plusTen(int i) {
        return i+10;
    }
}

@Test
public void test() throws NoSuchMethodException, SecurityException {
    ExpressionParser parser = new SpelExpressionParser();
    //1、获取需要设置的java.lang.reflect.Method,需是static类型
    Method plusTen = MathUtils.class.getDeclaredMethod("plusTen", int.class);
    StandardEvaluationContext context = new StandardEvaluationContext();
    //2、注册方法到StandardEvaluationContext,第一个参数对应表达式中需要使用的方法名
    context.registerFunction("plusTen", plusTen);
    //3、表达式中使用注册的方法
    Expression expression = parser.parseExpression("#plusTen(10)");
    //4、传递包含对应方法注册的StandardEvaluationContext给Expression以获取对应的值
    int result = expression.getValue(context, int.class);
    Assert.assertTrue(result == 20);
}

获取一个对象的属性:

//获取一个对象的属性,注意getValue时,需要将对象作为入参
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); 
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true

安全导航

我们可能经常会使用类似于“a.b.c”这样的用法,表示 a 的 b 属性的 c 属性,但如果 a 为 null 或者 a 的 b 属性为 null 时都会出现空指针。为了避免此种情况发生,我们可以在 SpEl 表达式中使用安全导航,这样当 a 为 null 或 a 的 b 属性为 null 时将直接返回 null,而不抛出空指针异常。SpEl 表达式中安全导航的语法是将点“.”替换为“?.”,即不使用“a.b.c”,而是使用“a?.b?.c”。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    Assert.assertNull(parser.parseExpression("null?.abc").getValue());
    Assert.assertNull(parser.parseExpression("T(System)?.getProperty('abc')?.length()").getValue());//数字1不为null
}

上面这些不是我们需要重点关注的方式,下面来看下如何在注解中使用SpEl表达式。

注解中使用SpEL表达式

简单字符串注入

@Value("spring boot")
private String spring;

从系统属性中获取并注入

@Value("#{systemProperties['os.name']}")
private String osName;

调用类的方法

@Value("#{T(java.lang.Math).random() * 100}")
private double randomNumber;

输出的是随机的一个0~100的double值,T()中填写的是调用的java中类。它可以直接使用算术运算符计算值 。

获取类的类的属性

首先写一个简单的Person类


import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
@Component
public class Person {
 
    @Value("zhangsan")
    private String pName;
 
    public String getpName() {
        return pName;
    }
 
    public void setpName(String pName) {
        this.pName = pName;
    }
}

获取pName属性:

@Value("#{person.pName}")
private String pName;

注意:这里Person类的pName属性是私有的,所以必须提供get方法,不然会报错,但是如果pName是共有方法,则不需要提供get方法也可以获取值。

链式调用方法

@Value("#{person.getpName().toUpperCase()}")
private String pName;

此时我们输出的结果是"ZHANGSAN",可见成功的调用了String的toUpperCase方法。如果获取出来的值是null会抛出org.springframework.beans.factory.UnsatisfiedDependencyException的异常,那么要怎么避免呢?只需要如下地方加一个"?"号就可以解决了。

@Value("#{person.getpName()?.toUpperCase()}")
private String pName;

?号的作用就是在调用方法之前,先判断结果是不是null值,如果是则直接返回null,如果不是,才会继续调用后面的方法。

算术运算

@Value("#{(1+2)>3}")
public boolean count;

三元运算符

@Value("#{(1+2)>3 ? 'yes' : 'no'}")
public String count;

elvis 运算符

如果有值则输出值,为null则输出默认值

@Value("#{person.getpName()?:'zhangsan'}")
private String pName;

集合类型的运用

先改写Person类,在其中加入了age属性,重新toString方法,无参构造和有参构造。

import org.springframework.stereotype.Component;
 
@Component
public class Person {
 
    public Person() {
    }
 
    public Person(String pName, int age) {
        this.pName = pName;
        this.age = age;
    }
 
    private String pName;
    private int age;
 
    public void setpName(String pName) {
        this.pName = pName;
    }
 
    public String getpName() {
        return pName;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}

然后加入测试bean:

import org.springframework.stereotype.Component;
 
import java.util.*;
 
@Component
public class TestBean {
 
    public List<Person> list = Arrays.asList(new Person("zhangsan",19),new Person("lisi",19), new Person("wangwu",20));
    public Map<String, String> map;
 
    public TestBean(){
 
        map = new HashMap<>();
        map.put("key1","value1");
        map.put("key2","value2");
        map.put("key3","value3");
    }
}

1、查询集合下标为2的元素 

@Value("#{testBean.list[2]}")
private Person person;
//Person{pName='wangwu', age=20}

2、在Map中,取出的Map对应key的值

@Value("#{testBean.map[key1]}")
private String value;
value1

3、查询集合中值对应的对象

@Value("#{testBean.list.?[age==19]}")
private List<Person> person;
//[Person{pName='zhangsan', age=19}, Person{pName='lisi', age=19}]

取出了age为19的对象,注意要拿list去接收,不然会报错。其中.?[]是查询所有,而.^[]是查询第一个满足条件的,.$[]是查询最后一个满足条件的

4、集合投影

@Value("#{testBean.list.![pName]}")
private List<String> pNames;
//[zhangsan, lisi, wangwu]

使用.![]进行集合投影,将Person集合中的pName属性都拿了出来,并且塞到了一个新的集合之中。

 

posted @ 2022-11-22 09:30  残城碎梦  阅读(1473)  评论(0编辑  收藏  举报