Raportowanie z kodu za pomocą adnotacji w czasie kompilacji
Długi tytuł, czyli będzie ciekawie.
Co chcemy zrobić?
Załóżmy, że chcemy stworzyć sobie narzędzie do raportowania postępów w projekcie. Potrzebujemy czegoś co pozwoli nam na oznaczanie w kodzie informacji typu TODO, czy identyfikacji miejsca w którym mamy buga. Ogólnie istnieją narzędzia takie jak Checkstyle, które potrafią analizując kod źródłowy wyłapać jakieś standardowe wzorce w komentarzach. My chcemy czegoś więcej.
Rozwiązaniem są oczywiście adnotacje. Dla uproszczenia pokażę co i jak przyjmując, że mamy tylko jedną adnotację i możemy nią oznaczać tylko metody. Raport będzie produkowany do logu kompilatora. Oczywiście będziemy potrzebować jakiegoś narzędzia do obróbki adnotacji… w trakcie kompilacji. I o tym będzie ten wpis.
Adnotacja
Zacznijmy od zdefiniowania adnotacji @Todo
Listing 1. Definicja @Todo
package pl.koziolekweb.blog.development.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Todo {
public String description();
}
Pierwsze zaskoczenie. Otóż wykorzystamy zakres CLASS po to by adnotacja nie była dostępna w trakcie uruchomienia. Istotne jest by można było przetwarzać informacje na etapie kompilacji. Później nie powinny być one dostępne dla klienta. Zresztą po co mu one?
Projekt testowy
Tworzymy nowy projekt i dodajemy do niego jako zależność projekt z adnotacjami. Jest to o tyle ważne, że jeżeli używamy mavena to trzeba w pierwszym projekcie wyłączyć przetwarzanie adnotacji. Dlaczego? O tym innym razem. Tworzymy sobie kilka metod i dodajemy do nich nasze adnotacje…
Procesor właściwy
Procesor adnotacji jest uruchamiany jako serwis SE, czyli w katalogu META-INF/services tworzymy plik javax.annotation.processing.Processor i dodajemy w nim linijkę z informacją o naszej klasie procesora.
Sama klasa jest prosta (w tej wersji!):
Listing 2. Implementacja DevelopmentAnnotationProcessor
package pl.koziolekweb.blog.development;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
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.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import pl.koziolekweb.blog.development.annotations.Todo;
@SupportedAnnotationTypes("pl.koziolekweb.blog.development.annotations.Todo")
@SupportedSourceVersion(SourceVersion.RELEASE_5)
public class DevelopmentAnnotationProcessor extends AbstractProcessor {
private Map<name list="">> markedElements;
private boolean justRun = false;
public DevelopmentAnnotationProcessor() {
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
markedElements = new HashMap<name list="">>();
}
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (justRun)
return true;
justRun = true;
Set extends Element> todoElements = roundEnv.getElementsAnnotatedWith(Todo.class);
for (Element element : todoElements) {
if (element.getKind() != ElementKind.METHOD) {
System.out.println("[WARN] Oznaczony element to nie metodą");
}
TypeElement ownerClass = (TypeElement) element.getEnclosingElement();
Name qualifiedName = ownerClass.getQualifiedName();
addElementToReport(element, qualifiedName);
}
produceReport();
return true;
}
private void produceReport() {
Set<entry list="">>> markedElementsToReport = markedElements.entrySet();
if (markedElementsToReport.size() > 0) {
for (Entry<name list="">> entry : markedElementsToReport) {
System.out.println(entry.getKey().toString());
for (Element e : entry.getValue()) {
System.out.println("\t" + e.toString() + " TODO: " + e.getAnnotation(Todo.class).description());
}
}
}
}
private void addElementToReport(Element element, Name qualifiedName) {
if (markedElements.containsKey(qualifiedName)) {
List<element> classElements = markedElements.get(qualifiedName);
classElements.add(element);
} else {
List<element> classElements = new LinkedList<element>();
classElements.add(element);
markedElements.put(qualifiedName, classElements);
}
}
}
</element></element></element></name></entry></name></name>
Prawda, że proste?