Статически проверяемые ссылки на свойства Java-бинов04.12.2014 11:03
Когда долго и серьезно используешь какой-либо инструмент, неминуемо возникают претензии к нему — неудобства, с которыми сперва миришься, но в какой-то момент понимаешь, что проще один раз исправить, чем все время страдать. Хорош тот инструмент, который позволяет «допилить» сам себя.Java — хороший инструмент, поэтому об одном таком неудобстве и о том, как мы его исправляли, и пойдет речь.
Итак, неудобствоВ Java нет синтаксиса, позволяющего сослаться на свойство бина. Проще пояснить на примере. Допустим, есть Account, у которого есть свойство customer типа Customer, у которого, в свою очередь, есть свойство name. Иными словами:
public class Account {
public Customer getCustomer () { … }
}
public class Customer {
public String getName () { … }
}
И есть TableBuilder, который умеет создавать таблички на интерфейсе для показа списка бинов, нужно лишь сообщить ему, какие их свойства (возможно, вложенные) мы хотим вывести, а он уже сделает всю рутинную работу.Как сказать, что мы хотим показать name customer«а Account«а? Обычно используют строковые литералы:
TableBuilder tableBuilder = TableBuilder.of (Account.class);
…
tableBuilder.addColumn («customer.name»);
Недостаток этого способа в том, что компилятор не знает, что это не просто строка, и не может проверить ее корректность, а значит, все опечатки обернутся ошибками только во время исполнения. По той же причине среда разработки не сможет подсказать нам, какие свойства есть у Customer«а. И даже если мы все проверим и отладим, первый же рефакторинг разрушит наши старания.Есть здесь еще одно менее очевидное неудобство: типизация addColumn (String) никак не подсказывает нам, что этот метод ожидает не абы какую строку, а цепочку свойств. Хочется, чтобы компилятор всё проверил, среда подсказала, а рефакторинг не сломал. Не так уж это и много, учитывая, что вся необходимая для этого информация уже есть.
На пути к решению
Казалось бы, задача — нерешаемая: в Java действительно нет синтаксической конструкции, чтобы сослаться на член класса без обращения к нему. Однако это уже давно не мешает mock-фреймворкам изящно и строго выражать «когда будет вызван метод…», как, например, умеет Mockito:
Account account = mock (Account.class);
when (account.getCustomer ()).thenReturn (…);
Метод mock () создает и возвращает прокси, который выглядит как Account, но ведет себя совсем по-другому: запоминает информацию о вызванном методе в ThreadLocal-переменной, которую потом извлекает, и использует when (). Можно использовать такой же трюк для решения нашей задачи:
Account account = root (Account.class);
tableBuilder.addColumn ($(account.gertCustomer ().getName ()));
root () возвращает прокси, который запоминает вызванные методы в ThreadLocal-переменную и возвращает следующий прокси, позволяя писать цепочки вызовов, которые превратятся в цепочку свойств.$() возвращает не строку, а объект типа BeanPath, который представляет цепочку свойств в объектно-ориентированном виде. Можно перемещаться по отдельным элементам этой цепочки (для каждого элемента сохраняется имя и тип) или преобразовать в уже знакомую нам строку:
$(account.gertCustomer ().getName ()).toDotDelimitedString () => «customer.name»
$(), помимо основной своей функции, захватывает тип цепочки (последнего свойства в цепочке), а значит, позволяет добавить еще капельку типизации в TableBuilder:
public ColumnBuilder addColumn (BeanPath path) {…}
Вот такой небольшой фреймворк мы написали в CUSTIS, пользуемся им сами, а теперь выложили на GitHub.Аспекты использования
Реализация через динамическое проксирование налагает следующие ограничения.Во-первых, «корень» и незамыкающие свойства в цепочке не могут быть final-классами (в том числе enum«ом, строкой, j.l.Integer и т. д.). Фреймворк не может проксировать их и возвращает null:
$(account.getCustomer ().getName ().length ()) // => NPX!
Тем не менее замыкать цепочку может свойство любого типа: и final-класс, и примитив (который в середине цепочки бессмыслен и невозможен).Во-вторых, геттеры должны быть видимы для фреймворка, то есть не должны быть private или package-local.А вот конструктора по умолчанию и вообще публичного конструктора может и не быть — прокси инстанцируется в обход конструктора. Поскольку законным способом это сделать нельзя, используется проприетарный для HotSpot JVM интринзик sun.misc.Unsafe.allocateObject (), что делает фреймворк непереносимым на другие JVM. «Руты» можно и нужно переиспользовать, они не содержат состояния:
Account account = root (Account.class);
tableBuilder.addColumn ($(account.getCustomer ().getName ()));
tableBuilder.addColumn ($(account.getNumber ()));
tableBuilder.addColumn ($(account.getOpenDate ()));
Методы root () и $() можно алиасить, поскольку это просто статические методы:
public class BeanPathMagicAlias {
public static BeanPath path (T callChain) {
return BeanPathMagic.$(callChain);
}
}
Можно это использовать для переименования методов в угоду вкусу или чтобы создать полезный шорткат. В частности, один такой уже объявлен в beanpath:
public static String $$(Object callChain) { return $(callChain).toDotDelimitedString (); }
Пригодится для использования beanpath в коде, который ожидает строковые литералы. Инстанс BeanPath можно сконструировать и вручную — его поведение полностью определяется состоянием, которое задается при конструировании. Так:
BeanPath bp1 = $(account.getCustomer ().getName ());
BeanPath bp2 = BeanPath.root (Account.class)
.append («customer», Customer.class)
.append («name», String.class);
bp1.equals (bp2) // => true
Это может пригодиться, чтобы обойти упомянутые выше ограничения (если в цепочке оказался final-класс или нет публичных геттеров). При этом корректность цепочки остается на совести разработчика.Планы на будущее
Сейчас beanpath доступен только в исходных кодах. Поэтому прежде всего хочется наладить его полноценную сборку и деплой в Maven Central. Потом заменить использование sun.misc.Unsafe на Objnesis, чтобы сделать beanpath переносимым. Ну и совсем на перспективу — подойти к решению задачи с другого края: использовать статическую кодогенерацию а-ля JPA static metamodel.Такой вариант имеет ряд плюсов: Нулевые накладные расходы в рантайме.
Возможность захватить типизацию «корня» цепочки.
В API сгенеренных классов можно отфильтровать лишние методы (которые не относятся к свойствам).
P.S. Похожий функционал есть в Querydsl, и реализован он так же, но сильно завязан на инфраструктуру Querydsl.
© Habrahabr.ru