Annotation Processor Klassen erstellen lassen

angryBird

Grünschnabel
Hallo,

ich will mithilfe von APT während der Compile-Time eine neue Klasse im Projekt-Ordner hinzufügen.

Es soll folgendermaßen funktionieren:
Ich annotiere bestimmte Klassen und Felder mit selbst erzeugten Annotation z.B. @Test und @NoTest. Jetzt soll mir der AnnotationProcessor die annotierten Klassen herausfiltern und für alle @Test annotierten Klassen Tests generieren. Also soll während der Compile-Time innerhalb des Processor Code generiert werden und alles als .java-Datei in einen bestimmten Projektpfad "gepumpt" werden.
Bisher habe ich versucht den AnnotationProcessor zu benutzen um mir per FileOutputStream aus dem JAR heraus eine .txt-Datei aufs Dateisystem zu schreiben. Das klappt soweit.
Nur halt in einen bestimmten Projektpfad klappt es bei mir nicht.

Daher die Frage :

Ist es möglich mit APT aus einem JAR heraus Dateien in einen bestimmten Projektpfad zu schreiben? :confused:

Gruß angryBird
 
Zuletzt bearbeitet:
Hallo,

dazu gibts IMHO mehrere Möglichkeiten.

Z.B.:
Bei javac kannst du über -s:
-s <directory> Specify where to place generated source files
den Zielort für die generierten Quelldateien mitgeben. in deinem AnnotationProcessor kannst du nun relativ zu diesem Zielort Dateien erzeugen:
Java:
...
FileObject res = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", "generated_tests.txt");

Hier mal ein größeres Beispiel:

Unser Annotation Processor, der die Annotation @Tested und @NotTested kennt (@Test & Co ist von JUnit schon mit anderer Semantik verwendet...) generiert für jede damit Annotierte Klasse entsprechende JUnit Test-Methoden. Methoden für die man keine Test-Methode generieren möchte kann man mit @NotTested ausklammern.

Java:
package de.tutorials.testing;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
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.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({ 
		"de.tutorials.testing.Testing.Tested",
		"de.tutorials.testing.Testing.NotTested" })
public class Testing extends AbstractProcessor {

	@Inherited
	@Documented
	@Retention(RetentionPolicy.RUNTIME)
	@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE })
	public static @interface Tested {
	}

	@Inherited
	@Documented
	@Retention(RetentionPolicy.RUNTIME)
	@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE })
	public static @interface NotTested {
	}

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

		for (Element element : roundEnv.getElementsAnnotatedWith(Tested.class)) {
			switch (element.asType().getKind()) {
			case DECLARED:
				processingEnv.getMessager().printMessage(Kind.NOTE, "generateTestCase for " + element, element);
				generateTestCase(element);
				break;
			default:
				;
			}
		}

		return true;
	}

	private void generateTestCase(Element element) {
		TypeElement clazz = (TypeElement) element;
		List<? extends Element> allMembers = processingEnv.getElementUtils().getAllMembers(clazz);

		try {
			String classNameSuffix = "Test";
			JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(clazz.getQualifiedName() + classNameSuffix, clazz);
			
			
			FileObject res = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", "generated_tests.txt");
			

			try (PrintWriter sourceWriter = new PrintWriter(sourceFile.openWriter());
				 PrintWriter resWriter =	new PrintWriter(res.openWriter())) {

				sourceWriter.println(processingEnv.getElementUtils().getPackageOf(element).toString()+ ";");

				sourceWriter.println("import org.junit.Test;");
				sourceWriter.println("import static org.junit.Assert.*;");
				sourceWriter.println("import javax.annotation.Generated;");

				sourceWriter.println("@Generated(\"" + getClass().getName()+ "\")");

				sourceWriter.println("public class " + clazz.getSimpleName()+ classNameSuffix + " {");

				for (Element member : allMembers) {
					switch (member.getKind()) {
					case METHOD:
						if (skipTestMethodGeneration(clazz, member)) {
							continue;
						}

						String qualifiedTestMethodName = generateTestMethod(clazz, member, sourceWriter);
						
						resWriter.println(qualifiedTestMethodName);
						
						break;
					default:
						break;
					}
				}

				sourceWriter.println("}");
			}

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	private boolean skipTestMethodGeneration(Element clazz, Element member) {
		boolean hasNotTestedAnnotation = member.getAnnotation(NotTested.class) != null;
		boolean sameType = processingEnv.getTypeUtils().isSameType(
				member.getEnclosingElement().asType(), clazz.asType());
		return hasNotTestedAnnotation || !sameType;
	}

	private String generateTestMethod(TypeElement clazz, Element member,
			PrintWriter sourceWriter) {

		String testMethodName = member.getSimpleName().charAt(0) == '<' /*
																		 * <init>
																		 */? "constructor"
				: member.getSimpleName().toString();

		sourceWriter.println("    @Test ");
		sourceWriter.println("    public void " + testMethodName + "() {");
		sourceWriter.println("        fail(\"test: " + testMethodName + " not implemented yet\");");
		sourceWriter.println("    }");
		
		return clazz.getQualifiedName() + "." + testMethodName;

	}

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);

	}
}


Unsere Beispiel-Klasse Service
Java:
package de.tutorials;

import de.tutorials.testing.Testing.NotTested;
import de.tutorials.testing.Testing.Tested;

@Tested
public class Service {

	public Service() {
	}

	public void operation1() {
	}

	@NotTested
	void operation2() {
	}
 
	public void operation3() {
	}
}

und hier die generierten Dateien:

ServiceTest.java
Java:
package de.tutorials;
import org.junit.Test;
import static org.junit.Assert.*;
import javax.annotation.Generated;
@Generated("de.tutorials.testing.Testing")
public class ServiceTest {
    @Test 
    public void operation1() {
        fail("test: operation1 not implemented yet");
    }
    @Test 
    public void operation3() {
        fail("test: operation3 not implemented yet");
    }
}

und generated_tests.txt:
Code:
de.tutorials.Service.operation1
de.tutorials.Service.operation3

Ich würde dir empfehlen den Code nicht über Strings zu generieren sondern statt dessen eine Template Engine wie Velocity, Freemarker oder StringTemplate zu verwenden. Außerdem sollte man darauf achten, vom Entwickler geänderten Code nicht zu überschreiben.

Diese Art von Annotation Prozessoren lassen sich auch wunderbar in IDE's integrieren. Hier mal ein Beispiel wie man einen eigenen AnnotationProcessor in die Eclipse IDE integrieren kann:
http://www.tutorials.de/java/367287...n-und-anzeigen-mit-annotation-processors.html

Wenn man eigene AnnotationProcessors entwickelt muss man die auch vernünftig Debuggen können.
Hier ein Beispiel zum Debugging von eigenen AnnotationProcessors:
http://www.tutorials.de/java/367200-annotation-target-annotationprocessor-debug.html

Gruß Tom
 
Vielen Dank für die ausführliche Antwort!
Leider hat das mit der relativen Pfadangabe nicht geklappt.
Ich habe in eclipse -s als processor option angegeben, oder war das falsch?
Also unter Properties -> Java Compiler -> Annotation Processing und da gibts eine Tabelle "processor options" dort habe ich -s als key und src/de/annotation/ als value angegeben. Dies brachte nicht den gewünschten Erfolg!

Gruß angryBird
 
Zurück