Spring Expression Language (коротко — "SpEL") — це потужна мова виразів, яка підтримує запити та маніпуляції з графом об'єктів під час виконання програми. Синтаксис мови схожий на Unified EL, але пропонує додаткові можливості, в першу чергу виклик методів і базові функції шаблонизації рядків. 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, який втілює логіку роботи виразу під час виконання, та використовує цей клас для здійснення набагато швидшого обчислення виразів. Зважаючи на відсутність типізації для виразів, компілятор при компіляції використовує інформацію, зібрану під час інтерпретованих обчислень виразу. Наприклад, він не розпізнає тип посилання на властивість суто з виразу, але під час першого інтерпретованого обчислення починає розуміти, що вона собою являє. Звісно, компіляція на основі такої похідної інформації може призвести до проблем надалі, якщо типи різних елементів виразу з часом зміняться. Тому компіляція найкраще підходить для виразів, інформація про тип яких не буде змінюватися при повторних обчисленнях.

Розглянемо наступний базовий вираз:

div class="spring-block-content">
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> }.

Конфігурація 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) {
    // ...
}