Java Plugin schreiben

BlubBlub

Mitglied
Hi,

ich versuche ein Programm "FunnyFunctions" zu schreiben, welches erlaubt weitere Funktionen zu integrieren.

Ich habe also, das Hauptprogramm "FunnyFunctions".
Zudem habe ich einen Ordner namens "External".
In den Ordner "External" soll ein Dritter eine ".class" Datei ablegen können,
deren Funktionalität beim starten des Hauptprogramms zur Verfügung steht.

Ich sage also, dass jede ".class" Datei ein bestimmtes Interface implementieren muss,
mit den Funktionen die dem Hauptprogramm später zur Verfügung stehen sollen.
Die neue Klasse wird dynamisch über einen URLClassLoader geladen.

Allerdings habe ich ein Problem. Sowohl das Hauptprogramm als auch die neue Klasse
müssen das Interface kennen. Somit muss man jedes mal von Hand den ClassPath setzen zu dem Pfad wo sich das Interface befindet. Dies ist jedoch recht mühselig.

Mein Ziel ist es, dass ein unerfahrener Benutzer sich von irgendwo her die neue Klasse in Form einer ".class" Datei holt oder runterlädt und diese lediglich in einen bestimmten Ordner, in diesem Fall in den Ordner "External" einfügt. Anschließend soll beim Ausführen des Hauptprogramm die neue Funktionalität zur Verfügung stehen.

Frage: Wie schaffe ich es ohne immer wieder den ClassPath selbst setzen zu müssen das Interface sowohl dem Hauptprogramm als auch der neuen Klasse bekannt zu machen?

Ich habe auch einen recht interessanten Artikel gefunden zum Erstellen von Java Plugins gefunden plugin-entwicklung-in-java/. Jedoch wird da immer der ClassPath von Hand gesetzt, was ich ja vermeiden will.
Damit diese beiden Klassen als Schnittstelle zwischen Plugin und Programm agieren können, müssen sowohl die Plugins, als auch die Anwendung selbst, diese beiden Interfaces kennen. Es bietet sich also an, ein eigenes PluginInterface.jar zu erstellen, welches aus dem PluginManager und dem Pluggable Interface besteht. Dieses JAR muss dann während der Entwicklung der Applikation und der Plugins als Classpath-Eintrag zur Verfügung stehen. Das Programm selbst benötigt die Schnittstelle natürlich auch beim Ausführen.
 
Ich weiß Zwar nicht die Lösung, bzw die genau Beschreibung.

Aber ist es nicht der falsche Ansatz mit .class zu arbeiten? Ich habe mit nem Kumpel zusammen eine Anwendung geschrieben, die das was du vorhast mit .jar Dateien macht.

Die jar enthält selber das Interface genau so wie die Hauptanwendung und muss nicht von Hand bekannt gemacht werden. und die jar lädst du locker aus einen Ordner in deine Anwendung.


Wenn du bei deinen Ansatz bleiben möchtest ist wohl Reflections das Stichwort (hoffe es ist richtig geschrieben)

Ich bin der Meinung man kann dort den Classpath auslesen und das setzen kann man sicher dann leicht automatisieren ;)
 
Aber du hast schon mal von der SuchFunktion gehört ? Dann hättest du meine beiden alten Threads zu diesem Thema eigentlich finden müssen

http://www.tutorials.de/java/357126-wieder-mal-java-und-plug-ins.html
http://www.tutorials.de/java/365360-applikation-updaten.html

Das ganz ist mitlerweile deutlich überaltert und ich habe mirlerweile bessere Methoden und Code entwickelt mit denen ich auch zur Zeit arbeite. Wenn du willst kann ich dir mal mein gesamtes PluginSystem zur verfügung stellen wie es zur Zeit. Es dürfte zumindest das machen was du willst.

Wo ich wakoz allerdings zustimmen muss : CLASS-Files sind hier der falsche Ansatz ... arbeite lieber mit JAR-Files ... das ist einfacher.

Was das Thema jetzt an sich angeht :

Das Plugin muss das Interface überhaupt nicht kennen ... nur der Compiler der das Plugin compiled. Und da reicht auch das CLASS-File aus deinem Projekt ... dafür braucht man keinen Source *zumindest nicht wenn eine ordentliche Doc bei liegt*.
Wer allerdings das Interface kennen muss ist der SystemClassLoader ... heißt nichts weiter als das das Interface beim start der VM bekannt sein sollte ... das erreichts du in dem du das CLASS-File einfach mit in das JAR legst welches gestartet wird. Dann kannst du über die gesamte VM-Instanz damit arbeiten.

Auch hast du bei deinem Ansatz übersehen das es nicht nur möglich sein muss vom Launcher die einzelnen Plugins zu laden und ihre Funktion anzustoßen ... es sollte auch möglich sein das ein Plugin mit dem Launcher und vielleicht sogar anderen Plugins kommunizieren kann. Der einfachste Weg dafür sind Reflections und eine halbwegs vernünftige Struktur.

Wie gesagt : wenn du aus den anderen Threads nicht ganz schlau wirst und/oder eine aktuelle Version haben möchtest stelle ich diese gerne zur Verfügung.
 
Ich habe nun schon in zahlreichen Foren geschrieben und mir etliche Websites angeschaut. Ich hab das Gefühl, dass ich nah an der Lösung bin, aber das letzte Stückchen krieg ich einfach nicht hin. Ich poste jetzt einfach mal meine bisherige Lösung. Um die Sache möglichst einfach zu halten, hab ich mal ein einfaches Programm geschrieben.

@Spike habe mich durch deine Links durchgelesen, teilweise steht da auch das drinne was ich versuche und teilweise sind da Sachen mit denen ich nicht ganz vorwerts komme in meiner Sache.

@wakoz das klingt recht interessant allerdings versteh ich das noch nicht recht in der Praxis umzusetzen

Was mir am meisten helfen würde, ist wenn ihr euch diesen Beispielcode anschaut und mir anhand dessen sagt was ich da genau zu ändern habe damit es funktioniert.

Kurz worums geht: Es wird ein java.lang.NoClassDefFoundError geschmissen.
Der Fehler liegt meiner Meinung daran, dass ich die Klasse aus einem anderen
Ordner versuche zu laden die ein Interface implementiert. Dieses Interface liegt
aber nicht in dem Ordner wo die zu ladende Klasse liegt.

Fehlermeldung:
java.lang.NoClassDefFoundError: ExternalFunction


Kurze Beschreibung:
Auf dem Desktop habe ich zwei Ordner:
Ordner: "One"
Ordner: "Two"

Diese haben folgenden Aufbau:

Code:
One
  |--------- interfaces
  |                 |------------- ExternalFunction.java
  |                 |------------- ExternalFunction.class
  |
  |--------- starter
                    |------------- Starter.java
                    |------------- Starter.class

Two
  |---------- MyExternalClass.java
  |---------- MyExternalClass.class



Hier der Code der 3 Java Klassen:
Code:
package starter;

import interfaces.ExternalFunction;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class Starter 
{
	public static void main(String[] args)
	{
		try 
		{			  
			URL urls = new URL("file:\\\\\\C:\\Users\\Ich\\Desktop\\Two\\");
			URL[] classpath = {urls};
			URLClassLoader urlClassLoader = new URLClassLoader(classpath);
			Class<?> clazz = urlClassLoader.loadClass("MyExternalClass");  //Hier wird der Fehler geschmissen
			Object object = clazz.newInstance();
			ExternalFunction externalFunction = (ExternalFunction)object;
			externalFunction.doSomething();
		}
		catch (MalformedURLException e)
		{
			e.printStackTrace();
		} 
		catch (ClassNotFoundException e)
		{
			e.printStackTrace();
		}
		catch (InstantiationException e)
		{
			e.printStackTrace();
		} 
		catch (IllegalAccessException e)
		{
			e.printStackTrace();
		}
	}
}

Code:
package interfaces;

public interface ExternalFunction 
{
    public String doSomething();
    public String getDescription();
}

Code:
public class MyExternalClass implements ExternalFunction
{
	public MyExternalClass(){}
	 
    @Override
    public String doSomething()
    {
        System.out.println("MyExternalClass doSomething");
        return null;
    }
 
    @Override
    public String getDescription() 
    {
        String description = "MyExternalClass Description";
        return description;
    }
}
 
Ui ... mal davon abgesehen das es hier JAVA-Tags gibt *was die Lesbarkeit extrem verbessert* ist deine Ordner-Struktur nich gerade die beste.
Auch hast du unsere Hinweise bezüglich CLASS vs JAR ignoriert.

Was der Fehler aussagt : das Interface ExternalFunction wurd nicht gefunden.
Möglich Ursachen : fehlerhafter ClassPath , fehlerhafte Überschreibung des ClassPath , fehlerhafte Ordner- oder Paketstruktur , fehlende Dateien ... und so weiter

Du siehst also : die Liste der möglichen Ursachen für dieses eindeutige Fehlermeldung ist lang ...

Es ist trotzdem möglich deinen Code ... genau so wie er ist ... zu verwenden -> JAR ...
Packe einfach alles in JAR-Files und dann sollte es gehen.
Deine Struktur sollte folgendermaßen aussehen :

Code:
Desktop
|-> One
| |-> One.jar
|   |-> interfaces
|   | |-> ExternalFunction.class
|   |-> starter
|   | |-> Starter.class
|   |-> META-INF
|     |-> MANIFEST.MF
|
|-> Two
  |-> Two.jar
    |-> MyExternalFunction.class
    |-> META-INF
      |-> MANIFEST.MF

Und dem URLClassLoader übergibst du dann folgenden Pfad
Code:
C:/Users/Ich/Desktop/Two/Two.jar

Das sollte dein Problem lösen *ich hab es jetzt nicht getestet*.

Was meine Threads angeht : es wäre schön zu wissen WAS genau du nicht verstehst / wobei genau du Probleme hast. Dann kann ich dir gerne weiterhelfen.
 
Zu dem Punkt, dass ich die ".class" Dateien in die beiden "jar" Dateien legen kann, komm ich ja erst gar nicht.
Code:
Desktop
|-> One
| |-> One.jar
|   |-> interfaces
|   | |-> ExternalFunction.class
|   |-> starter
|   | |-> Starter.class
|   |-> META-INF
|     |-> MANIFEST.MF
|
|-> Two
  |-> Two.jar
    |-> MyExternalFunction.class
    |-> META-INF
      |-> MANIFEST.MF

Das Problem ist ja, dass um überhaupt die "MyExternalFunction.class" Datei erzeugen zu können aus der unten angengenbe ".java" Datei, muss ich erst einen classpath zu "interfaces.ExternalFunction" setzen.

Sprich ich muss in der Windows Konsole folgende Eingabe machen:
"C:\Users\Ich\Desktop\Two> javac -classpath C:\Users\Ich\Desktop\One MyExternalClass.java"
Lasse ich den classpath weg so wird man es nicht schaffen die Datei zu kompilieren.
Der "NoClassDefFoundError" wird ja gerade dewegen geworfen, weil er ohne der -classpath Angabe das Interface nicht finden wird.

Ich bin ja nicht dagegen versperrt .jar Dateien zu verwenden. Ganz im Gegenteil ich begrüße das sogar. Nur um überhaupt eine sinnvolle .jar Datei zu erstellen, muss ich erstmal die einzelnen ".class" Dateien erzeugen, die ich dann im Anschluss gern in einer .jar zusammenfüge un dem Hauptprogramm zur Verfügung stelle.

Code:
import interfaces.ExternalFunction;

public class MyExternalClass implements ExternalFunction
{
	public MyExternalClass(){}
	 
    @Override
    public String doSomething()
    {
        System.out.println("MyExternalClass doSomething");
        return null;
    }
 
    @Override
    public String getDescription() 
    {
        String description = "MyExternalClass Description";
        return description;
    }
}



Vielleicht erkläre ich meine eigentlichen Absichten an einem strukturierterem Beispeil, wie es gern hätte, dass es zum Schluss aussieht:

Der Benutzer beschafft sich zunächst einmal das Programm. Dazu erhält er einen Ordner
"MyProgram" in dem alles nötige enthalten ist.

Code:
MyProgram (Ordner)
  |------------------ MainProgram.jar
  |                              |------------------- META-INF
  |                              |                           |----------- MANIFEST.MF
  |                              |
  |                              |------------------- starter
  |                              |                          |------------ Starter.class
  |                              |
  |                              |------------------- interfaces
  |                                                          |------------ ExternalFunction.class
  |
  |------------------ MyPlugins (Ordner)
  |                              |------------------ PluginsFromA.jar
  |                              |                               |------------- ExternalClass1FromA.class
  |                              |                               |------------- ExternalClass2FromA.class
  |                              |
  |                              |------------------ PluginsFormX.jar
  |                                                               |--------------ExternalClassFromX.class

Der erste Teil funktioniert ja problemlos. Sprich die Erzeugeugung der MainProgramm.jar die zugleich eine ausführbare .jar Datei darstellt und die Erzeugung der in dieser .jar Datei enthaltenen .class Datei funktioniert.

Was allerdings "Probleme" macht ist, das erstellen der Plugin ".class" Dateien.
Momentan sieht meine Lösung so aus:

Ich habe eine Api.jar Datei erstellt, die so aussieht:
Code:
Api.jar
  |---------- META-INF
  |              |--------------MANIFEST.MF
  |
  |--------- interfaces
                 |-------------- MyExternalFunction.class

Jemand der ein Plugin entwickelt muss das Interface implementieren, welches
durch MyExternalFunction beschrieben ist.
Momentan muss ich ihm diese Api.jar mitgeben, damit er seine Klasse kompilieren kann, und damit ein Objekt der Klasse später von meinem Hauptprogramm dynamisch erzeugt werden kann. Dabei muss die Ordnerstruktur in der Api.jar auch diese Aufbau haben interfaces.MyExternalFunction.class. Der Grund liegt darin, dass meine Hauptprogramm,
selbst auch das Interface implementiert und die Interface Klasse so importiert wird.
Ich habe im Hauptbrogramm selbst das Interface in abgelegt. Somit verhinder ich, dass ein Dritter das Interface verändert.

Wünschenswert wäre es um möglichst viel Arbeit dem plugin Entwickler zu entnehmen, dass dieser, diese Api.jar nicht bräuchte. Doch ich hab momentan die Vermutung, dass es nicht anders umsetzbar ist.

Beim Kompilieren der Klasse MUSS das Interface dem Compiler bekannt sein. In der Regel verpackt man sowas in nen meineApp-api.jar file, dass man dann den Entwicklern mit ausliefert damit die ihre Plugins kompilieren können.
In dem meineApp-api.jar File würde dann unter anderem dein Interface liegen.
Anscheinend ist das einfach der Weg den ein Plugin Entwickler gehen muss.
Diese bisherige Lösung funktioniert ja auch, nur hätte ich dem Entwickler die Arbeit mit dem Classpath setzen beim Compilieren noch irgendwie entehmen können, wäre dies natürlich noch idealer gewesen.


Es wurde mehrfach versucht TO klarzumachen das das Interface dem SystemClassLoader bekannt sein muss ... und das der einfachste Weg um das zu erreichen das Zusammenpacken von Loader und Interface in einem Jar ist ... jedoch wurden diese Hinweise bis jetzt ignoriert.

Ich kann dem nicht ganz folgen, wie ich das jetzt genau auf meine Problematik übertragen soll.
 
Ok .. ich erklär es mal so ...

Du hast ein Interface welches für eine Art Plugin-System verwendet wird ... soweit ist uns das allen bis hier hin klar.

Was brauchst du nun um ein "Plugin" überhaupt compilen zu können ?

In der Regel eine gute Doc oder andere Dinge die dem Entwickler sagen wie das Interface funktioniert ... und das Interface selbst.
Es führt kein weg dran vorbei : wenn jemand ein Plugin für deine App entwickeln will braucht er zwangsläufig das Interface ... ob nun als vor-compilete Class oder als Source ... das ist dabei egal.
Was allerdings unter allen Umständen erfüllt sein muss : der Compiler muss das Interface kennen. Und er muss es auch an einem Ort finden an dem er es erwartet ... relativ zum CLASSPATH.

Ich weis nicht wie lange du schon mit Java arbeitest ... aber scheinbar hast du wenig Erfahrung im Umgang mit der CLASSPATH-System-Variable.

Beispiel : ich möchte z.B. ein Plugin X für deine API entwickeln ... und brauche dazu das Interface. Um dich nun nicht persönlich darum zu bitten nehme ich mir einfach die compilte CLASS aus dem Jar deiner Anwedung ... wo es ja auch sein MUSS damit deine App später zur Runtime dieses Interface kennt.
Da ich nun aber faul bin und nich jedes Mal einen kilometerlangen CP dem Compiler übergeben will um alle meine Libs einzubinden habe ich in meinem System eine Super-Globale eingerichtet ... CLASSPATH.
In dieser System-Umgebungsvariable *wie es auch heißt* stehen nun alle Pfad in denen irgendwelche Libs mit relativen Ordnerstrukturen und der gleichen liegen ...
Also mache ich es mir einfach : ich habe einen speziellen Ordner in diesem CP der einfach nur EXT heißt. In diesem liegen so ziemlich alle Libs die ich jemals für irgendwelche Codes brauchte. Also füge ich dein Interface entsprechend deiner Package-Struktur in EXT/interfaces/ExternalFunction.class ein ... und mache dieses Interface damit meinem gesamten System bekannt ... da jedes Java-Programm auf die Super-Globale CLASSPATH zugreifen und darin nach dem entsprechenden Interface suchen kann.

Viel anderst funktioniert mein PluginSystem auch nicht ... und du hättest genau die selben Probleme wenn du versuchen würdest ein Plugin für meine App zu schreiben ... außer das meins wesentlich ausgereifter und halbwegs gut dokumentiert ist.

WIE genau du jetzt für dein Betriebssystem diese Super-Globale einrichtest fragst du bitte GooGLe.

Und zu deinen Bedenken anderer Programmierer gegenüber : so oder so ähnlich machen es auch alle anderen "guten" Java-Programmierer. Die meisten erstellen einfach ein neues Projekt in ihrer IDE *hier vorwiegend Eclipse und NetBeans* und fügen dein App.jar wo sich ja dein Interface drin befindet einfach als Resource-Path hinzu und haben so Zugriff auf das Interface.
Jemand der das nicht drauf hat und sich desshalb mir sehr langen Compiler-Aufrufen rumqälen muss sollte vielleicht noch mal in die ersten paar Kapitel der JavaInsel gucken.

//EDIT : ich wäre dir dankbar wenn du das Quote vom java-forum.org auch als solches kennzeichnen würdest.
 
Alles klar. Danke für die ausführliche Antwort. Ich denke jetzt habe ich eine gewisse Ordnung in meinen Gedankengängen herstellen können, so dass ich jetzt den einen oder anderen Zusammenhang verstehe. Puh war gar nicht mal so einfach, aber ich denke allmählich krieg ich den Dreh raus. Falls das Angebot mit dem Verschicken deines Plugin Programms noch besteht würde ich mir das gerne mal anschauen, nur um mir eventuell ein paar Ideen zu verschaffen, wie man solche Sachen noch optimieren kann.
 
Alles klar ... kommt sofort per PN *muss es nur ein wenig *Aufräumen**.

ps : makiere das Thema bitte als ERLEDIGT wenn deine Fragen beantwortet wurden.

//EDIT
Ich lege noch eine Beispielimplementierung dazu ... daher könnte das einen kleinen Moment länger dauern.
 
So ... nach dem das ganze nun doch einige Stunden gedauert hat bin ich auch schon fertig.
Der Grund für die verzögerung war das ich mein ganzes Plugin-System noch mal neu schreiben und dokumentieren durfte *auch wenn diese zur Zeit nicht vollständig ist*.
Auch habe ich 3 Test-Plugins beigelegt die die Arbeitsweise verdeutlichen sollen.

Was ist zu beachten :

SICHERHEITSHINWEIS : Dieses Plugin-System dient lediglich zu Demonstrationszwecken und ist nicht für den produktiven Einsatz gedacht !

Weiterhin gibt es bei mir einige Besonderheiten :

1) In jedem Plugin-Jar muss sich eine Datei befinden welche auf "Plugin.rsf" endet. Der Inhalt dieser Datei ist der vollständige Klassenpfad und Klassenname. Für das beigelegte Test-Plugin A ist dies also z.B.
Code:
de.tutorials.spikee.samplePluginA.SamplePluginA
Nach dieser Datei sucht meine Implementierung des PluginLoader und versucht aus dieser den Klassennamen zu lesen.

2) Ich habe der Einfachheit halber sämtliche Exceptions mit "throws Exception" weitergereicht ... dies sollte bei einem produktiven Einsatz DRINGEND angepasst werden.

3) Es liegt eine ausführlich Doc bei. Bitte diese verwenden ! Wer daraus nicht schlau wird : HIER ! fragen ... ich werde KEINE Fragen per PN beantworten !

Ein ausführliches Tutorial mit weitreichenden Erklärungen wird in den nächsten Tagen folgen.

Worauf noch zu achten ist : Die Kontrolle der Plugins liegt beim Entwickler ... also hat auch dieser dafür Sorgen zu tragen das sich einzelne Plugins nicht gegenseitig stören und in DeadLocks verfangen.
Dies gilt insbesondere für :
-mehrere Plugins die von EINEM Stream lesen / in EINEN Stream schreiben
-Plugins die gegenseitig auf irgendwelche Rückgabewerte warten
-Threads die sich gegenseitig blockieren könnten

Auch finde ich deinen Ansatz "ich möchte es anderen Entwicklern ermöglichen Plugins für meien App zu schreiben" sehr gewagt. Es ist ein leichtes für einen guten Java-Programmierer mit Reflections Schadcode einzuschleusen , absichtlich Deadlocks zu verursachen und damit Programmabstürze oder an sensible Daten herankommen.


Da gesamte Paket wurde mit Java7u1 pre-compiled ! Wer Probleme beim Ausführen auf älteren VMs hat sollte die Source-Files re-compilen.

Die Methode
Java:
private void setPath()
ist keines Falls universaltauglich ! Getestet wurde eine fehlerfreie Funktion nur auf folgenden Systemen

-Win XP Pro SP2 DEUTSCH Java7
-Win Vista Home Premium x86 SP2 DEUTSCH Java7
-Win 7 Ultimate x64 SP1 DEUTSCH Java7
-OpenSuSE 11.3 KDE DEUTSCH Java7

Für Fehlfunktionen auf anderen Systemen wird KEINE Haftung übernommen !
 

Anhänge

  • Plugin_System.zip
    110,8 KB · Aufrufe: 50
Zurück