Einfacher Plugin Mechanismus mit dem ServiceLoader API

Thomas Darimont

Erfahrenes Mitglied
Hallo,

hier mal wieder ein kleines einfaches Beispiel für einen flexiblen Erweiterungsmechanismus auf Basis des ServiceLoader APIs (http://download.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html).

Dazu definieren wir uns ein Marker-Interface womit wir "Erweiterungsklassen" in unserer Anwendung markieren könnnen.

Unser Marker-Interface Extension:
Java:
package de.tutorials.extensibility;

public interface Extension {
}

Unser Interface ExtensionRegistry:
Java:
package de.tutorials.extensibility;

import java.util.List;


public interface ExtensionRegistry {
	<TExtension extends Extension> List<TExtension> getExtensions(Class<TExtension> extensionClass);
}

Hier unsere Standard ExtensionRegistry Implementierung ServiceLoaderExtensionRegistry :
Java:
package de.tutorials.extensibility;

import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;

public class ServiceLoaderExtensionRegistry implements ExtensionRegistry{

	private final static Logger logger = Logger.getLogger(ServiceLoaderExtensionRegistry.class.getName());
	
	private ClassLoader extensionClassLoader;

	private AtomicBoolean initialized = new AtomicBoolean();
	
	protected String extensionLookupPath = System.getProperty("de.tutorials.extensibility.extension.lookupPath", "./ext/");
	
	protected String extensionFileExtension = ".ext.jar";
	
	protected FilenameFilter extensionJarFilter = createExtensionJarFilter();
	
	
	public void init() {
		this.extensionClassLoader = createExtensionClassLoader(lookupExtensionUrls());
		this.initialized.set(true);
	}
	
	@Override
	public <TExtension extends Extension> List<TExtension> getExtensions(Class<TExtension> extensionClass){
		
		if(!this.initialized.get()){
			init();
		}
		
		ServiceLoader<TExtension> extensionLoader = ServiceLoader.load(extensionClass, getExtensionClassLoader());
		
		List<TExtension> extensions = new ArrayList<TExtension>();
		for(Iterator<TExtension> iter = extensionLoader.iterator();iter.hasNext();){
			TExtension extension = iter.next();
			logger.info(String.format("Found extension for %s: %s from URL: %s",extensionClass.getName(), extension, extension.getClass().getProtectionDomain().getCodeSource().getLocation()));
			extensions.add(extension);
		}
		
		if(extensions.isEmpty()){
			logger.info(String.format("No extensions were found for %s", extensionClass.getName()));
		}
		
		return extensions;
	}

	private URLClassLoader createExtensionClassLoader(List<URL> extensionJars) {
		return new URLClassLoader(extensionJars.toArray(new URL[extensionJars.size()]));
	}

	private List<URL> lookupExtensionUrls() {
		File extensionsFolder = new File(getExtensionLookupPath());
		
		logger.info("Using extension folder: " + extensionsFolder.getAbsolutePath());
		
		List<URL> extensionUrlList = new ArrayList<URL>();
		File[] extensionJars = extensionsFolder.listFiles(getExtensionJarFilter());
		
		if(extensionJars == null || extensionJars.length == 0){
			logger.info("Found 0 extension jars");	
		}else{
			logger.info(String.format("Found %s extensions",extensionJars.length));
			for(File extensionJar : extensionJars){
				try {
					URL extensionJarUrl = extensionJar.toURI().toURL();
					logger.info("Found extension jar: " + extensionJarUrl);
					extensionUrlList.add(extensionJarUrl);
				} catch (MalformedURLException e) {
					e.printStackTrace();
				}
			}
		}
		
		return extensionUrlList;
	}

	protected FilenameFilter createExtensionJarFilter() {
		return new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(extensionFileExtension);
			}
		};
	}

	public String getExtensionLookupPath() {
		return extensionLookupPath;
	}

	public void setExtensionLookupPath(String extensionLookupPath) {
		this.extensionLookupPath = extensionLookupPath;
	}

	public String getExtensionFileExtension() {
		return extensionFileExtension;
	}

	public void setExtensionFileExtension(String extensionFileExtension) {
		this.extensionFileExtension = extensionFileExtension;
	}

	public FilenameFilter getExtensionJarFilter() {
		return extensionJarFilter;
	}

	public void setExtensionJarFilter(FilenameFilter extensionJarFilter) {
		this.extensionJarFilter = extensionJarFilter;
	}

	public ClassLoader getExtensionClassLoader() {
		return extensionClassLoader;
	}

	public void setExtensionClassLoader(ClassLoader extensionClassLoader) {
		this.extensionClassLoader = extensionClassLoader;
	}
}


Unsere Anwendung:
Java:
package de.tutorials.app;

import de.tutorials.app.components.Component;
import de.tutorials.extensibility.ExtensionRegistry;
import de.tutorials.extensibility.ServiceLoaderExtensionRegistry;

public class Application {

	protected ExtensionRegistry extensionRegistry;

	public static void main(String[] args) {
		new Application().boot();
	}

	protected void boot() {
		init();
		start();
	}

	protected void init() {
		setExtensionRegistry(new ServiceLoaderExtensionRegistry());
	}

	protected void start() {
		for (Component component : getExtensionRegistry().getExtensions(Component.class)) {
			component.start();
		}
	}

	public ExtensionRegistry getExtensionRegistry() {
		return extensionRegistry;
	}

	public void setExtensionRegistry(ExtensionRegistry extensionRegistry) {
		this.extensionRegistry = extensionRegistry;
	}
}

In unserer Anwendung kann man Komponenten hinzufügen welche "gestartet" werden können.
Deshalb definieren wir ein interface Component das von unserem Extension-Marker Interface erbt:
Java:
package de.tutorials.app.components;

import de.tutorials.extensibility.Extension;

public interface Component extends Extension{
	void start();
}

Für das Beispiel definieren wir noch eine Abstrakte Klasse AbstractComponent
damit wir schnell viele kleine Component Implementierungen definieren können, die beim start() ihren Namen ausgeben:
Java:
package de.tutorials.app.components;

import java.util.logging.Logger;

public abstract class AbstractComponent implements Component {
	@Override
	public void start() {
		Logger.getLogger(getClass().getName()).info(
				"Started " + getClass().getSimpleName());
	}
}

Zusätzlich definieren wir eine StandardComponent, schließlich muss die Anwendung ja auch ein wenig Standard-Funktionalität mitbringen:
Java:
package de.tutorials.app.components;

public class StandardComponent extends AbstractComponent {
}

Damit diese Standard Implementierung vom ServiceLoader gefunden werden kann, müssen wir die Verzeichnisse META-INF/services/ im Classpath Root (im Eclipse zbsp. im src Folder) definieren. Dort erzeugen wir eine Datei mit dem Namen des Interfaces "de.tutorials.app.components.Component" (das ist die Konvention des ServiceLoaders).
In diese Datei schreiben wir nun den voll qualifizierten Klassennamen (fqcn) unserer Standardimplementierung:
Code:
de.tutorials.app.components.StandardComponent

Lassen wir nun unsere Anwendung laufen, so sehen wir folgende Ausgabe:
Code:
29.06.2011 00:08:19 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Using extension folder: C:\development\java\workspaces\2.6.1\de.tutorials.training.app\.\ext
29.06.2011 00:08:19 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Found 0 extension jars
29.06.2011 00:08:19 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.StandardComponent@b61d36b from URL: file:/C:/development/java/workspaces/2.6.1/de.tutorials.training.app/bin/
29.06.2011 00:08:19 de.tutorials.app.components.AbstractComponent start
INFO: Started StandardComponent

Wie man sieht wurde unsere StandardComponent gefunden, jedoch keine zusätztlichen Erweiterungen. Das ist auch richtig so, denn wir haben ja noch keine definiert.

Wir erstellen nun eine solche Erweiterung. Dazu erzeugen wir ein neues java-Projekt: de.tutorials.training.app.extension1 und erstellen dort folgende zwei Klassen:
Java:
package de.tutorials.app.components;

public class Component1A extends AbstractComponent {
}

und

Java:
package de.tutorials.app.components;

public class Component1B extends AbstractComponent {
}

wieder erzeugen wir eine Datei: /de.tutorials.training.app.extension1/src/META-INF/services/de.tutorials.app.components.Component ; nun mit folgendem Inhalt:
Code:
de.tutorials.app.components.Component1A
de.tutorials.app.components.Component1B

Diesmal möchten wir über unseren Erweiterungsmechanismus gleich zwei Component Implementierungen bereitstellen.

Nun erzeugen wir für unsere Anwendung (app.ext.jar als runnable jar!) und das Erweiterungs Projekt (extension1.ext.jar) jeweils ein jar.
Das das app.jar welches unsere Hauptanwendung enthält erzeugen wir zum test in ein Verzeichnis c:\temp\app
Dazu erstellen wir nun ein Verzeichnis c:\temp\app\ext. Dieses Verzeichnis wird unsere Erweiterungs-jars aufnehmen.
Ich habe die Erweiterungs-Jars durch die Endung .ext.jar explizit als solche gekennzeichnet.


Starten wir nun unsere Anwendung über die Konsole via:
java -jar app.jar
... so sehen wir folgende Ausgabe:
Code:
C:\temp\app>java -jar app.jar
29.06.2011 00:16:04 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Using extension folder: C:\temp\app\.\ext
29.06.2011 00:16:04 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Found 1 extensions
29.06.2011 00:16:04 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Found extension jar: file:/C:/temp/app/./ext/extension1.ext.jar
29.06.2011 00:16:04 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.StandardComponent@38503429 from URL: file:/C:/temp/app/app.jar
29.06.2011 00:16:04 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.Component1A@6bdd46f7 from URL: file:/C:/temp/app/./ext/extension1.ext.jar
29.06.2011 00:16:04 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.Component1B@7e0df503 from URL: file:/C:/temp/app/./ext/extension1.ext.jar
29.06.2011 00:16:04 de.tutorials.app.components.AbstractComponent start
INFO: Started StandardComponent
29.06.2011 00:16:04 de.tutorials.app.components.AbstractComponent start
INFO: Started Component1A
29.06.2011 00:16:04 de.tutorials.app.components.AbstractComponent start
INFO: Started Component1B

Wie wir sehen hat unsere Erweiterungsmechanismus nun auch die Erweiterungen aus unserem extension1.ext.jar erkannt (Component1A, Component1B).

Hier die Ausgabe mit einem weiteren extension-jar:
Code:
C:\temp\app>java -jar app.jar
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Using extension folder: C:\temp\app\.\ext
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Found 2 extensions
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Found extension jar: file:/C:/temp/app/./ext/extension1.ext.jar
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry lookupExtensionUrls
INFO: Found extension jar: file:/C:/temp/app/./ext/extension2.ext.jar
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.StandardComponent@b815859 from URL: file:/C:/temp/app/app.jar
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.Component1A@6100ab23 from URL: file:/C:/temp/app/./ext/extension1.ext.jar
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.Component1B@446b7920 from URL: file:/C:/temp/app/./ext/extension1.ext.jar
29.06.2011 00:19:59 de.tutorials.extensibility.ServiceLoaderExtensionRegistry getExtensions
INFO: Found extension for de.tutorials.app.components.Component: de.tutorials.app.components.Component2XXX@65bd0dd4 from URL: file:/C:/temp/app/./ext/extension2.ext.jar
29.06.2011 00:19:59 de.tutorials.app.components.AbstractComponent start
INFO: Started StandardComponent
29.06.2011 00:19:59 de.tutorials.app.components.AbstractComponent start
INFO: Started Component1A
29.06.2011 00:19:59 de.tutorials.app.components.AbstractComponent start
INFO: Started Component1B
29.06.2011 00:19:59 de.tutorials.app.components.AbstractComponent start
INFO: Started Component2XXX

Ich hoffe dieses Beispiel hat klar gemacht wie einfach man sehr flexible Erweiterungsmechanismen auf Basis des ServiceLoader APIs definieren kann.

Hier ein paar weitere Beispiele zum ServiceLoader API:
http://www.tutorials.de/java/357126-wieder-mal-java-und-plug-ins.html#post1850187
http://www.tutorials.de/java/358931-services-dynamisch-laden.html#post1859684

Gruß Tom
 

Anhänge

  • de.tutorials.serviceLoader.extensibility.zip
    29,7 KB · Aufrufe: 98
S

SE

Anmerkung aus Erfahrung:
Dieses Prinzip lässt sich NICHT in ein externes Jar auslagern. Die gesamte ServiceLoader-Einheit muss zum Startzeitpunkt der VM im CLASSPATH bekannt sein !
 

Thomas Darimont

Erfahrenes Mitglied
Hallo,

Dieses Prinzip lässt sich NICHT in ein externes Jar auslagern. Die gesamte ServiceLoader-Einheit muss zum Startzeitpunkt der VM im CLASSPATH bekannt sein !

Ich denke das stimmt so nicht.... das Interface mit dem du nach Erweiterungen suchst muss natürlich dem Modul bekannt sein, das Erweiterungen dazu sucht... ;-)

Weitere jars kannst du der Applikation auch einfach hinzufügen, du musst nur in der Anwendung erkennen, dass es neue Erweiterungen gibt und einen neuen ClassLoader erzeugen welche dann die neuen Erweiterungen berücksichtigt.

Gruß Tom
 
S

SE

Mein Post zielte darauf ab das du immer nur Lösungen und "Tricks" postest an denen man ganz schön rumtricksen muss ... nämlich das manuelle angeben des CP. Genau das ist aber irgendwie nicht der Sinn eines Plug-In basierten Systems da sich der Plug-In-Handler in aller regel seine Plug-In's selbst suchen können sollte. Das wirklich Problem was hierbei auftritt ist wenn man einen solchen Plug-In-Handler ebenfalls als art Plug-In oder Extension der Applikation hinzufügen möchte. Es kracht an genau dem Punkt an dem das gesamte Plug-In System zur Runtime nachgeladen wird und nicht schon zur Laufzeit bekannt ist.

Hier sollte man wirklich auf diese Besonderheit hinweisen da einige User *unter anderem auch ich* sonst daran verzweifeln das es nicht läuft weil man es als neue Modul seiner Applikation hinzugefügt hat. Es besteht schon ein gewisser Aufwand in einen bestehende Anwendung soetwas im nachhinein zu integrieren.