Персональные инструменты
 

Статически проверяемые ссылки на свойства Java-бинов — различия между версиями

Материал из CustisWiki

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

Текущая версия на 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 сгенеренных классов можно отфильтровать лишние методы (которые не относятся к свойствам).