четверг, 11 октября 2012 г.

Debug Java annotation processing

Как известно, Java имеет уже довольно таки развитые механизмы мета-программирования посредством использования аннотаций. Самым интересным в этих механизмах является возможность создания так называемого процессора аннотаций, который будет вызываться компилятором автоматически. Однако в процессе разработки таких процессоров аннотаций возникает одна не очень тривиальная проблема - как осуществить тестирование и отладку самого процессора аннотаций?
Гуглинг выдает некоторые инструкции по этому поводу, но они предписывают использовать довольно сложные средства разработки плагинов Eclipse, создавать плагин, в нем определять точку расширения и осуществлять отладку этого плагина. На мой взгляд эту проблему можно решить значительно проще.

С помощью вот такой очень несложной функции становится очень легко тестировать, и (что самое главное!) дебажить процессоры аннотаций в Java:
package ap.util;

import java.io.File;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

public class CompilerUtil {

 public static boolean compile(File... files) {

  final JavaCompiler compiler = 
      ToolProvider.getSystemJavaCompiler();

  return compiler.getTask(null, null, null, null, null,
      compiler.getStandardFileManager(null, null, null)
         .getJavaFileObjects(files)).call();
 }
}
Чтобы это работало необходимо чтобы в classpath присутствовал tools.jar из JDK. В Eclipse этого присутствия можно добиться следующим образом:
В Project Explorer выбрать мышкой проект, нажать правую кнопку мышки, в появившемся контекстном меню нажать на пункт Build Path а в появившемся при этом подменю - пункт Configure Build Path ... . В открывшемся окне выбрать вкладку Libraries и нажать кнопку (справа список кнопок) Add Variable ... . В новом окне нажать сначала кнопку Configure Variables ... (она снизу под списком переменных) и в новом окне создать новую переменную JDK_HOME в которой указать путь к вашему JDK. Нажать OK, и вернувшись в предыдущее окно, выбрать в списке вновь созданную переменную JDK_HOME и нажать справа сверху от списка переменных кнопку Extend. В появившемся окне выбрать в дереве каталог lib, раскрыть его, и выбрать файл tools.jar. Дальше нажимать OK в каждом из диалогов. Всё, библиотека tools.jar подключена к вашему проекту.

Вот пример самого простого annotation processor:
package ap.sample;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes(value = { "*" })
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class APSample extends AbstractProcessor {

  private Messager messager;

  @Override
  public synchronized void init(
         ProcessingEnvironment processingEnv) {

    messager = processingEnv.getMessager();
  }

  @Override
  public boolean process(
        final Set<? extends TypeElement> annotations, 
        final RoundEnvironment env) 
  {

     for (final TypeElement annotation : annotations) {

       for (final Element annotated : 
               env.getElementsAnnotatedWith(annotation)) {

          messager.printMessage(Kind.WARNING, 
            annotation.getSimpleName(), annotated);

       }
     }
     return true;
  }
}
Чтобы он автоматически вызывался при компиляции, нужно в каталоге META-INF/services создать файл с названием javax.annotation.processing.Processor где просто указать полное имя класса, в данном случае:
ap.sample.APSample
Теперь можно создать например такую тестовую аннотацию:
package ap.sample;

public @interface ApSampleAnnotation {
}
и аннотировать ней тестовый класс:
package ap.sample;

@ApSampleAnnotation
public class ApSampleClass {

}
Теперь можно создать такой unit-test:
package ap.sample;

import static org.junit.Assert.assertTrue;
import static ap.util.CompilerUtil.compile;

public class ApSampleTest {

  @Test
  public void testAp() {
    assertTrue(compile(new File(
      "test/ap/sample/ApSampleClass.java")));
  }
}
, поставить в коде процессора аннотаций, то есть в классе APSample, где нибудь точку прерывания отладчика (debug breakpoint), запустить отладку теста ApSampleTest (Debug as ... / JUnit test) и посмотреть как отладчик остановится там где мы поставили ему точку прерывания. Что интересно, стектрейс при этом будет таким:
APSample.process(Set<TypeElement>, RoundEnvironment) line: 31
JavacProcessingEnvironment.callProcessor(Processor, Set<TypeElement>, RoundEnvironment) line: 627
JavacProcessingEnvironment.discoverAndRunProcs(Context, Set<TypeElement>, List<ClassSymbol>, List<PackageSymbol>) line: 556
JavacProcessingEnvironment.doProcessing(Context, List<JCCompilationUnit>, List<ClassSymbol>, Iterable<PackageSymbol>) line: 701
JavaCompiler.processAnnotations(List<JCCompilationUnit>, List<String>) line: 987
JavaCompiler.compile(List<JavaFileObject>, List<String>,Iterable<Processor>) line: 727
Main.compile(String[], Context, List<JavaFileObject>, Iterable<Processor>) line: 353
JavacTaskImpl.call() line: 115
CompilerUtil.compile(File...) line: 14
ApSampleTest.testAp() line: 16
...
А так же мы получим от компилятора сообщение о том что процесс компиляции был прерван изза warning'a который сгенерировал наш процессор, что и докажет нам что всё честно и мы находимся прямо посреди процесса компиляции java-класса.

Затем полученный процессор аннотаций, упакованный в jar, вместе с файлом META-INF/services/javax.annotation.processing.Processor внутри (в котором он прозрачно подключается к процессу компиляции), можно очень удобно использовать в Eclipse через его механизмы поддержки таких процессоров. Подключается процессор аннотаций к проекту в диалоге Properties проекта в пункте Java Compiler/Annotation Processing/Factory Path, там нужно просто нажать Add Jar (или Add Variable, это по сути тоже самое, разница только в том как будет определен путь к файлу) и там выбрать jar с процессором. После этого проект будет автоматически использовать процессор аннотаций прямо в среде разработки.

Подробнее по ссылкам:
  1. http://jcp.org/en/jsr/detail?id=269
  2. http://docs.oracle.com/javase/6/docs/api/javax/annotation/processing/package-summary.html
  3. http://docs.oracle.com/javase/1.5.0/docs/guide/apt/GettingStarted.html
  4. http://docs.oracle.com/javase/tutorial/java/javaOO/annotations.html
  5. http://www.eclipse.org/jdt/apt/introToAPT.html