Spring Framework 4.1 包括一个基本的表达式编译器。表达式通常是被解释的,这在评估过程中提供了很多动态的灵活性,但并没有提供最佳的性能。对于偶尔使用表达式来说,这很好,但是,当被其他组件(如 Spring Integration)使用时,性能可能非常重要,而且对动态性没有实际需要。

SpEL 编译器就是为了满足这一需求。在评估过程中,编译器会生成一个 Java 类,在运行时体现表达式的行为,并使用该类来实现更快的表达式评估。由于缺乏围绕表达式的类型,编译器在执行编译时使用在表达式的解释评估中收集的信息。例如,它并不纯粹从表达式中知道属性引用的类型,但在第一次解释评估期间,它发现了它是什么。当然,如果各种表达式元素的类型随着时间的推移而改变,那么基于这种派生信息的编译会在以后造成麻烦。由于这个原因,编译最适合于那些类型信息不会在重复求值时改变的表达式。

考虑以下基本表达式:

  1. someArray[0].someProperty.someOtherProperty < 0.1

因为前面的表达式涉及到数组访问、一些属性去引用和数字操作,所以性能的提升是非常明显的。在一个50000 次迭代的微观基准运行的例子中,使用解释器评估需要 75ms,而使用该表达式的编译版本只需要 3ms。

编译器配置

编译器默认是不开启的,但你可以通过两种不同的方式开启它。你可以通过使用解析器配置过程(前面讨论过)来打开它,或者当 SpEL 的使用被嵌入到其他组件中时,通过使用 Spring 属性来打开它。本节将讨论这两个选项。

编译器可以在三种模式中的一种运行,这些模式在org.springframework.expression.spel.SpelCompilerMode枚举中被定义。这些模式如下:

  • OFF(默认):编译器被关闭
  • IMMEDIATE:在即时模式下,表达式被尽快编译。这通常是在第一次解释的评估之后。如果编译的表达式失败(通常是由于类型改变,如前所述),表达式评估的调用者会收到一个异常。
  • MIXED:在混合模式下,表达式随着时间的推移在解释模式和编译模式之间默默地切换。在经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(比如类型改变,如前所述),表达式会自动再次切换到解释形式。稍后的某个时候,它可能会生成另一个编译形式并切换到它。基本上,用户在 IMMEDIATE 模式下得到的异常反而被内部处理。

IMMEDIATE 模式的存在是因为 MIXED 模式可能会给有副作用的表达式带来问题。如果一个已编译的表达式在部分成功后被炸毁,它可能已经做了一些影响系统状态的事情。如果发生了这种情况,调用者可能不希望它在解释模式下默默地重新运行,因为表达式的一部分可能会运行两次。

在选择了一种模式之后,使用 SpelParserConfiguration 来配置分析器。下面的例子显示了如何做到这一点:

  1. package cn.mrcode.study.springdocsread.data;
  2. import org.springframework.expression.Expression;
  3. import org.springframework.expression.ExpressionParser;
  4. import org.springframework.expression.spel.SpelCompilerMode;
  5. import org.springframework.expression.spel.SpelParserConfiguration;
  6. import org.springframework.expression.spel.standard.SpelExpressionParser;
  7. import java.util.List;
  8. /**
  9. * @author mrcode
  10. */
  11. public class DemoTest {
  12. public static void main(String[] args) {
  13. SpelParserConfiguration config = new SpelParserConfiguration(
  14. // 设置模式
  15. SpelCompilerMode.IMMEDIATE,
  16. // 设置类加载器
  17. DemoTest.class.getClassLoader());
  18. SpelExpressionParser parser = new SpelExpressionParser(config);
  19. Expression expr = parser.parseExpression("payload");
  20. Demo message = new Demo();
  21. Object payload = expr.getValue(message);
  22. }
  23. static class Demo {
  24. public String payload;
  25. }
  26. }

当你指定编译器模式时,你也可以指定一个 classloader(允许传空)。被编译的表达式被定义在任何提供的子类加载器下创建的子类加载器中。重要的是要确保,如果指定了一个 classloader,它可以看到参与表达式评估过程的所有类型。如果你没有指定类加载器,就会使用默认的类加载器(通常是表达式评估过程中运行的线程的上下文类加载器)。

配置编译器的第二种方式是用于 SpEL 被嵌入到其他组件中,并且可能无法通过配置对象进行配置的情况。在这些情况下,可以通过 JVM 系统属性(或通过 SpringProperties 机制)将spring.expression.compiler.mode属性设置为 SpelCompilerMode枚举值之一(关闭、立即或混合)。

编译器限制

从 Spring Framework 4.1 开始,基本的编译框架已经到位了。然而,该框架还不支持编译所有类型的表达式。最初的重点是那些可能在性能关键环境下使用的常见表达式。以下种类的表达式目前还不能被编译:

  • 涉及赋值的表达式
  • 依赖于转换服务的表达式
  • 使用自定义解析器或访问器的表达式
  • 使用选择或投影的表达方式(Expressions using selection or projection)