В статье рассматривается декларативный скриптовый язык JavaPath как альтернатива использованию Java Reflection и способ избежать "сервисного ада" в обособленных приложениях использующих сложные структуры данных.
Описание проблемы
Рассмотрим глубоко вложенную структуру
class A{
B b;
}
class B{
C c;
}
class C{
String name;
}
Если нам нужно присвоить значение полю name класса C, не имея при этом непосредственного доступа к экземпляру A, то на помощь, обычно, приходит промежуточный слой сервисного API.
private A a;
public void setNameService(String name) {
a.b.c.name = name;
}
Совершенно очевидно, что данный код не работоспособен, так как ни одно из полей у нас не проинициализированно. Для того чтобы сделать этот код работоспособным, все поля должны существовать до того как выполнится цепочка вызовов.
private A a;
public void setNameService(String name) {
if(a == null) {
a = new A();
}
if(a.b == null) {
a.b = new B();
}
if(a.b.c == null) {
a.b.c = new C();
}
a.b.c.name = name;
}
Если данный упрощенный пример кажется вам избыточным, то представьте, что setNameService(String name) не является единственным сервисом в вашей программе, и есть другие независимые сервисы, управляющие содержимым полей a, b и c, а сама структура имеет намного больше полей и методов, которые здесь просто не представлены для краткости.
В слабо связанном коде разные части приложения имеют доступ только к публичным интерфейсам API, а значит, каждый раз когда нам потребуется доступ к определенной ветви сложного дерева структуры данных, мы должны оформить это в виде соответствующего метода в сервисном слое API. Количество методов быстро разрастается и поддерживать их становится все сложнее и сложнее. Даже в таком простом примере, существует множество вариантов как клиент может установить значение нужного поля. Можно присвоить значение полю, как в данном примере, можно заменить целиком объект 'C' с предустановленным полем name, можно заменить 'B' или 'A' соответственно.
Проблема еще более усложняется если код клиента физически отделен от соответствующего сервиса, например в случае микро-сервисов или архитектуре построенной на сообщениях. Наконец, в предельном случае может оказаться так, что только клиент обладает полной информацией о конкретных деталях структуры данных, которую он собирается изменить. Такое случается в сервисах работающих с ключ-значение или документ-ориентированными базами данных. Сервис хранит и обеспечивает доступ к данным, но имеет слабое представление о том какие конкретно данные в нем хранятся. Закачивать весь блок данных на сторону клиента, а потом обратно, ради небольшого изменения может быть слишком дорого, опасно, да и не удобно.
На помощь может прийти небольшой проект JavaPath с открытым кодом, который я и хочу здесь представить.
JavaPath и его отличие от других шаблонов инициализации
Проще всего начать с простого примера решения вышеописанной проблемы с помощью JavaPath.
private A a = new A();
private final JavaPath javaPath = new JavaPath(A.class);
public void setNameService(String name) {
javaPath.evalPath("b.c.name", a, name);
}
Данный пример функционально полностью эквивалентен предыдущей реализации setNameService.
Таким образом, JavaPath — это скриптовый язык и тулкит для глубокого доступа, инициализации и исполнения произвольных цепочек вызовов полей и методов в Java. В языке JavaPath отсутствуют управляющие конструкции. Нет ни циклов, ни ветвлений. JavaPath позволяет декларативно описать доступ к полям и методам в заданной иерархии классов. Поля и методы могут быть не-public. Это особенно полезно для тестирования. JavaPath существенно облегчает нициализацию сложных объектов с private или final полями.
Пример: Строки — мутируемые объекты для JavaPath!
final String str = "VALUE";
JavaPath javaPath = new JavaPath(String.class);
assertEquals("VALUE",str);
//Доступ к private final byte[] value; класса String
javaPath.evalPath("value",str,"THE HACK".getBytes());
assertEquals("THE HACK",str);
Даже если ваша структура данных проста и не требует какого-то особого доступа к компонентам, но вам необходимо обеспечить произвольный доступ контролируемый на стороне клиента, JavaPath избавит вас от необходимости работать непосредственно с Java Reflection или скриптовыми языками с избыточной для данной задачи сложностью.
Как видно из примеров, парсер JavaPath требует указать корневой класс. Все возможные пути вычисляются относительно него. Результаты вычисления иерархического дерева для класса кешируются и могут быть использованы в дальнейшем.
Основной метод для работы с JavaPath
public Object evalPath(String path, Object root, Object... values);
Метод evalPath получает три параметра
- сам путь — строка максимально близкая к эквивалентному Java коду
- корневой объект класса A — именно в нем мы ожидаем найти поле b
- значения, которые хотим присвоить элементам пути по ходу инициализации. Здесь — последнему элементу указанному в пути — полю name
Элементами пути могут быть не только поля, но и методы. Слегка изменим декларацию класса C. Добавим к нему сеттер.
class C{
String name;
public void setName(String name) {
this.name = name;
}
}
Пример использования сеттера очень мало отличается от прямого обращения к полю. Разве что имя поменялось.
A a = new A();
JavaPath javaPath = new JavaPath(A.class);
public void setNameService(String name) {
javaPath.evalPath("b.c.setName", a, name);
}
JavaPath не требует указывать скобки и имя параметра для сеттера, если это единственный параметр, но, для большей выразительности можно записать и так:
javaPath.evalPath("b.c.setName($)", a, name);
где $ или $0 — ссылка на первый из списка передаваемых параметров инициализации.
В цепочке вызова
a0.a1.… .an
все элементы
a0.a1.… .an-1
должны быть полями или геттерами, то есть, иметь/возвращать значения не примитивных типов и не null. Последний элемент an это всегда сеттер или поле. Само значение возвращаемое сеттером, если оно есть, или поле, будет возвращено методом evalPath. Не вдаваясь в подробности, можно рассматривать поле как геттер и сеттер самого себя. Синтаксически поля и методы в JavaPath эквивалентны.
Основы синтаксиса JavaPath
Строка JavaPath представляет собой набор элементов пути, разделенных точками. Каждый элемент пути является полем или методом соответствующего класса. Для каждого элемента неявно подразумевается тип и набор параметров. Как правило, парсер сам способен сделать заключение о типе элемента и типах параметров, но, в отдельных случаях потребуется явное указание нужного типа. Вы, как пользователь, заинтересованны в том, чтобы строка пути была
как можно более простой и визуально максимально напоминала не осложненный вызов цепочки эквивалентного Java кода. Но, иногда это невозможно, как правило, если вам необходим доступ к сторонним классам и их API.
Синтаксическая диаграмма
Параметры
Элементы JavaPath могут получать параметры. Параметры заключаются в скобки и разделяются запятыми. Если параметры достаточно просты, для того чтобы быть выражены простой строкой, то они могут указываться тут же, в строке. Строка параметра без пробелов и спец символов не требует кавычек. Если вам нужно включить в строку пробел, точку, запятую, и т.п. — заключайте параметр в одинарные или двойные кавычки.
Прямые и обратные ссылки $ и #
Прямые Ссылки
На любое значение из массива значений, который получает метод evalPath можно ссылаться с помощью $[:digit:]* Нумерация начинается с ноля. $ без числа означает ссылку на параметр с тем же индексом, что и индекс текущего пути. См. Выполнение нескольких путей
Можно также ссылаться на поля и методы внутри ссылочных значений.
JavaPath | Комментарий |
---|---|
"a($)" |
ссылка на значение с тем же индексом, что и индекс пути |
"a($0)" |
ссылка на первое значение из списка значений |
"a($1.name)" |
ссылка на поле или метод name, который ожидается во втором значении |
Обратные ссылки
Каждый последующий элемент пути может ссылаться в качестве параметра на предыдущий или на корневой объект. Ссылка выглядит так: #[:digit:]* Нумерация начинается с корневого объекта и он имеет значение # или #0
Можно также ссылаться на поля и методы внутри обратных ссылок.
JavaPath | Комментарий |
---|---|
"a(#)" |
ссылка на корневой объект |
"a(#0.name)" |
ссылка на поле name в корневом объекте |
"a.b(#1)" |
ссылка на 'a' |
Пример инициализации цепочки, имеющей зависимость родитель-потомок.
public class A {
A parent;
A child;
String name;
public A(A parent) {
this.parent = parent;
}
}
A a = new A(null);
JavaPath javaPath = new JavaPath(A.class);
javaPath.evalPath("name",a,"PARENT");
javaPath.evalPath("child(#0).name",a,"CHILD");
javaPath.evalPath("child(#0).child(#1).name",a,"GRAND-CHILD");
assertEquals("PARENT",a.name);
assertEquals("CHILD",a.child.name);
assertEquals("GRAND-CHILD",a.child.child.name);
assertEquals("CHILD",a.child.child.parent.name);
assertEquals("PARENT",a.child.child.parent.parent.name);
Если нужно передать сроку совпадающую с прямой или обратной ссылкой — заключите ее в кавычки.
"a.set('$0')" // строка $0
InLine параметры:
Строки, а также объекты которые естественным образом можно воссоздать из их строкового представления можно передавать непосредственно в строке JavaPath. Если тип не указан, то подразумевается, что метод или поле ожидают строковый параметр. Во всех остальных случаях перед самим значением нужно указать тип.
JavaPath | Комментарий |
---|---|
"a.b.c" |
Не параметризованный вызов цепочки. Неявно предполагается, что если методу evalPath передан хотя бы один параметр, то он будет присвоен последнему элементу в цепи. |
"a.b.c($)" |
Эквивалентный пример, но ссылка на первое значение передана явно. |
"a.b.c($0)" |
$0 эквивалентно $ для единственного пути. |
"a.b.c('THE VALUE')" |
полю c будет присвоено значение 'THE VALUE' если метод evalPath не получил вообще никаких параметров. В противном случае явно переданное значение из массива значений получит приоритет. |
"a().b().c" |
При отсутствии параметров скобки не обязательны, но могут быть полезны для визуального отделения методов от полей |
"a(ПРИМЕР).b.c" |
Инлайн подстановка строкового параметра. Указание типа не требуется для строк |
"a.b(int 1024).c" |
Инлайн подстановка примитивного целого типа |
"a.b(Int 1024).c" |
Инлайн подстановка типа Integer |
"a.b(int 1024,'БУФФЕР ОБМЕНА').c" |
Инлайн подстановка двух параметров, один из которых содержит пробел |
a.setX(PhoneType CELL) |
Ожидается значение пользовательского типа enum PhoneType{HOME,CELL,WORK}, который может быть представлен строкой "CELL" |
Для классов имеющих конструктор, принимающий единственную строку, либо имеющих статическую фабрику valueOf (в частности, все enum классы и все примитивные типы) конвертация произойдет без каких либо дополнительных усилий.
Для пользовательских классов, если вариант с конструктором или статическим методом valueOf по каким-либо причинам не подходит, можно создать собственный StringConverter, который обеспечит необходимые преобразования.
Инициализация полей
Если поле-геттер имеет значение null и класс имеет дефолтный конструктор, то никаких действий не требуется. Инициализация произойдет автоматически и только один раз.
Если класс не имеет конструктора, например, потому что это абстрактный класс или интерфейс, желаемый конкретный класс может быть указан в качестве типа элемента пути. Пара тип — элемент пути заключается в скобки и разделяется пробелом. Тип является либо полным именем класса Java, либо его коротким алиасом.
"(T a).b"
Пример: поле map не может быть создано без указания конкретной имплементации.
public class A {
Map<String,String> map;
}
A a = new A();
JavaPath javaPath = new JavaPath(A.class);
//Первый вызов должен знать конкретную имплементация поля map
javaPath.evalPath("(HashMap map).put(firstName,$)", a, "John");
//Второму вызову тип не требуется, так как поле уже существует.
javaPath.evalPath("map.put(lastName,$)", a, "Silver");
Вызов конструктора может быть параметризован.
//Вариант инициализации HashMap с начальной емкостью и фактором загрузки.
//Обратите внимание на кавычки в которые помещено значение 0.8
javaPath.evalPath("(HashMap map(int 100,float '0.8')).put(firstName,$)", a, "John");
Хотя это и выглядит как вызов метода с параметрами, инициализация поля происходит только если поле имеет значение null. Последующие вызовы путей с участием поля map будут использовать уже созданный экземпляр.
Пример использования пользовательских типов
enum PhoneType{HOME,CELL,WORK}
public static class A {
String firstName;
String lastName;
Map<PhoneType, Set<String>> phones;
Map<String, PhoneType> reversedPhones;
}
A a = new A();
ClassRegistry classRegistry = new ClassRegistry();
classRegistry.registerClass(PhoneType.class,PhoneType.class.getSimpleName());
JavaPath javaPath = new JavaPath(A.class,classRegistry);
javaPath.evalPath("(map phones).put(PhoneType WORK)", a, new HashSet<>());
javaPath.evalPath("phones.computeIfAbsent(PhoneType HOME,key->new HashSet).@", a);
javaPath.evalPath("phones.computeIfAbsent(PhoneType CELL,key->new HashSet).@", a);
javaPath.evalPath("(map reversedPhones).@", a);
javaPath.evalPath("firstName", a, "John");
javaPath.evalPath("lastName", a, "Smith");
javaPath.evalPath("phones.get(PhoneType CELL).add", a, "1-101-111-2233");
javaPath.evalPath("phones.get(PhoneType HOME).add", a, "1-101-111-7865");
javaPath.evalPath("phones.get(PhoneType WORK).add", a, "1-105-333-1100");
javaPath.evalPath("phones.get(PhoneType WORK).add($)", a, "1-105-333-1104");
javaPath.evalPath("reversedPhones.put($,PhoneType CELL)", a, "1-101-111-2233");
javaPath.evalPath("reversedPhones.put($,PhoneType HOME)", a, "1-101-111-7865");
javaPath.evalPath("reversedPhones.put($,PhoneType WORK)", a, "1-105-333-1100");
javaPath.evalPath("reversedPhones.put($,PhoneType WORK)", a, "1-105-333-1104");
Префикс @
Не типизированный элемент пути начинающийся с символа @ пропускается и вместо него выполняется следующий элемент. Если это последний элемент пути, то вместо этого возвращается результат от предыдущего геттера. Может использоваться в качестве своеобразного комментария.
Оператор альтернативной инициализации ||
Если геттер в цепочке может вернуть null, то JavaPath предлагает возможность сделать попытку вызвать соответствующий сеттер или иной метод инициализации объекта. После этого геттер будет вызван еще один раз.
JavaPath | Комментарий |
---|---|
"getA??setA($1).name($0)" |
Если getA вернет null будет вызван метод setA, затем снова getA |
"getA??init.name($0)" |
вызов абстрактного метода инициализации без параметров |
Оператор явного указания фабрики экземпляров ::
Если для создания объекта используется не конструктор, а статический метод-фабрика, то он может быть указан явно с помощью оператора ::
JavaPath | Комментарий |
---|---|
"(UserInfo::newInstance userInfo).phone.ext" |
Экземпляр UserInfo создается с помощью фабрики UserInfo.newInstance() |
"(UserInfo::newInstance userInfo(John,Smith)).phone.ext" |
Экземпляр UserInfo создается с помощью параметризованной фабрики UserInfo.newInstance(String a, String b) |
"a.b(Integer::valueOf 100).c" |
Метод конвертации строки в целое. Приведено для иллюстрации. Фабрики для базовых типов не требуют явного указания |
"(HashMap::new map).get" |
псевдо-метод new описывает вызов конструктора. Не требует явного указания. |
ClassRegistry
Вспомогательный класс для регистрации алиасов для пользовательских классов, фабрик конвертации строк параметров в произвольные типы, и т.п. Время действия объекта ClassRegistry в основном ограниченно временем построения дерева зависимостей классов, поэтому все изменения в него должны вноситься до создания экземпляра JavaPath.
ClassRegistry позволяет вносить изменения глобально или локально. Глобально устанавливаемые изменения не влияют на уже имеющиеся экземпляры ClassRegistry, и соответственно JavaPath.
Пример локальной регистрации коротких имен для пользовательского класс PhoneType
ClassRegistry classRegistry = new ClassRegistry();
classRegistry.registerClass(PhoneType.class,PhoneType.class.getSimpleName(),"Phone");
JavaPath javaPath = new JavaPath(A.class,classRegistry);
После регистрации PhoneType доступен не только по полному, но и по короткому имени, а также по имени Phone.
ClassRegistry так же позволяет регистрировать конвертеры строк в произвольные типы. Если конвертер зарегистрирован явно, то во многих случаях не потребуется использования оператора :: и даже явного указания типа.
Пример глобальной регистрации конвертора строки в пользовательский тип А. Все последующие экземпляры ClassRegistry будут иметь этот конвертер. Обратите внимание, что StringConverter — это функциональный интерфейс.
public class A {
...
static {
ClassRegistry.registerGlobalStringConverter(A.class,A::stringToA);
}
public static A stringToA(String str) {
A a = new A("{"+str+"}"); //Делаем что-то необычное
return a;
}
}
public class B {
A a;
}
JavaPath javaPath = new JavaPath(B.class); //имеет конвертер A::stringToA
Аннотации @PathElement и @NoPathElement
@PathElement устанавливает альтернативные имена для поля или метода.
public class A {
String name; //доступ к полю маскируется сеттерм с аннотацией "name"
@PathElement({"name","first_name","firstName"})
public void setName(String name) {
this.name = name;
}
}
Обратите внимание, что метод setName теперь доступен под именем "name", совпадающем с именем поля. В этом случае сеттеру будет отдан приоритет перед полем с тем же именем.
@NoPathElement исключает метод или поле из области видимости JavaPath.
public class A {
StringBuilder stringBuilder = new StringBuilder();
@NoPathElement
private final String protectedField = "IMMUTABLE BY JAVA PATH!";
public void add(String str) {
stringBuilder.append(str == null ? "N/A" : str);
}
@NoPathElement
public void add(Object val) {
stringBuilder.append(val);
}
}
Аннотации взаимоисключающие и не могут применяться одновременно.
Выполнение нескольких путей
Строка JavaPath может включать в себя несколько путей. Каждый путь отделяется от предыдущего точкой с запятой или новой строкой.
public class A {
String firstName;
String lastName;
int age;
}
A a = new A();
JavaPath javaPath = new JavaPath(A.class);
javaPath.evalPath("firstName; lastName; age", a, "John","Smith",55);
// $ ссылается на значение с тем же индексом, что и индекс пути
javaPath.evalPath("firstName($); lastName($); age($)", a, "John","Smith",55);
// Во избежание путаницы в сложных скриптах используйте явную нумерацию.
javaPath.evalPath("firstName($0); lastName($1); age($2)", a, "John","Smith",55);
Параметрическая ссылка без номера '$', а так же отсутствие ссылки в последнем элементе для множественных путей означает номер параметра с тем же индексом, что и путь. Так, в примере firstName($) будет присвоено значение "John" (первый путь, первое значение), а age($) — целое число 55 (третий путь, третье значение). Во избежание путаницы в сложных скриптах используйте явную нумерацию.
Обратные ссылки, кроме корневого объекта, не наследуются и вычисляются заново для каждого пути.
В случае множественных путей возвращается значение полученное для последнего из них. Остальные значения будут утеряны.
Инициализация корневых объектов из JavaPath
Все предыдущие примеры исходили из предположения, что корневой объект так или иначе доступен. Но что если он отсутствует, и должен быть создан до того как его параметры будут проинициализированы? Семейство методов initPath позволяет решить
проблему.
Строка JavaPath для инициализации немного отличается от предыдущих примеров. Она начинается с дополнительного элемента пути, символизирующего корневой объект. Поскольку данного поля или метода просто физически не существует ни в одном классе, то это может быть любое имя, например root. Так же можно использовать символы обратных ссылок на корневой элемент # и #0
Методы initPath возвращают экземпляр созданного корневого объекта.
Примеры:
// Тип указан непосредственно в строке JavaPath. Потребуется кастинг
Object instanceOfA = javaPath.initPath("(com.my.project.A #0).b", "test");
// Тип передан как параметр метода initPath
A instanceOfA = javaPath.initPath(A.class, "#.b", "test");
// Имя корневого объекта может быть любым идентификатором, # или #0
A instanceOfA = javaPath.initPath(A.class, "root.b", "test");
Кеширование путей
Метод setEnablePathCaching(boolean enableCaching) класса JavaPath позволяет сохранять результат работы парсера в кеше. Не следует путать с неотключаемым кешированием иерархии полей и методов классов. Кеш путей отключен по умолчанию, потому что может привести к неконтролируемому расходу памяти, если пути вычисляются динамически.
Пример — в кеше окажется три разных пути:
evalPath("user.name('John'));"
evalPath("user.name('Peter'));"
evalPath("user.name('Mike'));"
Вместо этого стоит использовать явную передачу изменяемых значений. Нижний пример сохранит один путь.
evalPath("user.name($0)","John");
evalPath("user.name($0)","Peter");
evalPath("user.name($0)","Mike");
Зависимости
Java8 и старше
<dependency>
<groupId>com.aegisql</groupId>
<artifactId>java-path</artifactId>
<version>0.2.0</version>
</dependency>
sshikov
А чем это в принципе лучше, например MVEL, которому сто лет в обед?
vektory79
Я не автор статьи, но могу сказать, что когда-то использовали MVEL, но отказались из-за его плохого масштабирования под нагрузкой и глючности. Может с тех пор что и изменилось...
Тогда перешли на JXPath, но со временем стало понятно, что и у него тоже под нагрузкой всё печально. Так что теперь опять приходится искать альтернативу.
sshikov
В принципе, я ваш аргумент понял. Хотя применительно к данному продукту — его же надо сначала померять, чтобы убедиться, что он реально быстрый.
Ну и как бы, если вы уже решили интерпретировать выражения (вы же их интерпретируете, скорее всего?) — то о какой производительности вообще речь? Тогда надо брать в руки что-то типа asm, и компилировать в байт код, хотя бы.
vektory79
Ситуации бывают разные. У нас есть места где никакого байткода не хватит. Да и устраивало бы всё в JXPath, если бы не было косяков с синхронизацией.
А мерить да надо. Просто была мысль написать свою балалайку. А тут глядь — уже кто-то сделал. Ну и стало интересно.
aegisql Автор
Принимаю пожелания, кроме «взять все переделать с другим синтаксисом» :)
aegisql Автор
Текущая версия 0.2 сфокусирована на стабилизации и развитии синтаксиса. Специально производительность не оптимизировалась и кое какие ресурсы здесь еще есть. Но, все верно, ожидать большего чем может предложить Java Reflection и родственные библиотеки не приходится.
Впрочем, здесь же, в силу особенностей предложенного языка, кроется резерв — всегда можно заменить длинную медленную цепочку короткой, просто написав нужный метод в корневом классе.
aegisql Автор
Тем что намного проще. javaPath это язык для инлайновых инструкций. В нем нет управляющих конструкций
sshikov
Ну, вообще мне например (а еще в рамках apache camel) ничего не мешает применять в качестве именно такого языка даже например groovy. Управляющих конструкций… дело в том, что если я их не использую — их для меня и нет. Используете только выражения — и там все почти так же. Ну то есть это (наличие чего либо) — ну не совсем аргумент.
vektory79
С одной стороны вроде как и нет, а с другой стороны — выдвигает требования к архитектуре библиотеки, из-за которых даже тривиальные вещи становятся намного более громоздкими в выполнении.
Так что лично мне подход автора весьма импонирует.
Но если у вас camel — то тогда экономить на спичках уже смысла нету, да :)
sshikov
Ну, я на самом деле про подход ничего плохого не хотел сказать — я всего-лишь пытаюсь лучше понять цели.
aegisql Автор
Вообще-то это офф-спринг другого, более масштабного проекта github.com/aegisql/conveyor/wiki — реактивного аггрегатора и строителя
В нем он является одним из компонентов. Цель — декларативная команда для какой части сложной структуры данных предназначается конкретный кусочек данных. Сами данные поступают асинхронно и в произвольной последовательности, возможно даже, из независимых источников.
ekuleshov
Присоединяюсь к этому вопросу. Почему, например не SpEL из Spring? Он компилируется, поддерживает конструкции для работы с коллекциями, литералы для мапов и списков, расширяемый и поддерживает условные выражения…
aegisql Автор
Да собственно, можно вообще любой скриптовый язык задействовать. Хоть JavaScript, или Python. В данном случае, отказ от усложнения вполне сознательный. Ну и, не используется SPRING. Я, лично, даже не в курсе, можно ли каким-то естественным образом извлечь и использовать только один язык из всей монстроидной корневой библиотеки SPRING
ekuleshov
Модуль называется spring-expression и это не скриптовый язык, а язык выражений.
aegisql Автор
Давно сам лично не работал с Spring Expression. Обновил знания.
Главная моя претензия к SE — этот язык не очень здорово подходит для динамического исполнения. Собственно, и создавался то для параметризации статичных XML файлов. У меня можно по выбору, либо включать литерал непосредственно шаблон пути — а это удобно если все просто, никогда не меняется и описывается короткими строками, либо можно передать все параметры в массиве и ссылаться на них как $1, $2, и т.д. Не надо мудрить с генерацией путей, как это пришлось бы делать с SE. Обратные ссылки, опять же. Не только this или root, а любой однажды возникший в контексте объект в порядке их появления.
Чего у меня нет — вкусностей при работе со списками, и не уверен, надо ли. Попробуйте меня переубедить.
Так же у меня нет именованных параметров. Думал об этом, но не сложилась пока общая картина. Скажем, вместо $2 написать $userEmail, как то так.
ekuleshov
В SpEL есть поддержка параметров. Они там называюстся переменными — #primes.?[#this > #maxValue]. Фактически передается мап значений.
На счет обратных ссылок, SpEL это язык выражений. Фактически каждое выражение — функция без побочных эффектов. На входе какието данные и параметры — на выходе результат.
Работа с параметрами и обработка результатов вычисления выражений, или складывание их в параметры или контекст — это все снаружи или часть инфраструктуры — которую лучше на обычном языке писать.
Что мне в SpEL нравится, так это то что одни и те же выражения могут работать на разных входных структурах данных — Java модель, мапы мапов/списков, результат из DB, XML DOM, и т.п.
ekuleshov
Кстати, есть еще такая штука как Janino. Жив, курилка, лет 15 уже… Там тоже есть компилятор выражений, но язык Java — более многословно чем SpEL