Statische Methode "vererben" - Zwang durch annotation processor

adalbert

Mitglied
Hallo zusammen!

Ich möchte für eine Anwendung ein einfaches Plugin-System realisieren. Die Applikation durchsucht nach dem Start ein definiertes Unterverzeichniss nach *.jar Dateien, öffnet diese und sucht via Reflection nach allen nicht abstrakten Klassen die von der abstrakten Klasse PluginFactory erben. Von diesen Klassen erwarte ich eine parameterlose statische öffentliche Methode createFactory, die mir eine Factory für dieses jeweilige Plugin liefert.
Soweit so gut, das funktioniert bisher einwandfrei. Mich stört an dieser Lösung nur, dass es (scheinbar) keine Möglichkeit gibt die statische Methode zu erzwingen, so dass es zu einem Compilerfehler o.ä. kommt wenn eine entsprechende Klasse nicht eine eigene Version der factory-Methode implementiert. Einfach gesagt
[JAVA=1337]
public abstract class PluginFactory {
public abstract static PluginFactory createFactory() ;
}
[/code]
wäre genau das was ich gerne hätte. Leider steht das ja etwas im wiederspruch mit OO-Denke und dem was Java bietet.

Meine Idee war nun eine annotation zu erstellen, die von einem eigenen annotation processor ausgewertet wird. Der annotation gebe ich den Namen der statischen Factory Methode mit. Annotiert durch @Inherits wird die Annotation ja auch an die ableitungen weiter vererbt.
Der annotation processor muss nun alle gekennzeichneten Klassen durchlaufen und prüfen ob diese eine statische öffentliche Methode des gesetzten Namens imlementiert.

Hier die annotation:
Java:
package test;

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;

@Inherited
@Documented
@Retention( RetentionPolicy.RUNTIME)
@Target( ElementType.TYPE )
public @interface StaticInherit {
	public String value();
}

und hier das wichtigste aus dem annotation Processor

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

//		for (Element element : roundEnv.getRootElements()) {
//			checkClass(element);
//			
//		}
		
		for (TypeElement type : annotations) {
			for (Element element : roundEnv.getElementsAnnotatedWith(type)) {
				checkClass(element);
			}
		}

		return true;
	}
	
	private void checkClass(Element element) {
		boolean found = false;
		this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "found occurence: " + element.toString(), element);
		
		StaticInherit inherit = element.getAnnotation(StaticInherit.class);
		String name = inherit.value();
		
		if (name == null || "".equals(name))
			this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "no value: " + element.toString(), element);
		else
			this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "value: " + name + " " + element.toString(), element);

		for (ExecutableElement execelement : ElementFilter.methodsIn(element.getEnclosedElements())) {
			if (execelement.getSimpleName().toString().equals(name)) {
				if (execelement.getModifiers().contains(Modifier.PUBLIC)
						&& execelement.getModifiers().contains(Modifier.STATIC)) {
					found = true;
					this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "method accepted: " + execelement.toString() + " " 
							+ System.currentTimeMillis(), execelement);
					break;
				}
			}
		}

		if (!found) {
			this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "public static method called " + name + "() not implemented!", element);
		} else {
			this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "check not performed!", element);
		}
	}

Der Code oben ist etwas abgewandelt um den Fehler leichter zu finden, also die prüfung auf abstract in der class und den Rückgabetyp der Methode habe ich mal aussen vorgelassen.

Der Code wirft ja viele Warnings, welche mir eclipse auch schön brav auswirft.

Auf folgende Beispielklassen wird das ganze nun angewandt
Java:
package test.banane;

import test.StaticInherit;

@StaticInherit("createFactory")
public abstract class SuperClass {
	public static void createFactory() {
		
	}
}
Java:
package test.banane;

public class SubClass extends SuperClass{

}

Die Warnings die der Compiler wirft sind im anhang zu finden.
Die erste Warnung kann ignoriert werden, da der Check für die Classe SuperClass ja ausgeführt wurde.
Die beiden nächsten sind interessanter: found occurence für SuperClass und SubClass. Die vererbung der annotation hat also geklappt.
Die 4te Warnung method accepted ist auch sehr schön, da die gesuchte Methode in der Klasse SuperClass gefunden wurde.
Die 5te Warnung gibt den Namen der gesuchten methode an, der passt auch.

Allerdings wird scheinbar in der Klasse SubClass keinerlei Prüfung durchgeführt, obwohl die Klasse vom Annotation processor gefunden wurde.


Ich hab jetzt etliche Stunden google nach diesem Thema gelöchert aber nichts brauchbares gefunden. Hat von euch jemand eine Idee was falsch sein könnte, oder wie mein Problem gelöst werden könnte?

(Und bitte keine Belehrungen darüber, dass es mehr als einen Grund gibt, warum statische methoden nicht vererbbar sind. Sowohl die Tatsache als auch die Gründe sind mir hinreichend bekannt. 🙂 )


Gruß
Adalbert
 
Wo ist der Unterschied zwischen createFactory() und einem parameterlosen Konstruktor? Angenommen, du hast folgendes:

Java:
public interface PluginFactory {
    //entsprechende Methoden
}

public MyPluginFactory implements PluginFactory {
   //Methoden-Implementierungen
}

Dann kannst du per Reflection MyPluginFactory finden, indem du nach dem Interface PluginFactory suchst, und mit Class.forName("MyPluginFactory").newInstance eine Factory erzeugen. Leider kann in Java ohne Reflection die Existenz eines öffentlichen argumentlosen Konstruktors nicht garantiert werden. Es gibt Lösungen, aber die sind nicht hübsch, z.B. [JavaSpecialists 167] - Annotation Processing Tool
 
Ich würde für die Klassensuche Tools wie Javassist oder ähnliches nutzen um die Klassen (zur Prüfung nach dem Interface) nicht echt in den Classloader laden zu müssen.

Alternative ist einen neuen Classloader instanzieren, Klassen mit diesen Classloader laden, Interface prüfen, CanonicalPath der Klasse speichern, am Ende den neuen Classloader wegwerfen und alle eventuellen Referenzen auf die geholten Klassen löschen.

Dann mit der Liste von CanonicalPaths und einem sauberen Classloader die Klassen erneut laden und nutzen.
 
Hallo Landei!

Der unterschied ist natürlich in diesem Falle minimal, beide Lösungen sind gleichwertig.
Das Problem: Die existenz des Parameterlosen Konstruktors bzw. der statischen Methode wird via Reflection erst zur Laufzeit geprüft. Ich möchte aber schon eine Prüfung vom Compiler erreichen.

Ich möchte einfach rausfinden ob es in diesem Fall eine Alternative zur Konvention gibt.
 
@Noctarius: Danke für die Ideen für die Klassen suche. Meine variante scheint mir etwas langsam zu sein, auch wenn sie funktioniert. Bei Zeit werde ich die mal einbauen.

@Landei: Jap, die Seite kenne ich. Davon inspiriert habe ich meine Variante mit der statischen Methode erstellt. Allerdings funktioniert der Source aus dem Quellcode nicht. Ich habe ihn 1:1 kopiert und getestet, es wird jedoch keine Compilerwarnung erzeugt.
 
Mit einem Maven- oder Ant-Plugin lässt sich auch eine Überprüfung erreichen aber eben nur explizit nicht vom Compiler selbst.

Zur Class-Suche:
Das Problem ohne eigenen Classloader für die Suche der Klasse ist:
- Alle Klassen bleiben im Speicher (sie wurden ja geladen und werden erst entfernt wenn der Classloader weg ist und keine Referenzen mehr bestehen)
- Eventuelle Sicherheitsfeatures wie Classloaderabtrennung machen später Probleme weil die Class schon von einem höheren Classloader geladen wurde
 
Maven kann ich für dieses Projekt nicht benutzen, ant jedoch wird bereits mit genutzt.
Wie könnte eine Lösung mit ant funktionieren? Mir fällt nur ein mit ant den APT aufzurufen, welcher dann aber das gleiche Problem wie der annotation processor haben dürfte.
 
Danke euch beiden für die schnellen Antworten!

Ich werd mich morgen dann mit ant auseinander setzen und versuchen diese Prüfung zu realisieren.

Ob jetzt statische Methode oder Default-Konstruktor genutzt wird ist mir zemlich egal (zumal die statische Methode meist eh nur den (privaten) Default-Konstruktor aufruft).

Aber jetzt hau ich mich erstmal aufs Ohr 🙂
 
Es ist doch möglich in Klassen über Annotations einen default Constructor (oder was auch immer) durch den Compiler zu erzwingen.

So sieht jetzt die process-methode des zugehörigen processors aus (falls es jemand interessieren sollte):
Java:
@Override
	public boolean process(Set<? extends TypeElement> annotations,
			RoundEnvironment roundEnv) {

		for (TypeElement type : annotations) {
			for (Element element : roundEnv.getElementsAnnotatedWith(type)) {
				checkClass(element);
			}
		}

		return true;
	}

	private void checkClass(Element element) {
		boolean found = false;

		if (element.getModifiers().contains(Modifier.ABSTRACT)) {
			found = true;
		} else {
			for (Element subelement : element.getEnclosedElements()) {
				if (subelement.getKind() == ElementKind.CONSTRUCTOR) {
					if (subelement.getModifiers().contains(Modifier.PUBLIC)) {
						
						TypeMirror mirror = subelement.asType();
						if (mirror.accept(noArgsVisitor, null)) {
							found = true;
							break;
						}
					}
				}
			}
		}

		if (!found) {
			this.processingEnv.getMessager().printMessage(
					Diagnostic.Kind.ERROR,
					"Public no-args constructor not implemented.", element);
		}
	}

	private static final TypeVisitor<Boolean, Void> noArgsVisitor = new SimpleTypeVisitor6<Boolean, Void>() {
		public Boolean visitExecutable(ExecutableType t, Void v) {
			return t.getParameterTypes().isEmpty();
		}
	};

Das Beispiel aus dem Link war also doch fast lauffähig 🙂
 

Zurück
Oben