Spring Expression Language (кратко - "SpEL") – это мощный язык выражений, который поддерживает запросы и манипуляции с графом объектов во время выполнения программы. Синтаксис языка похож на Unified EL, но предлагает дополнительные возможности, в первую очередь вызов методов и базовые функции шаблонизации строк.

Хотя существует несколько других языков выражений Java - OGNL, MVEL, JBoss EL и другие - язык выражений Spring Expression Language был создан для того, чтобы дать сообществу Spring единый язык выражений с надлежащей поддержкой, который можно использовать во всех продуктах портфеля Spring. Особенности языка определяются требованиями проектов в портфеле Spring, включая требования к инструментарию для поддержки автодополнения кода в Spring Tools for Eclipse. При этом SpEL основан на технологически независимом API-интерфейсе, который позволяет интегрировать другие реализации языка выражений, если возникнет такая необходимость.

Хотя SpEL и является основополагающим продуктом для вычисления выражений в портфеле Spring, он не связан напрямую со Spring и может использоваться самостоятельно. Для демонстрации самодостаточности многие примеры в этой главе используют SpEL так, как если бы он был независимым языком выражений. Это требует создания нескольких классов загрузочной инфраструктуры, таких как парсер. Большинству пользователей Spring не нужно иметь дело с данной инфраструктурой, поэтому вместо этого они могут создавать только строки выражений для вычисления. Примером такого типичного использования является интеграция SpEL в создание определений бинов на основе XML или аннотаций.

В данной главе рассматриваются особенности языка выражений, его API-интерфейс и синтаксис языка. В некоторых местах в качестве целевых объектов для вычисления выражений используются классы Inventor и Society. Объявления этих классов и данные, используемые для их заполнения, перечислены в конце главы.

Язык выражений поддерживает следующую функциональность:

  • Литеральные выражения (литералы);

  • Булевы операторы и операторы отношений;

  • Регулярные выражения;

  • Класс-выражения;

  • Доступ к свойствам, массивам, спискам и ассоциативным массивам;

  • Обращение к методам;

  • Операторы отношений;

  • Присваивание;

  • Вызов конструкторов;

  • Ссылки на бины;

  • Построение массива;

  • Встраиваемые списки;

  • Встраиваемые ассоциативные массивы;

  • Тернарный оператор;

  • Переменные;

  • Определяемые пользователем функции;

  • Проекция коллекций;

  • Выборка коллекций;

  • Шаблонные выражения.

Вычисление

Этот раздел знакомит с простым использованием интерфейсов SpEL и их языка выражений.

Следующий код использует API-интерфейс SpEL для оценки литерального строкового выражения Hello World.

Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); 
String message = (String) exp.getValue();
  1. Значение переменной сообщения равно 'Hello World'.
Kotlin
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") 
val message = exp.value as String
  1. Значение переменной сообщения равно 'Hello World'.

Классы и интерфейсы SpEL, которыми вы, скорее всего, будете пользоваться, находятся в пакете org.springframework.expression и его подпакетах, таких как spel.support.

Интерфейс ExpressionParser отвечает за синтаксический анализ строки выражения. В предыдущем примере строка выражения представляет собой строковый литерал, обозначенный окружающими его одинарными кавычками. Интерфейс Expression отвечает за оценку ранее определенной строки выражения. Два исключения ParseException и EvaluationException могут быть сгенерированы при вызове parser.parseExpression и exp.getValue, соответственно.

SpEL поддерживает широкий спектр функций, таких как вызов методов, доступ к свойствам и вызов конструкторов.

В следующем примере вызова метода мы вызываем метод concat для строкового литерала:

Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); 
String message = (String) exp.getValue();
  1. Значение message теперь равно 'Hello World!'.
Kotlin
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") 
val message = exp.value as String
  1. Значение message теперь равно 'Hello World!'.

Следующий пример вызова свойства JavaBean вызывает String свойство Bytes:

Java
ExpressionParser parser = new SpelExpressionParser();
// обращается к 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); 
byte[] bytes = (byte[]) exp.getValue();
  1. Данная строка преобразует литерал в байтовый массив.
Kotlin
val parser = SpelExpressionParser()
// обращается к 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") 
val bytes = exp.value as ByteArray
  1. Данная строка преобразует литерал в байтовый массив.

SpEL также поддерживает вложенные свойства с использованием стандартной записи через точку (например, prop1.prop2.prop3), а также соответствующее задание значений свойств. Также возможен доступ к публичным полям.

В следующем примере показано, как использовать запись через точку для получения длины литерала:

Java
ExpressionParser parser = new SpelExpressionParser();
// обращается к 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); 
int length = (Integer) exp.getValue();
  1. 'Hello World'.bytes.length задает длину литерала.
Kotlin
val parser = SpelExpressionParser()
// обращается к 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") 
val length = exp.value as Int
  1. 'Hello World'.bytes.length задает длину литерала.

Конструктор String можно вызывать вместо использования строкового литерала, как показано в следующем примере:

Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); 
String message = exp.getValue(String.class);
  1. Создайте новую String из литерала и переведите ее в верхний регистр.
Kotlin
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()")  
val message = exp.getValue(String::class.java)
  1. Создайте новую String из литерала и переведите ее в верхний регистр.

Обратите внимание на использование общего метода: public <T> T getValue(Class<T> desiredResultType). Использование данного метода устраняет необходимость приведения значения выражения к нужному типу результата. Если значение нельзя привести к типу T или преобразовать с помощью зарегистрированного преобразователя типов, то генерируется EvaluationException.

Более распространенным использованием SpEL является предоставление строки выражения, которая оценивается по определенному экземпляру объекта (называемому корневым объектом). В следующем примере показано, как получить свойство name из экземпляра класса Inventor или создать булево логическое условие:

Java
// Создаем и настраиваем календарь
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// Аргументами конструктора являются имя, день рождения и национальность.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // Провести синтаксический анализ имени как выражения
String name = (String) exp.getValue(tesla);
// имя == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// результат == true
Kotlin
// Создаем и настраиваем календарь
val c = GregorianCalendar()
c.set(1856, 7, 9)
// Аргументами конструктора являются имя, день рождения и национальность.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")
val parser = SpelExpressionParser()
var exp = parser.parseExpression("name") // Провести синтаксический анализ имени как выражения
val name = exp.getValue(tesla) as String
// имя == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// результат == true

Основные сведения об EvaluationContext

Интерфейс EvaluationContext используется при оценке выражения для разрешения свойств, методов или полей, а также для выполнения преобразования типов. Spring предоставляет две реализации.

  • SimpleEvaluationContext: Открывает подмножество основных возможностей языка SpEL и опций конфигурации для категорий выражений, которые не требуют полного синтаксиса языка SpEL и должны быть значительно ограничены. Примеры включают, помимо прочего, выражения привязки данных и фильтры на основе свойств.

  • StandardEvaluationContext: Открывает полный набор возможностей языка SpEL и опций конфигурации. Вы можете использовать его для указания корневого объекта по умолчанию и для настройки всех доступных стратегий, связанных с вычислением.

SimpleEvaluationContext разработана только для частичной поддержки синтаксиса языка SpEL. Она исключает ссылки на типы Java, конструкторы и ссылки на бины. Она также требует явного выбора уровня поддержки свойств и методов в выражениях. По умолчанию статический метод фабрики create() позволяет получить доступ к свойствам только на чтение. Также можно получить конструктор для точной настройки необходимого уровня поддержки, ориентируясь на один конкретный параметр или некоторую комбинацию следующих параметров:

  • Только специальный PropertyAccessor (без рефлексии)

  • Свойства привязки данных для доступа только для чтения

  • Свойства привязки данных для чтения и записи

Преобразование типа

По умолчанию SpEL использует службу преобразования, доступную в ядре Spring (org.springframework.core.convert.ConversionService). Эта служба преобразования распространяется со множеством встроенных преобразователей для осуществления преобразований общего характера, но также является полностью расширяемой, чтобы можно было добавлять специальные преобразования между типами. Кроме того, она может работать с дженериками. Это означает, что если вы работаете с типизированными типами в выражениях, SpEL пытается выполнить преобразования, чтобы сохранить корректность типов для любых объектов, с которыми он сталкивается.

Что это означает на деле? Предположим, что присвоение с помощью setValue() используется для задания свойства List. Типом свойства фактически является List<Boolean>. SpEL распознает, что элементы списка необходимо преобразовать в Boolean перед помещением в него. В следующем примере показано, как это сделать:

Java
class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// "false" передается здесь как String. SpEL и служба преобразования
// распознает, что оно должно быть Boolean, и преобразует его соответствующим образом.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b - false
Boolean b = simple.booleanList.get(0);
Kotlin
class Simple {
    var booleanList: MutableList<Boolean> = ArrayList()
}
val simple = Simple()
simple.booleanList.add(true)
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// "false" передается здесь как String. SpEL и служба преобразования
// распознает, что оно должно быть Boolean, и преобразует его соответствующим образом.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")
// b - false
val b = simple.booleanList[0]

Конфигурирование парсера

Можно настроить парсер выражений SpEL с помощью объекта конфигурации парсера (org.springframework.expression.spel.SpelParserConfiguration). Объект конфигурации управляет логикой работы некоторых компонентов выражения. Например, если вы индексируете массив или коллекцию, а элемент в указанном индексе является null, SpEL может автоматически создать этот элемент. Это имеет смысл при использовании выражений, состоящих из цепочки ссылок на свойства. Если вы индексируете массив или список и указываете индекс, который находится за пределами текущего размера массива или списка, SpEL может автоматически увеличить массив или список, чтобы вместить этот индекс. Чтобы добавить элемент по указанному индексу, SpEL попытается создать элемент, используя конструктор типа элемента по умолчанию, прежде чем задать указанное значение. Если тип элемента не имеет конструктора по умолчанию, в массив или список будет добавлен null. Если нет встроенного или специального преобразователя, который знает, как задать значение, null останется в массиве или списке по указанному индексу. Следующий пример демонстрирует автоматическое расширение списка:

Java
class Demo {
    public List<String> list;
}
// Включаем:
// - автоматическую инициализация пустой (null) ссылки
// - автоколлекция расширяется
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 теперь будет реальной коллекцией из 4 записей
// Каждая запись представляет собой новую пустую строку
Kotlin
class Demo {
    var list: List<String>? = null
}
// Включаем:
// - автоматическую инициализация пустой (null) ссылки
// - автоколлекция расширяется
val config = SpelParserConfiguration(true, true)
val parser = SpelExpressionParser(config)
val expression = parser.parseExpression("list[3]")
val demo = Demo()
val o = expression.getValue(demo)
// demo.list теперь будет реальной коллекцией из 4 записей
// Каждая запись представляет собой новую пустую строку

Компилирование на SpEL

Spring Framework 4.1 включает в себя компилятор базовых выражений. Обычно выражения интерпретируются, что обеспечивает большую динамическую гибкость при вычислении, но не обеспечивает оптимальной производительности. Для нерегулярного использования выражений это нормально, но при использовании других компонентов, таких как Spring Integration, производительность может играть важную роль, а реальной необходимости в динамизме нет.

Компилятор SpEL призван удовлетворить эту потребность. Во время вычисления компилятор генерирует класс Java, который воплощает логику работы выражения во время выполнения, и использует этот класс для осуществления гораздо более быстрого вычисления выражений. Ввиду отсутствия типизации для выражений компилятор при компиляции использует информацию, собранную во время интерпретированных вычислений выражения. Например, он не распознает тип ссылки на свойство сугубо из выражения, но во время первого интерпретированного вычисления начинает понимать, что она из себя представляет. Естественно, компиляция на основе такой производной информации может привести к проблемам в дальнейшем, если типы различных элементов выражения со временем изменятся. По этой причине компиляция лучше всего подходит для выражений, информация о типе которых не будет меняться при повторных вычислениях.

Рассмотрим следующее базовое выражение:

someArray[0].someProperty.someOtherProperty < 0.1

Поскольку предыдущее выражение включает в себя доступ к массиву, некоторое разыменование свойств и числовые операции, выигрыш в производительности может быть очень заметным. В примере микроэталонного прогона теста 50000 итераций для проведения оценки с помощью интерпретатора потребовалось 75 мс, а при использовании скомпилированной версии выражения - всего 3 мс.

Конфигурация компилятора

Компилятор не активирован по умолчанию, но вы можете активировать его одним из двух различных способов. Вы можете активировать его с помощью процедуры конфигурирования синтаксического анализатора или с помощью свойства Spring, если использование SpEL внедрено в другой компонент. В данном разделе рассматриваются оба этих варианта.

Компилятор может работать в одном из трех режимов, которые отражены в перечисляемом типе org.springframework.expression.spel.SpelCompilerMode. Режимы следующие:

  • OFF (по умолчанию): Компилятор выключен.

  • IMMEDIATE: В немедленном режиме выражения компилируются как можно быстрее. Как правило, это происходит после первого интерпретированного вычисления. Если скомпилированное выражение не работает (обычно из-за изменения типа, что было описано ранее), вызывающий код вычисления выражений получает исключение.

  • MIXED: В смешанном режиме выражения незаметно переключаются между интерпретируемым и компилируемым режимом с течением времени. После некоторого количества интерпретируемых выполнений они переключаются в компилируемую форму, но если в компилируемой форме что-то будет идти не так (например, изменится тип, как было описано ранее), выражение автоматически снова переключается в интерпретируемую форму. Некоторое время спустя выражение может сгенерировать другую скомпилированную форму и переключиться на нее. В принципе, исключение, которое пользователь получает в режиме IMMEDIATE, вместо этого обрабатывается внутренне.

Режим IMMEDIATE существует потому, что режим MIXED может приводить к проблемам в случае выражений, имеющих побочные эффекты. Если скомпилированное выражение приводит к катастрофической ошибке после частично успешной работы, возможно, оно уже произвело какое-то действие, которое повлияло на состояние системы. Если это произошло, вызывающий код, возможно, не захочет, чтобы оно молча выполнялось повторно в интерпретируемом режиме, поскольку часть выражения может быть выполнена дважды.

После выбора режима используйте SpelParserConfiguration, чтобы сконфигурировать парсер. В следующем примере показано, как это сделать:

Java
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);
Kotlin
val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
        this.javaClass.classLoader)
val parser = SpelExpressionParser(config)
val expr = parser.parseExpression("payload")
val message = MyMessage()
val payload = expr.getValue(message)

Если вы указываете режим компилятора, то также можете указать загрузчик классов (допускается передача null). Скомпилированные выражения определяются в дочернем загрузчике классов, создаваемом под любым указываемым классом. Важно убедиться, что если указан загрузчик классов, то он может распознавать все типы, участвующие в процессе вычисления выражений. Если вы не укажете загрузчик классов, будет использоваться загрузчик классов по умолчанию (обычно это загрузчик классов контекста для потока, который выполняется во время вычисления выражений).

Второй способ настройки компилятора предназначен для использования, если SpEL внедрен в какой-либо другой компонент, и сконфигурировать его через объект конфигурации не представляется возможным. В этих случаях можно установить свойство spring.expression.compiler.mode через системное свойство из JVM (или через механизм SpringProperties) для одного из значений перечисляемого типа SpelCompilerMode(off, immediate или mixed).

Ограничения компилятора

Начиная со Spring Framework 4.1 используется базовый фреймворк компиляции. Однако фреймворк пока не поддерживает компиляцию всех видов выражений. Первоначально основное внимание было уделено распространенным выражениям, которые, вероятно, будут использоваться в контекстах, критичных для производительности. Следующие виды выражений в настоящее время не могут быть скомпилированы:

  • Выражения, связанные с присвоением;

  • Выражения, пользующиеся службой преобразования;

  • Выражения, использующие специальные распознаватели или акцессоры;

  • Выражения, использующие выборку или проекцию.

В будущем можно будет компилировать больше типов выражений.

Выражения в определениях бинов

Для определения экземпляров BeanDefinition можно использовать выражения SpEL с метаданными конфигурации на основе XML или аннотаций. В обоих случаях синтаксис для определения выражения имеет вид #{ <expression string> }.

4.2.1. Конфигурация XML

Значение свойства или аргумента конструктора может быть задано с помощью выражений, как показано в следующем примере:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
    <!-- другие свойства -->
</bean>

Все бины в контексте приложения доступны как предопределенные переменные с их обычным именем бина. Сюда входят стандартные контекстные бины, такие как environment (типа org.springframework.core.env.Environment), а также systemProperties и systemEnvironment (типа Map<String, Object>) для получения доступа к среде выполнения.

В следующем примере продемонстрировано получение доступа к бину systemProperties как к переменной SpEL:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
    <!-- другие свойства -->
</bean>

Обратите внимание, что в этом случае не нужно ставить префикс предопределенной переменной с символом #.

Вы также можете ссылаться на другие свойства бинов по имени, как показано в следующем примере:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
    <!-- другие свойства -->
</bean>
<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>
    <!-- другие свойства -->
</bean>

Конфигурация аннотации

Чтобы задать значение по умолчанию, можно поместить аннотацию @Value на поля, методы, параметры метода или конструктора.

В следующем примере значение по умолчанию задано для поля:

Java
public class FieldValueTestBean {
    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
Kotlin
class FieldValueTestBean {
    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

В следующем примере показан эквивалент, но для сеттера свойства:

Java
public class PropertyValueTestBean {
    private String defaultLocale;
    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
Kotlin
class PropertyValueTestBean {
    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

Автоматически обнаруженные и связанные методы и конструкторы также могут использовать аннотацию @Value, как показано в следующих примерах:

Java
public class SimpleMovieLister {
    private MovieFinder movieFinder;
    private String defaultLocale;
    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }
    // ...
}
Kotlin
class SimpleMovieLister {
    private lateinit var movieFinder: MovieFinder
    private lateinit var defaultLocale: String
    @Autowired
    fun configure(movieFinder: MovieFinder,
                @Value("#{ systemProperties['user.region'] }") defaultLocale: String) {
        this.movieFinder = movieFinder
        this.defaultLocale = defaultLocale
    }
    // ...
}
Java
public class MovieRecommender {
    private String defaultLocale;
    private CustomerPreferenceDao customerPreferenceDao;
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }
    // ...
}
Kotlin
class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao,
            @Value("#{systemProperties['user.country']}") private val defaultLocale: String) {
    // ...
}