пятница, 22 октября 2010 г.

Java сompile time annotations

Ох.. тут давече затеяли большой рефакторинг проекта в рамках которого я решил сделать пару классов immutable. Казалось бы, делаем все поля private + final и всё ок. Но нет, для ссылочных типов это не работает. Так как в этом случае мы не можем изменить ссылку на этот объект, которая храниться в поле, но за то можем изменить сам этот объект, например добавить в коллекцию ещё пару объектов. Тут есть два подхода, оба приводят к равно ценном результату (immutable классу):

  1. Требовать, чтобы все ссылочные типы также были immutable, как String например. Для коллекций можно использовать Collections.unmodifiableCollection метод.
  2. Второй способ заключается в основе ООП: инкапсуляции. Ни один метод не возвращает объект по ссылке, только его копию, например вместо возврата коллекции можно вернуть её неизменяемую копию с помощью описанного выше метода.
Вообщем то всё достаточно очевидно. Ввиду ряда особенностей и уменьшения последствий рефаторинга, посоветовавшись, я решил воспользоваться вторым способом. Но пока рассматривал первый метод, захотелось аннотацию, которая бы сигнализировала, что объект не изменяем и корректность класса проверялась на этапе компиляции. Если кого-то заинтересовала, краткое описание под катом.

Проверку сделал простой - просматриваются все не статические поля класса, они все должны быть private и final.
Стандартный javac нам уже не поможет. Будет использоваться утилита apt из стандартного набора JDK.
В начале напишем простенький класс и саму аннотацию:
  1. package me.drobushevich.blog.persist;  
  2.   
  3. import me.drobushevich.blog.annotation.Immutable;  
  4.   
  5. @Immutable  
  6. public final class User {  
  7.   
  8.     private int id;  
  9.     private final String name;  
  10.   
  11.     public User(int id, String name) {  
  12.         this.id = id;  
  13.         this.name = name;  
  14.     }  
  15.   
  16.     public int getId() {  
  17.         return id;  
  18.     }  
  19.   
  20.     public String getName() {  
  21.         return name;  
  22.     }  
  23.   
  24. }  
  1. package me.drobushevich.blog.annotation;  
  2.   
  3. import java.lang.annotation.ElementType;  
  4. import java.lang.annotation.Retention;  
  5. import java.lang.annotation.RetentionPolicy;  
  6. import java.lang.annotation.Target;  
  7.   
  8. @Retention(RetentionPolicy.RUNTIME)  
  9. @Target( { ElementType.TYPE })  
  10. public @interface Immutable {  
  11.   
  12. }  
Это простенькая аннотация, которая даже не содержит никаких полей и применяется только к классам. Теперь нам надо написать обработчик, который apt бы использовала для проверки наших классов, помеченных такой аннотацией. Для этого нам надо добавить tools.jar в classpath. Так как это стандартная библиотека JDK, то я модифицировал в Eclipse набор JDK библиотек, которые по умолчанию добавляются к каждому Java проекту, как это показано на скриншоте.
Теперь мы можем реализовать нужный нам обработчик. В лучших традициях Java и ООП, для этого нам понадобится написать два класса Factory и Processor, который эта Factory возвращает. Начнём с конца :)
  1. package me.drobushevich.blog.annotation;  
  2.   
  3. import static com.sun.mirror.declaration.Modifier.FINAL;  
  4. import static com.sun.mirror.declaration.Modifier.PRIVATE;  
  5. import static com.sun.mirror.declaration.Modifier.STATIC;  
  6.   
  7. import java.util.Collection;  
  8.   
  9. import com.sun.mirror.apt.AnnotationProcessor;  
  10. import com.sun.mirror.apt.AnnotationProcessorEnvironment;  
  11. import com.sun.mirror.declaration.AnnotationMirror;  
  12. import com.sun.mirror.declaration.AnnotationTypeDeclaration;  
  13. import com.sun.mirror.declaration.ClassDeclaration;  
  14. import com.sun.mirror.declaration.Declaration;  
  15. import com.sun.mirror.declaration.FieldDeclaration;  
  16. import com.sun.mirror.declaration.Modifier;  
  17.   
  18. public class ImmutableAnnotationProcessor implements AnnotationProcessor {  
  19.   
  20.     public static final String IMMUTABLE_ANNOTATION_NAME = "me.drobushevich.blog.annotation.Immutable";  
  21.   
  22.     private AnnotationProcessorEnvironment environment;  
  23.   
  24.     private AnnotationTypeDeclaration immutableDeclaration;  
  25.   
  26.     public ImmutableAnnotationProcessor(AnnotationProcessorEnvironment env) {  
  27.         environment = env;  
  28.         immutableDeclaration = (AnnotationTypeDeclaration) environment.getTypeDeclaration(IMMUTABLE_ANNOTATION_NAME);  
  29.     }  
  30.   
  31.     @Override  
  32.     public void process() {  
  33.         Collection<Declaration> declarations = environment.getDeclarationsAnnotatedWith(immutableDeclaration);  
  34.         for (Declaration declaration : declarations) {  
  35.             processAnnotations(declaration);  
  36.         }  
  37.     }  
  38.   
  39.     private void processAnnotations(Declaration declaration) {  
  40.         Collection<AnnotationMirror> annotations = declaration.getAnnotationMirrors();  
  41.         for (AnnotationMirror mirror : annotations) {  
  42.             if (mirror.getAnnotationType().getDeclaration().equals(immutableDeclaration)   
  43.                     && declaration instanceof ClassDeclaration) {  
  44.                 ClassDeclaration classDeclaration = (ClassDeclaration) declaration;  
  45.                 System.out.println("Checking immutable class [" + classDeclaration.getQualifiedName() + "] ....");  
  46.                 for (FieldDeclaration field : classDeclaration.getFields()) {  
  47.                     Collection<Modifier> fieldModifiers = field.getModifiers();  
  48.                     System.out.println("Field [" + field.getSimpleName() + "] modefiers: " + fieldModifiers);  
  49.                     if (!fieldModifiers.contains(STATIC)   
  50.                             && (!fieldModifiers.contains(PRIVATE) || !fieldModifiers.contains(FINAL))) {  
  51.                         throw new RuntimeException("Field [" + field.getSimpleName()   
  52.                                 + "] of class [" + classDeclaration.getQualifiedName()  
  53.                                 + "] must be private and final, because this class is annotated as immutable");  
  54.                     } else {  
  55.                         System.out.println("Field [" + field.getSimpleName() + "] is ok");  
  56.                     }  
  57.                 }  
  58.             }  
  59.         }  
  60.     }  
  61.   
  62. }  
Ничего сложно не происходит, метод пробегается по всем компилируемым элементам, и если это класс и он помечен нашей аннотацией, то получаем список полей и проверяем описанное выше условие. Factory того проще, возвращает список поддерживаемых аннотаций, который как не сложно догадаться содержит только одну, а другой метод возвращает описанный выше процессор
  1. package me.drobushevich.blog.annotation;  
  2.   
  3. import static com.sun.mirror.apt.AnnotationProcessors.NO_OP;  
  4. import static java.util.Collections.emptyList;  
  5. import static java.util.Collections.singletonList;  
  6. import static me.drobushevich.blog.annotation.ImmutableAnnotationProcessor.IMMUTABLE_ANNOTATION_NAME;  
  7.   
  8. import java.util.Collection;  
  9. import java.util.Set;  
  10.   
  11. import com.sun.mirror.apt.AnnotationProcessor;  
  12. import com.sun.mirror.apt.AnnotationProcessorEnvironment;  
  13. import com.sun.mirror.apt.AnnotationProcessorFactory;  
  14. import com.sun.mirror.declaration.AnnotationTypeDeclaration;  
  15.   
  16. public class ImmutableAnnotationProcessorFactory implements AnnotationProcessorFactory {  
  17.   
  18.     @Override  
  19.     public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> declarations, AnnotationProcessorEnvironment env) {  
  20.         AnnotationProcessor result;  
  21.         if (declarations.isEmpty()) {  
  22.             result = NO_OP;  
  23.         } else {  
  24.             result = new ImmutableAnnotationProcessor(env);  
  25.         }  
  26.         return result;  
  27.     }  
  28.   
  29.     @Override  
  30.     public Collection<String> supportedAnnotationTypes() {  
  31.         return singletonList(IMMUTABLE_ANNOTATION_NAME);  
  32.     }  
  33.   
  34.     @Override  
  35.     public Collection<String> supportedOptions() {  
  36.         return emptyList();  
  37.     }  
  38.   
  39. }  
Если остались непонятности, можно заглянуть на страницу официального туториала, теперь уже на домене oracle, или к комментариях похоливарить :) Да, и чуть не забыл, компилируем User'а мы теперь не javac, а apt, как-то так к примеру:
  1. apt -classpath bin -factory me.drobushevich.blog.annotation.ImmutableAnnotationProcessorFactory  \  
  2. src/me/drobushevich/blog/persist/User.java  

Комментариев нет:

Отправить комментарий