|
|
| Строка 1: |
Строка 1: |
| − | <blockquote>''<span style="color:#0645AD">Сергей Кошель</span>, ведущий разработчик, опубликовал в нашем корпоративном блоге на [http://habrahabr.ru/ «Хабрахабре»] статью [http://habrahabr.ru/company/custis/blog/243803/ «Статически проверяемые ссылки на свойства Java-бинов»]. В ней он рассказал о недостатках синтаксиса Java, связанных с невозможностью сослаться на свойство бина, о конкретных рабочих задачах, при работе над которыми необходимо было устранить данные недостатки, а также представил разработанное в компании решение и аспекты его использования.''</blockquote> | + | <blockquote>''<span style="color:#0645AD">Сергей Кошель</span>, ведущий разработчик, опубликовал в нашем корпоративном блоге на [http://habrahabr.ru/ «Хабрахабре»] статью [http://habrahabr.ru/company/custis/blog/243803/ «Статически проверяемые ссылки на свойства Java-бинов»]. В ней он рассказал о недостатках синтаксиса Java, связанных с невозможностью сослаться на свойство бина, о конкретных рабочих задачах, при работе над которыми необходимо было устранить данные недостатки, а также представил разработанное в компании решение и аспекты его использования.''</blockquote> |
| | | | |
| − | Когда долго и серьезно используешь какой-либо инструмент, неминуемо возникают претензии к нему — неудобства, с которыми сперва миришься, но в какой-то момент понимаешь, что проще один раз исправить, чем все время страдать. Хорош тот инструмент, который позволяет допилить сам себя. | + | Когда долго и серьезно используешь какой-либо инструмент, неминуемо возникают претензии к нему — неудобства, с которыми сперва миришься, но в какой-то момент понимаешь, что проще один раз исправить, чем все время страдать. Хорош тот инструмент, который позволяет допилить сам себя. |
| | | | |
| − | Java — хороший инструмент, поэтому об одном таком неудобстве и о том, как мы его исправляли, и пойдет речь. | + | Java — хороший инструмент, поэтому об одном таком неудобстве и о том, как мы его исправляли, и пойдет речь. |
| | | | |
| | == Итак, неудобство == | | == Итак, неудобство == |
| | | | |
| − | В Java нет синтаксиса, позволяющего сослаться на свойство бина. Проще пояснить на примере. | + | В Java нет синтаксиса, позволяющего сослаться на свойство бина. Проще пояснить на примере. |
| | | | |
| | Допустим, есть <code>Account</code>, у которого есть свойство <code>customer</code> типа <code>Customer</code>, у которого есть свойство <code>name</code>. Иными словами: | | Допустим, есть <code>Account</code>, у которого есть свойство <code>customer</code> типа <code>Customer</code>, у которого есть свойство <code>name</code>. Иными словами: |
| Строка 30: |
Строка 30: |
| | tableBuilder.addColumn(«customer.name»);</code-java> | | tableBuilder.addColumn(«customer.name»);</code-java> |
| | | | |
| − | Недостаток этого способа в том, что компилятор не знает, что это не просто строка и не может проверить ее корректность, а, значит, все опечатки обернутся ошибками только во время исполнения. По той же причине среда разработки не сможет подсказать нам, какие свойства есть у <code>Customer</code>’а. И даже если мы все проверим и отладим, первый же рефакторинг разрушит наши старания. | + | Недостаток этого способа в том, что компилятор не знает, что это не просто строка и не может проверить ее корректность, а, значит, все опечатки обернутся ошибками только во время исполнения. По той же причине среда разработки не сможет подсказать нам, какие свойства есть у <code>Customer</code>’а. И даже если мы все проверим и отладим, первый же рефакторинг разрушит наши старания. |
| | | | |
| | Есть здесь еще одно, менее очевидное, неудобство: типизация <code>addColumn(String)</code> никак не подсказывает нам, что этот метод ожидает не абы какую строку, а цепочку свойств. | | Есть здесь еще одно, менее очевидное, неудобство: типизация <code>addColumn(String)</code> никак не подсказывает нам, что этот метод ожидает не абы какую строку, а цепочку свойств. |
| | | | |
| − | Хочется, чтобы компилятор всё проверил, среда подсказала, а рефакторинг не сломал. Не так уж это и много, учитывая, что вся необходимая для этого информация уже есть. | + | Хочется, чтобы компилятор всё проверил, среда подсказала, а рефакторинг не сломал. Не так уж это и много, учитывая, что вся необходимая для этого информация уже есть. |
| | | | |
| − | Казалось бы, задача — нерешаемая: в Java действительно нет синтаксической конструкции, позволяющей сослаться на член класса без обращения к нему. Однако, это уже давно не мешает mock-фрэймворкам изящно и строго выражать «когда будет вызван метод…», как например умеет [http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html Mockito]: | + | Казалось бы, задача — нерешаемая: в Java действительно нет синтаксической конструкции, позволяющей сослаться на член класса без обращения к нему. Однако, это уже давно не мешает mock-фрэймворкам изящно и строго выражать «когда будет вызван метод…», как например умеет [http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html Mockito]: |
| | | | |
| | <code-java>Account account = mock(Account.class); | | <code-java>Account account = mock(Account.class); |
| Строка 42: |
Строка 42: |
| | when(account.getCustomer()).thenReturn(…);</code-java> | | when(account.getCustomer()).thenReturn(…);</code-java> |
| | | | |
| − | Метод <code>mock()</code> создает и возвращает прокси, который выглядит как <code>Account</code>, но ведет себя совсем по-другому: запоминает информацию о вызванном методе в <code>ThreadLocal</code>-переменной, которую потом извлекает и использует <code>when()</code>. | + | Метод <code>mock()</code> создает и возвращает прокси, который выглядит как <code>Account</code>, но ведет себя совсем по-другому: запоминает информацию о вызванном методе в <code>ThreadLocal</code>-переменной, которую потом извлекает и использует <code>when()</code>. |
| | | | |
| | Можно использовать такой же трюк для решения нашей задачи: | | Можно использовать такой же трюк для решения нашей задачи: |
| Строка 52: |
Строка 52: |
| | <code>root()</code> возвращает прокси, который запоминает вызванные методы в <code>ThreadLocal</code>-переменную и возвращает следующий прокси, позволяя писать цепочки вызовов, которые превратятся в цепочку свойств. | | <code>root()</code> возвращает прокси, который запоминает вызванные методы в <code>ThreadLocal</code>-переменную и возвращает следующий прокси, позволяя писать цепочки вызовов, которые превратятся в цепочку свойств. |
| | | | |
| − | <code>$() </code>возвращает не строку, а объект типа <code>BeanPath</code>, который представляет цепочку свойств в объектно-ориентированном виде. Можно перемещаться по отдельным элементам этой цепочки (для каждого элемента сохраняется имя и тип) или преобразовать в уже знакомую нам строку: | + | <code>$() </code>возвращает не строку, а объект типа <code>BeanPath</code>, который представляет цепочку свойств в объектно-ориентированном виде. Можно перемещаться по отдельным элементам этой цепочки (для каждого элемента сохраняется имя и тип) или преобразовать в уже знакомую нам строку: |
| | | | |
| | <code-java>$(account.gertCustomer().getName()).toDotDelimitedString() | | <code-java>$(account.gertCustomer().getName()).toDotDelimitedString() |
| Строка 58: |
Строка 58: |
| | => «customer.name»</code-java> | | => «customer.name»</code-java> |
| | | | |
| − | <code>$()</code>, помимо основной своей функции, захватывает тип цепочки (последнего свойства в цепочке), а, значит, позволяет добавить еще капельку типизации в <code>TableBuilder</code>: | + | <code>$()</code>, помимо основной своей функции, захватывает тип цепочки (последнего свойства в цепочке), а, значит, позволяет добавить еще капельку типизации в <code>TableBuilder</code>: |
| | | | |
| | <code-java>public <T> ColumnBuilder<T> addColumn(BaenPath<T> path) {…}</code-java> | | <code-java>public <T> ColumnBuilder<T> addColumn(BaenPath<T> path) {…}</code-java> |
| | | | |
| − | Вот такой небольшой фрэймворк мы написали в CUSTIS, немного попользовались им сами, а теперь выложили на GitHub. | + | Вот такой небольшой фрэймворк мы написали в CUSTIS, немного попользовались им сами, а теперь выложили на GitHub. |
| | | | |
| | == Аспекты использования == | | == Аспекты использования == |
| Строка 68: |
Строка 68: |
| | Реализация через динамическое проксирование налагает следующие ограничения. | | Реализация через динамическое проксирование налагает следующие ограничения. |
| | | | |
| − | Во-первых, «корень» и незамыкающие свойства в цепочке не могут быть <code>final</code>-классами (в том числе enum’ом, строкой, j.l.Integer и т. д.). Фрэймворк не может проксировать их и возвращает <code>null</code>: | + | Во-первых, «корень» и незамыкающие свойства в цепочке не могут быть <code>final</code>-классами (в том числе enum’ом, строкой, j.l.Integer и т. д.). Фрэймворк не может проксировать их и возвращает <code>null</code>: |
| | | | |
| | <code-java>$(account.getCustomer().getName().length()) // => NPX!</code-java> | | <code-java>$(account.getCustomer().getName().length()) // => NPX!</code-java> |
| | | | |
| − | Тем не менее, замыкать цепочку может свойство любого типа: и <code>final</code>-класс, и примитив (который в середине цепочки бессмысленен и невозможен). | + | Тем не менее, замыкать цепочку может свойство любого типа: и <code>final</code>-класс, и примитив (который в середине цепочки бессмысленен и невозможен). |
| | | | |
| − | Во-вторых, геттеры должны быть видимы для фрэймворка, то есть не должны быть <code>private</code> или <code>package-local</code>. | + | Во-вторых, геттеры должны быть видимы для фрэймворка, то есть не должны быть <code>private</code> или <code>package-local</code>. |
| | | | |
| − | А вот конструктора по умолчанию и вообще публичного конструктора может и не быть — прокси инстанцируется в обход конструктора. Поскольку, законным способом это сделать нельзя, используется проприетарный для HotSpot JVM интринзик <code>sun.misc.Unsafe.allocateObject()</code>, что делает фрэймворк непереносимым на другие JVM. | + | А вот конструктора по умолчанию и вообще публичного конструктора может и не быть — прокси инстанцируется в обход конструктора. Поскольку, законным способом это сделать нельзя, используется проприетарный для HotSpot JVM интринзик <code>sun.misc.Unsafe.allocateObject()</code>, что делает фрэймворк непереносимым на другие JVM. |
| | | | |
| | «Руты» можно и нужно переиспользовать, они не содержат состояния: | | «Руты» можно и нужно переиспользовать, они не содержат состояния: |
| Строка 100: |
Строка 100: |
| | }</code-java> | | }</code-java> |
| | | | |
| − | Можно это использовать для переименования методов в угоду вкусу или чтобы создать полезный шорткат. В частности, один такой уже объявлен в <code>beanpath</code>: | + | Можно это использовать для переименования методов в угоду вкусу или чтобы создать полезный шорткат. В частности, один такой уже объявлен в <code>beanpath</code>: |
| | | | |
| | <code-java>public static String $$(Object callChain) | | <code-java>public static String $$(Object callChain) |
| Строка 106: |
Строка 106: |
| | { return $(callChain).toDotDelimitedString(); }</code-java> | | { return $(callChain).toDotDelimitedString(); }</code-java> |
| | | | |
| − | Пригодится для использования <code>beanpath</code> в коде, который ожидает строковые литералы. | + | Пригодится для использования <code>beanpath</code> в коде, который ожидает строковые литералы. |
| | | | |
| − | Инстанс <code>BeanPath</code> можно сконструировать и вручную — его поведение полностью определяется состоянием, которое задается при конструировании. Так: | + | Инстанс <code>BeanPath</code> можно сконструировать и вручную — его поведение полностью определяется состоянием, которое задается при конструировании. Так: |
| | | | |
| | <code-java>BeanPath<String> bp1 = $(account.getCustomer().getName()); | | <code-java>BeanPath<String> bp1 = $(account.getCustomer().getName()); |
| Строка 120: |
Строка 120: |
| | bp1.equals(bp2) // => true</code-java> | | bp1.equals(bp2) // => true</code-java> |
| | | | |
| − | Это может пригодиться, чтобы обойти упомянутые выше ограничения (если в цепочке оказался <code>final</code>-класс или нет публичных геттеров). При этом корректность цепочки остается на совести разработчика. | + | Это может пригодиться, чтобы обойти упомянутые выше ограничения (если в цепочке оказался <code>final</code>-класс или нет публичных геттеров). При этом корректность цепочки остается на совести разработчика. |
| | | | |
| | === Планы на будущее === | | === Планы на будущее === |
| | | | |
| − | Сейчас <code>beanpath</code> доступен только в исходных кодах. Поэтому прежде всего хочется наладить его полноценную сборку и деплой в Maven Central. | + | Сейчас <code>beanpath</code> доступен только в исходных кодах. Поэтому прежде всего хочется наладить его полноценную сборку и деплой в Maven Central. |
| | | | |
| | Потом заменить использование <code>sun.misc.Unsafe</code> на <code>Objnesis</code>, чтобы сделать <code>beanpath</code> переносимым. | | Потом заменить использование <code>sun.misc.Unsafe</code> на <code>Objnesis</code>, чтобы сделать <code>beanpath</code> переносимым. |
| | | | |
| − | Ну и совсем на перспективу, подойти к решению задачи с другого края: использовать статическую кодогенерацию, а-ля <code>JPA static metamodel</code>. Такой вариант имеет ряд плюсов: | + | Ну и совсем на перспективу, подойти к решению задачи с другого края: использовать статическую кодогенерацию, а-ля <code>JPA static metamodel</code>. Такой вариант имеет ряд плюсов: |
| | | | |
| | * нулевые накладные расходы в рантайме; | | * нулевые накладные расходы в рантайме; |
| Строка 134: |
Строка 134: |
| | * возможность захватить типизацию «корня» цепочки; | | * возможность захватить типизацию «корня» цепочки; |
| | | | |
| − | * в API сгенеренных классов можно отфильтровать лишние методы (которые не относятся к свойствам). | + | * в API сгенеренных классов можно отфильтровать лишние методы (которые не относятся к свойствам). |
| | | | |
| | [[Категория:Хабрахабр (Публикации)]] | | [[Категория:Хабрахабр (Публикации)]] |
Текущая версия на 12:37, 24 апреля 2015
Сергей Кошель, ведущий разработчик, опубликовал в нашем корпоративном блоге на «Хабрахабре» статью «Статически проверяемые ссылки на свойства Java-бинов». В ней он рассказал о недостатках синтаксиса Java, связанных с невозможностью сослаться на свойство бина, о конкретных рабочих задачах, при работе над которыми необходимо было устранить данные недостатки, а также представил разработанное в компании решение и аспекты его использования.
Когда долго и серьезно используешь какой-либо инструмент, неминуемо возникают претензии к нему — неудобства, с которыми сперва миришься, но в какой-то момент понимаешь, что проще один раз исправить, чем все время страдать. Хорош тот инструмент, который позволяет допилить сам себя.
Java — хороший инструмент, поэтому об одном таком неудобстве и о том, как мы его исправляли, и пойдет речь.
Итак, неудобство
В Java нет синтаксиса, позволяющего сослаться на свойство бина. Проще пояснить на примере.
Допустим, есть Account, у которого есть свойство customer типа Customer, у которого есть свойство name. Иными словами:
public class Account {
public Customer getCustomer() { ... }
}
public class Customer {
public String getName() { ... }
}
И есть TableBuilder, который умеет создавать таблички на интерфейсе для показа списка бинов, нужно лишь сообщить ему, какие их свойства (возможно, вложенные) мы хотим вывести, а он уже сделает всю рутинную работу.
Как сказать, что мы хотим показать name customer’а Account’а? Обычно используют строковые литералы:
TableBuilder<Account> 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 <T> ColumnBuilder<T> addColumn(BaenPath<T> 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 <T> BeanPath<T> path(T callChain) {
return BeanPathMagic.$(callChain);
}
}
Можно это использовать для переименования методов в угоду вкусу или чтобы создать полезный шорткат. В частности, один такой уже объявлен в beanpath:
public static String $$(Object callChain)
{ return $(callChain).toDotDelimitedString(); }
Пригодится для использования beanpath в коде, который ожидает строковые литералы.
Инстанс BeanPath можно сконструировать и вручную — его поведение полностью определяется состоянием, которое задается при конструировании. Так:
BeanPath<String> bp1 = $(account.getCustomer().getName());
BeanPath<String> bp2 = BaenPath.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 сгенеренных классов можно отфильтровать лишние методы (которые не относятся к свойствам).