Как стать автором
Обновить

Используем статические ссылки на свойства объектов при помощи лямбд

Время на прочтение5 мин
Количество просмотров4.4K

Так уж исторически сложилось, что в Java для свойств объектов (properties) не предусмотрено никакой физической сущности. Свойства в Java — это некоторые соглашения в именовании полей и методов доступа к ним (аксессоров). И, хотя наличие физических свойств в языке упростило бы множество кейсов (начиная от глупой генерации геттеров-сеттеров), судя по всему, в ближайшем будущем в Java ситуация не изменится.


Тем не менее, разрабатывая многослойные бизнес приложения и используя различные фреймворки для меппинга и связки (binding) данных, часто бывает необходимо передать ссылку на свойство объекта. Рассмотрим какие для этого есть варианты.


Использовать имя свойства


Пока что единственным общепринятым способом сослаться на свойство объекта является строка с его именем. Низлежащая библиотека использует reflection или introspection для поиска методов-аксессоров и доступа к полям. Для ссылки ко вложенным объектам как правило используется следующая нотация:


person.contact.address.city

Проблема такого способа — отсутствие всяческого контроля над написанием имени и типом свойства со всеми вытекающими:


  • Нет контроля ошибок на стадии компиляции. Можно ошибиться в имени, можно применить не к тому классу, не контролируется тип свойства. Приходится дополнительно писать достаточно глупые тесты.
  • Нет поддержки со стороны IDE. Сильно утомляет, когда мепите 200+ полей. Хорошо если в наличии для этого есть джун, которому можно все сбагрить.
  • Сложный рефакторинг кода. Поменяйте название поля, и сразу много что отвалится. Хорошие IDE выведут еще стопицот мест, где встречается похожее слово.
  • Поддержка и анализ кода. Хотим посмотреть, где используется свойство, но “Find Usages” не покажет строки.

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


  • Привязан к конкретному классу
  • Содержит имя свойства
  • Имеет тип

Каким образом можно сослаться на геттер?


Проксирование


Одним из интересных способов является проксирование (или мокирование) объектов для перехвата цепочки вызовов геттеров, который используется в некоторых библиотеках: Mockito, QueryDSL, BeanPath. По поводу последней на Хабре была статья от автора.
Идея достаточно проста, но нетривиальна в реализации (пример из упомянутой статьи).


Account account = root(Account.class);
tableBuilder.addColumn( $( account.getCustomer().getName() ) );

При помощи динамической кодогенерации создается специальный прокси-класс, который наследуется от класса бина и перехватывает все вызовы геттеров в цепочке, конструируя путь в ThreadLocal-переменной. При этом вызова настоящих геттеров объекта не происходит.


В данной статье мы рассмотрим альтернативный способ.


Ссылки на методы


С появлением Java 8 пришли лямбды и возможность использовать ссылки на методы. Поэтому натурально было бы иметь что-то вроде:


Person person = …
assertEquals("name", $(Person::getName).getPath());

Метод $ принимает следующую лямбду в которой передается ссылка на геттер:


public interface MethodReferenceLambda<BEAN, TYPE> extends Function<BEAN, TYPE>, Serializable {}
...
public static <BEAN, TYPE> BeanProperty<BEAN, TYPE> $(MethodReferenceLambda<BEAN, TYPE> methodReferenceLambda)

Проблема в том, что благодаря стиранию типов, нет никакой возможности в рантайме получить типы BEAN и TYPE, а также отсутствует любая информация об имени геттера: метод, который вызывается “снаружи” — это Function.apply().


Тем не менее существует определенный трюк — это использование сериализованной лямбды.


MethodReferenceLambda<Person,String> lambda = Person::getName(); 
Method writeMethod = lambda.getClass().getDeclaredMethod("writeReplace");
writeMethod.setAccessible(true);
SerializedLambda serLambda = (SerializedLambda) writeMethod.invoke(lambda);
String className = serLambda.getImplClass().replaceAll("/", ".");
String methodName = serLambda.getImplMethodName();

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


Библиотека BeanRef


Использование библиотеки выглядит это примерно так:


Person person = ...
// цепочечная ссылка вложенное свойство
final BeanPath<Person, String> personCityProperty = 
    $(Person::getContact).$(Contact::getAddress).$(Address::getCity);
assertEquals("contact.address.city", personCityProperty.getPath());

и не требует магии кодогенерации и сторонних зависимостей. Вместо цепочки геттеров используется цепочка лямбд со ссылкой на геттеры. При этом соблюдается типобезопасность и достаточно неплохо работает IDE-шное автодополнение:


Можно использовать имя геттера как в стандартной нотации (getXXX()/isXXX()), так и нестандартной (xxx()). Библиотека попытается найти соответствующий сеттер, и, если он отсутствует, то свойство объявляется read-only.


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


Помимо имени свойства/пути при помощи объекта BeanPath можно получить доступ к значению свойства объекта:


Person person = ...
final BeanPath<Person, String> personCityProperty = 
    $(Person::getContact).$(Contact::getAddress).$(Address::getCity);
String personCity = personCityProperty.get(person);

При этом если промежуточный объект в цепочке null, то соответствующий вызов вернет также null вместо NPE. Это сильно упростит код, не требуя ставить проверки.


Через BeanPath также можно менять значение свойства объекта, если оно не read-only:


personCityProperty.set(person, “Madrid”);

Следуя той же идее — как можно меньше NPE — в этом случае если один из промежуточных объектов в цепочке null, то библиотека попытается автоматически его создать и сохранить в поле. Для этого соответствующее свойство должно быть writeable, а класс объекта иметь публичный конструктор без параметров.


В качестве экспериментальной фичи предлагается возможность работы с коллекциями. Для некоторых особых случаев иногда приходится конструировать пути, ссылаясь на объекты внутри коллекции. Для этого предусмотрен метод $$, который конструирует ссылку на последний элемент коллекции (считая его единственным).


final BeanPath<Person, String> personPhonePath = 
    $(Person::getContact).$$(Contact::getPhoneList).$(Phone::getPhone);
assertEquals("contact.phoneList.phone", personPhonePath.getPath());
assertEquals(personPhonePath.get(person), person.getContact().getPhoneList()
    .get(person.getContact().getPhoneList().size()-1).getPhone());

Проект хостится тут: https://github.com/throwable/beanref, бинарники доступны из maven-репозитория jcenter.


Полезняшки


java.beans.Introspector
Класс Introspector из стандартной RT джавы позволяет резольвить свойства бинов.


Apache Commons BeanUtils
Наиболее полная библиотека для работы с Java Beans.


BeanPath
Упомянутая библиотека, которая делает то же самое через проксирование.


Objenesis
Инстанциируем объект любого класса с любым набором конструкторов.


QueryDSL Aliases
Использование проксированных классов для задания критериев в QueryDSL


Jinq
Интереснейшая библиотека, которая использует лямбды для задания критериев в JPA. Много магии: проксирование, сериализация лямбд, интерпретация байткода.

Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+9
Комментарии13

Публикации

Истории

Работа

Java разработчик
299 вакансий

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань