Java аннотации. Пример 1

April 14, 2008Comments Off on Java аннотации. Пример 1

Когда в конце 2004 г. вышла версия java 1.5 (или 5.0, если так sun-у будет приятнее), то одной из самых ожидаемых мною новостей была добавленная поддержка аннотаций. Аннотации это способ внедрения в исходный текст программы специальных маркеров, меток. Метки эти применяются к классам, полям класса, методам, параметрам методов и даже отдельным переменным объявленным внутри какой-то функции. Затем эти метки (играющие роль как бы языка внутри языка и позволяющего расширить его новыми "фишками") обрабатывались специальным образом или компилятором или некоторой утилитой, создающей на основании меток, например, конфигурационный файл приложения. Надо сказать, что идея аннотаций или метаданных появилась не тогда в 2004 (Боже ... прошло целых четыре года), а гораздо раньше (и java явно не была самая первая). Еще до выхода java 1.5 и некоторое время после, я использовал XDoclet. Решение похожее на аннотации в java и позволяющее метить код специальными маркерами (правда, реализовывалось это не отдельными конструкциями поддерживаемыми на уровне языка а вставками в текст комментариев).

Собственно, тут я хотел вставить пример небольшого класса с помощью XDoclet-ов генерирующему указания для hibernate, но прошло слишком много времени: ... ну, в общем, я потерял исходники тех проектов где был использован XDoclet. Так что пришлось покопаться в библиотеке и вырезать пример из занимательной книжки "XDoclet in Action":
  1. * @hibernate.class   
  2.  *    table="Entry"   
  3.  */
  4. public class Entry {
  5.   private String id;
  6.   private String text;
  7.   private String title;
  8.   private Date createdDate;
  9.  
  10.   public Entry() {
  11.   }
  12.  
  13.   /**
  14.    * @hibernate.id                       
  15.    *     generator-class="uuid.string"   
  16.    */
  17.   public String getId() {
  18.     return id;
  19.   }
  20.   /**
  21.    * @hibernate.property       
  22.    */
  23.   public String getText() {
  24.     return text;
  25.   }
  26.   /**
  27.    * @hibernate.property       
  28.    */
  29.   public String getTitle() {
  30.     return title;
  31.   }
Затем некоторая утилита (реализованная в виде ant-задачи) сканировала исходный код программы, выдирала из нее такие маркеры и генерировала что-то еще (xml-конфиги для hibernate).

Собственно, XDoclet не был единственным продуктом такого класса (я еще слышал об Jakarta Commons Attributes, только слышал, не более).

Сегодня я покажу небольшой пример как аннотации могут облегчить жизнь. При создании приложения работающего с данными и использующего ORM (или просто объекты которые будут храниться в коллекциях, сортироваться, сравниваться) необходимо написание трех методов:
 hashCode
 equals
 compareTo
Например, класс User будет выглядеть так:
  1. public class User {
  2.     Integer id;
  3.     String fio;
  4.     Sex sex;
  5.     Date birthday;
  6.     List <Cat> cats = null;
  7.     /// и много-много методов getter-ов и setter-ов для указанных выше полей
  8.  
  9.     public int hashCode() {
  10.         if (id != null) return id;
  11.         return super.hashCode();
  12.     }
  13.  
  14.     public boolean equals(Object obj) {
  15.         if (obj == null) return false;
  16.         if (!(obj instanceof User)) return false;
  17.         User user = (User) obj;
  18.         if (id != null && user.id != null)
  19.             return id.equals(user.id);
  20.         return super.equals(obj);
  21.     }
  22.  
  23.     public String toString() {
  24.         String scats= "none";
  25.         if (cats != null && cats.size() > 0)
  26.             scats = cats.toString();
  27.         return "<"+getClass().getName()+">: id="+id+"; fio="+ fio +
  28.                "; birthday="+ birthday +"; sex="+sex+"; cats="+ scats;
  29.     }
  30. }
Собственная реализация hashCode, equals, toString, compareTo практически идентична и не не отличается для классов User, Cat, Animal (знай только меняй значения полей и все). Вот тут кроется проблема: при интенсивном рефакторинге можно добавить новое поле в класс, но забыть использовать его для остальных методов (надо сказать, что аннотации наиболее часто используются для решения подобной проблемы размазывания логики по нескольким файлам или методам класса).

Я создам новую аннотацию, позволяющую метить некоторые из полей класса маркером "учавствует в сравнении", "нужно вывести значение этого поля внутри метода toString". Единственное слабое место, что автоматически сгенерировать для класса новые методы не возможно (если не использовать CGLIB или JavaProxy и при этом перекроить правила создания объектов User, Cat ...). Хотя при создании аннотации можно указать кому она будет видна (только компилятору и после генерации class-файла подлежать удалению, или же, информация об метках будет доступна и на стадии выполнения программы, так что к ней можно добраться с помощью java reflection).

Увы но решение будет не чистое,а потребует создания некоторого утилитного класса. От которого и должны будут наследоваться классы User, Cat ... Методы toString, equals, compareTo, hashCode будут перекрыты в этом утилитном классе, и при обращении к ним будут сканировать объект, искать в нем поля помеченные специальной аннотацией и выполнять логику сравнения или представления содержимого класса как строки с учетом установленных для полей класса маркеров:
  1. package testi.ani.anno;
  2.  
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5.  
  6. /**
  7.  * Аннотация для пометки полей класса, обратите внимание на политику распространения 
  8.  * аннотации RetentionPolicy.RUNTIME
  9.  * это значит, что маркеры не будут удалены после компиляции программы, 
  10.  * а будут доступны во время выполнения кода
  11.  */
  12. @Retention(RetentionPolicy.RUNTIME)
  13. public @interface GenericField {
  14.     /**
  15.      * Приоритет поля, именно это число будет управлять тем в каком порядке 
  16.      * выполняется сравнение полей в операциях equals, hashCode, compareTo
  17.      * и в операции печати (преобразования в строку с помощью toString)
  18.      *
  19.      * @return
  20.      */
  21.     int priority() default 0;
  22.  
  23.     /**
  24.      * Это множитель на которые будет выполняться умножение поля при расчете hashCode.
  25.      * Формула hashCode = поле_1*priority_1 + поле_2*priority_2 + ....
  26.      *
  27.      * @return
  28.      */
  29.     byte multiplier() default 17;
  30.  
  31.     /**
  32.      * Политика использования поля, определяется перечислымым типом данных  GenericField.USEPOLICY
  33.      *
  34.      * @return
  35.      */
  36.     USEPOLICY kind() default USEPOLICY.BOTH_PRINT_AND_EQUALS;
  37.  
  38.     public static enum USEPOLICY {
  39.         /**
  40.          * Поле используется только для преоразования объекта в строку toString
  41.          */
  42.         ONLY_PRINT,
  43.         /**
  44.          * Поле используется только при расчете hashCode, equals и compareTo
  45.          */
  46.         ONLY_EQUALS,
  47.         /**
  48.          * Поле используется для двух перечисленных выше операций
  49.          */
  50.         BOTH_PRINT_AND_EQUALS
  51.     }
  52. }
Теперь я привожу код служебного класса от которого должны наследоваться все ваши классы-домены. Внутри этого класса создан статический кэш, так что при многократном вызове методов toString, hashCode, ... не будет каждый раз заново вычисляться информация об путях интроспекции и том какие атрибуты помечены маркерами.
  1. package testi.ani;
  2.  
  3. import testi.ani.anno.GenericField;
  4.  
  5. import java.lang.reflect.Field;
  6. import java.util.ArrayList;
  7. import java.util.Collections;
  8. import java.util.HashMap;
  9. import java.util.Comparator;
  10.  
  11. /**
  12.  * Базовый класс для всех классов желающий получить способность к сравнению, преобразованию в строку
  13.  */
  14. public class TGenericRecord implements Comparable {
  15.     /**
  16.      * Внутренний кэш результатов вычисления Reflection-информации
  17.      */
  18.     static final private HashMap<Class, DiscoveryCache> cache = new HashMap<Class, DiscoveryCache>();
  19.  
  20.     /**
  21.      * Этот и все последующие методы устроены схожим образом:
  22.      * 1 шаг, проверка того что в кэше есть правила извлеченная информация об аннотированных полях
  23.      * 2 шаг, собственно вычисление запрошенной операции
  24.      * Если в ходе выполнения операции произошла ошибка, то будет выброшено Runtime-исключение 
  25.      * @return
  26.      */
  27.     public int hashCode() {
  28.         final Class<? extends TGenericRecord> aClass = getClass();
  29.         DiscoveryCache cachedRule = cache.get(aClass);
  30.         if (cachedRule == null) {
  31.             cachedRule = discoveryClass(aClass);
  32.             cache.put(aClass, cachedRule);
  33.         }
  34.         try {
  35.             return cachedRule.callHashCode(this);
  36.         } catch (IllegalAccessException e) {
  37.             throw new RuntimeException(e);
  38.         }
  39.     }
  40.  
  41.     public boolean equals(Object obj) {
  42.         if (obj == null) return false;
  43.         if (!(obj instanceof TGenericRecord)) return false;
  44.  
  45.         final Class<? extends TGenericRecord> aClass = getClass();
  46.         DiscoveryCache cachedRule = cache.get(aClass);
  47.         if (cachedRule == null) {
  48.             cachedRule = discoveryClass(aClass);
  49.             cache.put(aClass, cachedRule);
  50.         }
  51.         try {
  52.             return cachedRule.callEquals(this, obj);
  53.         } catch (IllegalAccessException e) {
  54.             throw new RuntimeException(e);
  55.         }
  56.     }
  57.  
  58.     public String toString() {
  59.         final Class<? extends TGenericRecord> aClass = getClass();
  60.         DiscoveryCache cachedRule = cache.get(aClass);
  61.         if (cachedRule == null) {
  62.             cachedRule = discoveryClass(aClass);
  63.             cache.put(aClass, cachedRule);
  64.         }
  65.         try {
  66.             return cachedRule.callToString(this);
  67.         } catch (IllegalAccessException e) {
  68.             throw new RuntimeException(e);
  69.         }
  70.     }
  71.  
  72.     public int compareTo(Object o) {
  73.         final Class<? extends TGenericRecord> aClass = getClass();
  74.         DiscoveryCache cachedRule = cache.get(aClass);
  75.         if (cachedRule == null) {
  76.             cachedRule = discoveryClass(aClass);
  77.             cache.put(aClass, cachedRule);
  78.         }
  79.         try {
  80.             return cachedRule.callCompareTo(this, o);
  81.         } catch (IllegalAccessException e) {
  82.             throw new RuntimeException(e);
  83.         }
  84.     }
  85.  
  86.     /**
  87.      * Служебный метод выполняющий поиск в классе всех полей, определение среди них тех, 
  88.      * кто маркирован аннотацией GenericField
  89.      *
  90.      * @param aClass - Класс который нужно просканировать
  91.      * @return
  92.      */
  93.     private DiscoveryCache discoveryClass(Class<? extends TGenericRecord> aClass) {
  94.         DiscoveryCache c = new DiscoveryCache();
  95.         rec_discoveryClass(aClass, c);
  96.         Collections.sort(c.fields, new Comparator<CachedField>() {
  97.             public int compare(CachedField o1, CachedField o2) {
  98.                 return ((Integer) o1.anno.priority()).compareTo(o2.anno.priority());
  99.             }
  100.         });
  101.         return c;
  102.     }
  103.  
  104.     /**
  105.      * Вспомогательная функция вызываемая из метода {@see #discoveryClass} и 
  106.      * выполняющая рекурсивный спуск по иерархии наследования класса,
  107.      * поиск в нем всех полей помеченный данной аннотацией и сохранение информации в кэш
  108.      *
  109.      * @param aClass класс подлежащий сканированию
  110.      * @param c      кэш куда будут помещены все найденные поля и их маркеры
  111.      */
  112.     private void rec_discoveryClass(Class aClass, DiscoveryCache c) {
  113.         if (aClass == null) return;
  114.         if (aClass.isInterface()) return;
  115.         if (aClass.isEnum()) return;
  116.         final Field[] declaredFields = aClass.getDeclaredFields();
  117.         for (Field declaredField : declaredFields) {
  118.             final GenericField anoField = declaredField.getAnnotation(GenericField.class);
  119.             if (anoField == null) continue;
  120.             declaredField.setAccessible(true);
  121.             c.fields.add(new CachedField(declaredField, anoField));
  122.         }
  123.     }
  124.  
  125.  
  126.     /**
  127.      * Служебный класс представляющий собой кэш результатов сканирования определенных 
  128.      * типов данных (классов) на поля и их маркеры
  129.      */
  130.     private class DiscoveryCache {
  131.         /**
  132.          * Список пар: поле-аннотация
  133.          */
  134.         ArrayList<CachedField> fields = new ArrayList<CachedField>();
  135.  
  136.         /**
  137.          * Метод выполняющий расчет hashCode на основании правил из кэша
  138.          * @param tGenericRecord объект для которого нужно рассчитать hashCode
  139.          * @return
  140.          * @throws IllegalAccessException
  141.          */
  142.         public int callHashCode(TGenericRecord tGenericRecord) throws IllegalAccessException {
  143.             int summ = 0;
  144.             for (CachedField f : fields) {
  145.                 final GenericField.USEPOLICY usepolicy = f.anno.kind();
  146.                 if (usepolicy == GenericField.USEPOLICY.ONLY_PRINT) continue;
  147.                 final Object o = f.field.get(tGenericRecord);
  148.                 summ += o.hashCode() * f.anno.multiplier();
  149.             }
  150.             return summ;
  151.         }
  152.  
  153.         /**
  154.          * Метод выполняющий попытку сравнить два объекта на основании их маркерных правил
  155.          * @param objA Первый объект для сравнения
  156.          * @param objB Второй объект для сравнения
  157.          * @return
  158.          * @throws IllegalAccessException
  159.          */
  160.         public boolean callEquals(TGenericRecord objA, Object objB) throws IllegalAccessException {
  161.             for (CachedField f : fields) {
  162.                 final GenericField.USEPOLICY usepolicy = f.anno.kind();
  163.                 if (usepolicy == GenericField.USEPOLICY.ONLY_PRINT) continue;
  164.                 final Object a = f.field.get(objA);
  165.                 final Object b = f.field.get(objB);
  166.                 if (!a.equals(b)) return false;
  167.             }
  168.             return true;
  169.         }
  170.  
  171.         /**
  172.          * Преобразование объекта в строку
  173.          * @param tGenericRecord
  174.          * @return
  175.          * @throws IllegalAccessException
  176.          */
  177.         public String callToString(TGenericRecord tGenericRecord) throws IllegalAccessException {
  178.             StringBuilder s = new StringBuilder();
  179.             s.append("<").append(tGenericRecord.getClass().getName()).append(" ");
  180.             final int listSize = fields.size() - 1;
  181.             for (int i = 0; i <= listSize; i++) {
  182.                 final CachedField f = fields.get(i);
  183.                 final GenericField.USEPOLICY usepolicy = f.anno.kind();
  184.                 if (usepolicy == GenericField.USEPOLICY.ONLY_EQUALS) continue;
  185.                 final Object a = f.field.get(tGenericRecord);
  186.                 s.append(f.field.getName()).append("=").append(a).append(i == listSize ? "" : "; ");
  187.             }
  188.             s.append(">");
  189.             return s.toString();
  190.         }
  191.  
  192.         /**
  193.          * Сравнение объектов как поддерживающих интерфейс Comparable
  194.          * @param objA первый объект для сравнения
  195.          * @param objB второй объект для сравнения
  196.          * @return
  197.          * @throws IllegalAccessException
  198.          */
  199.         public int callCompareTo(TGenericRecord objA, Object objB) throws IllegalAccessException {
  200.             for (CachedField f : fields) {
  201.                 final GenericField.USEPOLICY usepolicy = f.anno.kind();
  202.                 if (usepolicy == GenericField.USEPOLICY.ONLY_PRINT) continue;
  203.                 final Object a = f.field.get(objA);
  204.                 final Object b = f.field.get(objB);
  205.                 if (a instanceof Comparable) {
  206.                     @SuppressWarnings(value = "unchecked")
  207.                     final int cmpResult = ((Comparable) a).compareTo(b);
  208.                     if (cmpResult != 0) return cmpResult;
  209.                 }
  210.             }
  211.             return 0;
  212.         }
  213.     }
  214.  
  215.     /**
  216.      * Очень простой класс, состоящий из двух полей: Field и Аннотация привязанная к полю класса
  217.      */
  218.     private class CachedField {
  219.         Field field;
  220.         GenericField anno;
  221.  
  222.         private CachedField(Field field, GenericField anno) {
  223.             this.field = field;
  224.             this.anno = anno;
  225.         }
  226.     }
  227. }
Теперь пример использования. Сначала класс с данными и маркерным метками:
  1. package testi.ani;
  2.  
  3. import testi.ani.anno.GenericField;
  4. import testi.catsandusers.Sex;
  5.  
  6.  
  7. /**
  8.  * Пример класса к которому применяютс аннотации для управления генерируемыми методами hashCode, equals, compareTo
  9.  */
  10. public class THuman extends TGenericRecord {
  11.     /**
  12.      * Каждое из полей будет помечено тремя характеристиками:
  13.      * <b>priority</b> - порядок поля, влияет на то в каком порядке поля 
  14.      * будут выводиться на экран в методе toString
  15.      * также на то в каком порядке поля будут сравниваться между собой 
  16.      * это важно для метода compareTo и, возможно, вычисления хэш-кода)
  17.      * второй параметр <b>multiplier</b> служит для задания величины числа множителя метода hashCode (значение по-умолчанию 17)
  18.      * третий параметр <b>kind</b> управляет тем, будут ли печаться в методе 
  19.      * toString поля на экран или будут учавствовать в сравнениях полей
  20.      * поля не помеченные аннотацией GenericField не учавствуют ни в одном из трех методов 
  21.      */
  22.     @GenericField(priority = 0)
  23.     int id;
  24.     @GenericField(priority = 1, kind = GenericField.USEPOLICY.BOTH_PRINT_AND_EQUALS)
  25.     String fio;
  26.  
  27.     @GenericField(priority = -1, kind = GenericField.USEPOLICY.ONLY_EQUALS)
  28.     Sex sex;
  29.  
  30.     @GenericField(priority = 2, kind = GenericField.USEPOLICY.ONLY_PRINT)
  31.     double money;
  32.  
  33.     public THuman(int id, String fio, Sex sex, double money) {
  34.         this.id = id;
  35.         this.fio = fio;
  36.         this.sex = sex;
  37.         this.money = money;
  38.     }
  39. }
И завершаю пример следующим тестом:
  1. package testi.ani;
  2.  
  3. import static org.junit.Assert.*;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.junit.runners.Parameterized;
  7. import testi.catsandusers.Sex;
  8.  
  9. import java.util.Arrays;
  10. import java.util.Collection;
  11.  
  12. /**
  13.  * Небольшой тест промаркированного класса
  14.  */
  15. @RunWith(Parameterized.class)
  16. public class TesterAni {
  17.  
  18.     @Parameterized.Parameters
  19.     public static Collection humanMaker() {
  20.         return Arrays.asList(new Object[][]{
  21.                 {
  22.                         true, 0,
  23.                         new THuman(1, "vasyano", Sex.MALE, 100.0), new THuman(1, "vasyano", Sex.MALE, 100.0)
  24.                 },
  25.                 {
  26.                         false, +1,
  27.                         new THuman(1, "vasyano", Sex.FEMALE, 100.0), new THuman(1, "vasyano", Sex.MALE, 100.0)
  28.                 },
  29.                 {
  30.                         false, -1,
  31.                         new THuman(1, "Avasyano", Sex.MALE, 100.0), new THuman(1, "Bvasyano", Sex.MALE, 100.0)
  32.                 },
  33.                 {
  34.                         true, 0,
  35.                         new THuman(1, "vasyano", Sex.MALE, 100.0), new THuman(1, "vasyano", Sex.MALE, 200.0)
  36.                 },
  37.  
  38.         });
  39.     }
  40.  
  41.  
  42.     boolean ifEqual;
  43.     int compareResult;
  44.     THuman humanA, humanB;
  45.  
  46.     public TesterAni(boolean ifEqual, int compareResult, THuman humanA, THuman humanB) {
  47.         this.ifEqual = ifEqual;
  48.         this.compareResult = compareResult;
  49.         this.humanA = humanA;
  50.         this.humanB = humanB;
  51.     }
  52.  
  53.     @Test
  54.     public void compareHumans() {
  55.         assertEquals("HumanA equals HumanB", humanA.equals(humanB), ifEqual);
  56.         assertEquals("HumanA compareTo HumanB", humanA.compareTo(humanB), compareResult);
  57.     }
  58. }
Или вот еще пример использования более простой:
  1. THuman vasyano = new THuman(10, "Vasyano", Sex.MALE, Math.random()*2000);
  2.         THuman petyano = new THuman(10, "Petyanio", Sex.FEMALE, Math.random()*2000);
  3.  
  4.         System.out.println("petyano = " + petyano);
  5.         System.out.println("vasyano = " + vasyano);
  6.  
  7.         System.out.println("vasyano hashCode = " + vasyano.hashCode() );
  8.         System.out.println("petyano hashCode = " + petyano.hashCode() );
  9.  
  10.         System.out.println("vasyano eq petyano = " + vasyano.equals(petyano) );
  11.  
  12.         System.out.println("vasyano compareTo petyano = " + vasyano.compareTo(petyano) );
А вот так будет выглядеть результат выполнения кода:
petyano = <testi.ani.THuman id=10; fio=Petyanio; money=1455.1612655356755>
vasyano = <testi.ani.THuman id=10; fio=Vasyano; money=176.3383329687862>
vasyano hashCode = -1906494401
petyano hashCode = 1119803717
vasyano eq petyano = false
vasyano compareTo petyano = -1