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> markedElements;

	private boolean justRun = false;

	public DevelopmentAnnotationProcessor() {

	}

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);
		markedElements = new HashMap>();
	}

	public boolean process(Set annotations, RoundEnvironment roundEnv) {
		if (justRun)
			return true;
		justRun = true;
		Set 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?

2 myśli na temat “Raportowanie z kodu za pomocą adnotacji w czasie kompilacji

  1. 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.

  2. @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 🙁

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax