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> markedElements; private boolean justRun = false; public DevelopmentAnnotationProcessor() { } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); markedElements = new HashMap >(); } 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 >> markedElementsToReport = markedElements.entrySet(); if (markedElementsToReport.size() > 0) { for (Entry > 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 classElements = markedElements.get(qualifiedName); classElements.add(element); } else { List classElements = new LinkedList (); classElements.add(element); markedElements.put(qualifiedName, classElements); } } }
Prawda, że proste?
Przyznam, że rozwiązanie jest interesujące. Tylko nie załapałem jaką ma ono przewagę nad standardowym (tj znacznikami TODO w komentarzach). Wtedy raport jest dostępny przez cały czas w oknie IDE. W rozwiązaniu z adnotacjami pojawia się dopiero po kompilacji, co jest IMHO mniej wygodne.
PS: w kodzie najwidoczniej coś się popsuło w definicjach generyków.
@trecio, nie samym IDE człowiek żyje, ale też np. narzędziami do raportowania. To rozwiązanie jest niezłe jeżeli używasz np. serwera ciągłej integracji i chcesz mieć możliwość wygenerowania sobie informacji o ilości zidentyfikowanych bugów. Zresztą sam procesor jest bardzo uproszczony jeżeli by go podrasować to można by było zrobić z tego calkiem niezłe narzędzie do weryfikowania postępów prac.
Co do generyków to jest to bug wtyczki do podświetlania kodu 🙁