Java аннотации пример 1

Добавлено : 2 Nov 2008, 17:30

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

Собственно, тут я хотел вставить пример небольшого класса с помощью XDoclet-ов генерирующему указания для hibernate, но прошло слишком много времени: ... ну, в общем, я потерял исходники тех проектов где был использован XDoclet. Так что пришлось покопаться в библиотеке и вырезать пример из занимательной книжки "XDoclet in Action":

/** @hibernate.class  
*    table="Entry"  
*/
public class Entry {
  
private String id;
  
private String text;
  
private String title;
  
private Date createdDate;
    
  
public Entry() {
   }
  
 
  
/**
    * @hibernate.id generator-class="uuid.string"
    */
  
public String getId() {
     
return id;
  
}
  
/**
    * @hibernate.property
    */
  
public String getText() {
     
return text;
  
}
  
/**
    * @hibernate.property
    */
  
public String getTitle() {
     
return title;
  
}
}

Затем некоторая утилита (реализованная в виде ant-задачи) сканировала исходный код программы, выдирала из нее такие маркеры и генерировала что-то еще (xml-конфиги для hibernate).

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

Сегодня я покажу небольшой пример как аннотации могут облегчить жизнь. При создании приложения работающего с данными и использующего ORM (или просто объекты которые будут храниться в коллекциях, сортироваться, сравниваться) необходимо написание трех методов:

hashCode
equals
compareTo

Например, класс User будет выглядеть так:

public class User {
   
Integer id;
    String fio;
    Sex sex;
    Date birthday;
    List cats =
null;
   
/// и много-много методов getter-ов и setter-ов для указанных выше полей
 
   
public int hashCode() {
       
if (id != null) return id;
       
return super.hashCode();
   
}
 
   
public boolean equals(Object obj) {
       
if (obj == null) return false;
       
if (!(obj instanceof User)) return false;
        User user =
(User) obj;
       
if (id != null && user.id != null)
           
return id.equals(user.id);
       
return super.equals(obj);
   
}
 
   
public String toString() {
       
String scats= "none";
       
if (cats != null && cats.size() > 0)
           
scats = cats.toString();
       
return "<"+getClass().getName()+">: id="+id+"; fio="+ fio +
              
"; birthday="+ birthday +"; sex="+sex+"; cats="+ scats;
   
}
}

Собственная реализация hashCode, equals, toString, compareTo практически идентична и не не отличается для классов User, Cat, Animal (знай только меняй значения полей и все). Вот тут кроется проблема: при интенсивном рефакторинге можно добавить новое поле в класс, но забыть использовать его для остальных методов (надо сказать, что аннотации наиболее часто используются для решения подобной проблемы размазывания логики по нескольким файлам или методам класса).

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

Увы но решение будет не чистое,а потребует создания некоторого утилитного класса. От которого и должны будут наследоваться классы User, Cat ... Методы toString, equals, compareTo, hashCode будут перекрыты в этом утилитном классе, и при обращении к ним будут сканировать объект, искать в нем поля помеченные специальной аннотацией и выполнять логику сравнения или представления содержимого класса как строки с учетом установленных для полей класса маркеров:

package testi.ani.anno;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
/**
* Аннотация для пометки полей класса, обратите внимание на политику распространения
* аннотации RetentionPolicy.RUNTIME
* это значит, что маркеры не будут удалены после компиляции программы,
* а будут доступны во время выполнения кода
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface GenericField {
   
/**
     * Приоритет поля, именно это число будет управлять тем в каком порядке
     * выполняется сравнение полей в операциях equals, hashCode, compareTo
     * и в операции печати (преобразования в строку с помощью toString)
     *
     *
@return
    
*/
   
int priority() default 0;
 
       
/**
     * Это множитель на которые будет выполняться умножение поля при расчете hashCode.
     * Формула hashCode = поле_1*priority_1 + поле_2*priority_2 + ....
     *
     *
@return
    
*/
   
byte multiplier() default 17;
 
   
/**
     * Политика использования поля, определяется перечислымым типом данных  GenericField.USEPOLICY
     *
     *
@return
    
*/
   
USEPOLICY kind() default USEPOLICY.BOTH_PRINT_AND_EQUALS;
 
   
public static enum USEPOLICY {
                
/**
         * Поле используется только для преоразования объекта в строку toString
         */
       
ONLY_PRINT,
       
/**
         * Поле используется только при расчете hashCode, equals и compareTo
         */
       
ONLY_EQUALS,
       
/**
         * Поле используется для двух перечисленных выше операций
         */
       
BOTH_PRINT_AND_EQUALS
   
}
}

Теперь я привожу код служебного класса от которого должны наследоваться все ваши классы-домены. Внутри этого класса создан статический кэш, так что при многократном вызове методов toString, hashCode, ... не будет каждый раз заново вычисляться информация об путях интроспекции и том какие атрибуты помечены маркерами.

package testi.ani;
 
import testi.ani.anno.GenericField;
 
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Comparator;
 
/**
* Базовый класс для всех классов желающий получить способность к сравнению, преобразованию в строку
*/
public class TGenericRecord implements Comparable {
   
/**
     * Внутренний кэш результатов вычисления Reflection-информации
     */
   
static final private HashMap cache = new HashMap();
 
            
/**
     * Этот и все последующие методы устроены схожим образом:
     * 1 шаг, проверка того что в кэше есть правила извлеченная информация об аннотированных полях
     * 2 шаг, собственно вычисление запрошенной операции
     * Если в ходе выполнения операции произошла ошибка, то будет выброшено Runtime-исключение
     *
@return
    
*/
   
public int hashCode() {
       
final Classextends TGenericRecord> aClass = getClass();
        DiscoveryCache cachedRule = cache.get
(aClass);
       
if (cachedRule == null) {
           
cachedRule = discoveryClass(aClass);
            cache.put
(aClass, cachedRule);
       
}
       
try {
           
return cachedRule.callHashCode(this);
       
} catch (IllegalAccessException e) {
           
throw new RuntimeException(e);
       
}
    }
 
   
public boolean equals(Object obj) {
       
if (obj == null) return false;
       
if (!(obj instanceof TGenericRecord)) return false;
 
       
final Classextends TGenericRecord> aClass = getClass();
        DiscoveryCache cachedRule = cache.get
(aClass);
       
if (cachedRule == null) {
           
cachedRule = discoveryClass(aClass);
            cache.put
(aClass, cachedRule);
       
}
       
try {
           
return cachedRule.callEquals(this, obj);
       
} catch (IllegalAccessException e) {
           
throw new RuntimeException(e);
       
}
    }
 
   
public String toString() {
       
final Classextends TGenericRecord> aClass = getClass();
        DiscoveryCache cachedRule = cache.get
(aClass);
       
if (cachedRule == null) {
           
cachedRule = discoveryClass(aClass);
            cache.put
(aClass, cachedRule);
       
}
       
try {
           
return cachedRule.callToString(this);
       
} catch (IllegalAccessException e) {
           
throw new RuntimeException(e);
       
}
    }
 
   
public int compareTo(Object o) {
       
final Classextends TGenericRecord> aClass = getClass();
        DiscoveryCache cachedRule = cache.get
(aClass);
       
if (cachedRule == null) {
           
cachedRule = discoveryClass(aClass);
            cache.put
(aClass, cachedRule);
       
}
       
try {
           
return cachedRule.callCompareTo(this, o);
       
} catch (IllegalAccessException e) {
           
throw new RuntimeException(e);
       
}
    }
 
                               
/**
     * Служебный метод выполняющий поиск в классе всех полей, определение среди них тех,
     * кто маркирован аннотацией GenericField
     *
     *
@param aClass - Класс который нужно просканировать
     *
@return
    
*/
   
private DiscoveryCache discoveryClass(Classextends TGenericRecord> aClass) {
       
DiscoveryCache c = new DiscoveryCache();
        rec_discoveryClass
(aClass, c);
        Collections.sort
(c.fields, new Comparator() {
           
public int compare(CachedField o1, CachedField o2) {
               
return ((Integer) o1.anno.priority()).compareTo(o2.anno.priority());
           
}
        })
;
       
return c;
   
}
 
   
/**
     * Вспомогательная функция вызываемая из метода {
@see #discoveryClass} и
     * выполняющая рекурсивный спуск по иерархии наследования класса,
     * поиск в нем всех полей помеченный данной аннотацией и сохранение информации в кэш
     *
     *
@param aClass класс подлежащий сканированию
     *
@param c      кэш куда будут помещены все найденные поля и их маркеры
     */
   
private void rec_discoveryClass(Class aClass, DiscoveryCache c) {
       
if (aClass == null) return;
       
if (aClass.isInterface()) return;
       
if (aClass.isEnum()) return;
       
final Field[] declaredFields = aClass.getDeclaredFields();
       
for (Field declaredField : declaredFields) {
           
final GenericField anoField = declaredField.getAnnotation(GenericField.class);
           
if (anoField == null) continue;
            declaredField.setAccessible
(true);
            c.fields.add
(new CachedField(declaredField, anoField));
       
}
    }
 
 
                        
/**
     * Служебный класс представляющий собой кэш результатов сканирования определенных
     * типов данных (классов) на поля и их маркеры
     */
   
private class DiscoveryCache {
       
/**
         * Список пар: поле-аннотация
         */
       
ArrayList fields = new ArrayList();
 
       
/**
         * Метод выполняющий расчет hashCode на основании правил из кэша
         *
@param tGenericRecord объект для которого нужно рассчитать hashCode
         *
@return
        
* @throws IllegalAccessException
         */
       
public int callHashCode(TGenericRecord tGenericRecord) throws IllegalAccessException {
           
int summ = 0;
           
for (CachedField f : fields) {
               
final GenericField.USEPOLICY usepolicy = f.anno.kind();
               
if (usepolicy == GenericField.USEPOLICY.ONLY_PRINT) continue;
               
final Object o = f.field.get(tGenericRecord);
                summ += o.hashCode
() * f.anno.multiplier();
           
}
           
return summ;
       
}
 
       
/**
         * Метод выполняющий попытку сравнить два объекта на основании их маркерных правил
         *
@param objA Первый объект для сравнения
         *
@param objB Второй объект для сравнения
         *
@return
        
* @throws IllegalAccessException
         */
       
public boolean callEquals(TGenericRecord objA, Object objB) throws IllegalAccessException {
           
for (CachedField f : fields) {
               
final GenericField.USEPOLICY usepolicy = f.anno.kind();
               
if (usepolicy == GenericField.USEPOLICY.ONLY_PRINT) continue;
               
final Object a = f.field.get(objA);
               
final Object b = f.field.get(objB);
               
if (!a.equals(b)) return false;
           
}
           
return true;
       
}
 
                           
/**
         * Преобразование объекта в строку
         *
@param tGenericRecord
         *
@return
        
* @throws IllegalAccessException
         */
       
public String callToString(TGenericRecord tGenericRecord) throws IllegalAccessException {
           
StringBuilder s = new StringBuilder();
            s.append
("<").append(tGenericRecord.getClass().getName()).append(" ");
           
final int listSize = fields.size() - 1;
           
for (int i = 0; i <= listSize; i++) {
               
final CachedField f = fields.get(i);
               
final GenericField.USEPOLICY usepolicy = f.anno.kind();
               
if (usepolicy == GenericField.USEPOLICY.ONLY_EQUALS) continue;
               
final Object a = f.field.get(tGenericRecord);
                s.append
(f.field.getName()).append("=").append(a).append(i == listSize ? "" : "; ");
           
}
           
s.append(">");
           
return s.toString();
       
}
 
       
/**
         * Сравнение объектов как поддерживающих интерфейс Comparable
         *
@param objA первый объект для сравнения
         *
@param objB второй объект для сравнения
         *
@return
        
* @throws IllegalAccessException
         */
       
public int callCompareTo(TGenericRecord objA, Object objB) throws IllegalAccessException {
           
for (CachedField f : fields) {
               
final GenericField.USEPOLICY usepolicy = f.anno.kind();
               
if (usepolicy == GenericField.USEPOLICY.ONLY_PRINT) continue;
               
final Object a = f.field.get(objA);
               
final Object b = f.field.get(objB);
               
if (a instanceof Comparable) {
                   
@SuppressWarnings(value = "unchecked")
                   
final int cmpResult = ((Comparable) a).compareTo(b);
                   
if (cmpResult != 0) return cmpResult;
               
}
            }
           
return 0;
       
}
    }
 
   
/**
     * Очень простой класс, состоящий из двух полей: Field и Аннотация привязанная к полю класса
     */
   
private class CachedField {
       
Field field;
        GenericField anno;
 
       
private CachedField(Field field, GenericField anno) {
           
this.field = field;
           
this.anno = anno;
       
}
    }
}

Теперь пример использования. Сначала класс с данными и маркерным метками:

package testi.ani;
 
import testi.ani.anno.GenericField;
import testi.catsandusers.Sex;
 
 
/**
* Пример класса к которому применяютс аннотации для управления генерируемыми методами hashCode, equals, compareTo
*/
public class THuman extends TGenericRecord {
   
/**
     * Каждое из полей будет помечено тремя характеристиками:
     *
priority - порядок поля, влияет на то в каком порядке поля
     * будут выводиться на экран в методе toString
     * также на то в каком порядке поля будут сравниваться между собой
     * это важно для метода compareTo и, возможно, вычисления хэш-кода)
     * второй параметр
multiplier служит для задания величины числа множителя метода hashCode (значение по-умолчанию 17)
     * третий параметр
kind управляет тем, будут ли печаться в методе
     * toString поля на экран или будут учавствовать в сравнениях полей
     * поля не помеченные аннотацией GenericField не учавствуют ни в одном из трех методов
     */
   
@GenericField(priority = 0)
   
int id;
   
@GenericField(priority = 1, kind = GenericField.USEPOLICY.BOTH_PRINT_AND_EQUALS)
   
String fio;
 
   
@GenericField(priority = -1, kind = GenericField.USEPOLICY.ONLY_EQUALS)
   
Sex sex;
 
   
@GenericField(priority = 2, kind = GenericField.USEPOLICY.ONLY_PRINT)
   
double money;
 
   
public THuman(int id, String fio, Sex sex, double money) {
       
this.id = id;
       
this.fio = fio;
       
this.sex = sex;
       
this.money = money;
   
}
}

И завершаю пример следующим тестом:

package testi.ani;
 
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import testi.catsandusers.Sex;
 
import java.util.Arrays;
import java.util.Collection;
 
/**
* Небольшой тест промаркированного класса
*/
@RunWith(Parameterized.class)
public class TesterAni {
  
 
  
@Parameterized.Parameters
  
public static Collection humanMaker() {
     
return Arrays.asList(new Object[][]{
      {
        
true, 0,
        
new THuman(1, "vasyano", Sex.MALE, 100.0), new THuman(1, "vasyano", Sex.MALE, 100.0)
      }
,
     
{
        
false, +1,
        
new THuman(1, "vasyano", Sex.FEMALE, 100.0), new THuman(1, "vasyano", Sex.MALE, 100.0)
      }
,
     
{
        
false, -1,
        
new THuman(1, "Avasyano", Sex.MALE, 100.0), new THuman(1, "Bvasyano", Sex.MALE, 100.0)
      }
,
     
{
        
true, 0,
        
new THuman(1, "vasyano", Sex.MALE, 100.0), new THuman(1, "vasyano", Sex.MALE, 200.0)
      }
,
             
     
});
  
}
  
 
    
  
boolean ifEqual;
  
int compareResult;
   THuman humanA, humanB;
    
  
public TesterAni(boolean ifEqual, int compareResult, THuman humanA, THuman humanB) {
     
this.ifEqual = ifEqual;
     
this.compareResult = compareResult;
     
this.humanA = humanA;
     
this.humanB = humanB;
  
}
  
 
  
@Test
  
public void compareHumans() {
     
assertEquals("HumanA equals HumanB", humanA.equals(humanB), ifEqual);
      assertEquals
("HumanA compareTo HumanB", humanA.compareTo(humanB), compareResult);
  
}
}

Или вот еще пример использования более простой:

THuman vasyano = new THuman(10, "Vasyano", Sex.MALE, Math.random()*2000);
THuman petyano =
new THuman(10, "Petyanio", Sex.FEMALE, Math.random()*2000);
 
System.out.println
("petyano = " + petyano);
System.out.println
("vasyano = " + vasyano);
 
System.out.println
("vasyano hashCode = " + vasyano.hashCode() );
System.out.println
("petyano hashCode = " + petyano.hashCode() );
 
System.out.println
("vasyano eq petyano = " + vasyano.equals(petyano) );a
System.out.println
("vasyano compareTo petyano = " + vasyano.compareTo(petyano) );

А вот так будет выглядеть результат выполнения кода:

petyano = 10; fio=Petyanio; money=1455.1612655356755>
vasyano = 10; fio=Vasyano; money=176.3383329687862>
vasyano hashCode = -
1906494401
petyano hashCode = 1119803717
vasyano eq petyano = false
vasyano compareTo petyano = -1

Источник — http://black-zorro.com/mediawiki/Java_аннотации_пример_1