2016-05-15 5 views
21

Ich versuche, den besten Weg zum Erstellen einer neuen Instanz einer Klasse zu ermitteln, basierend darauf, welche Klassen zur Laufzeit auf dem Klassenpfad verfügbar sind.Erstellen Sie die Implementierung der Java-Klasse dynamisch basierend auf bereitgestellten Abhängigkeiten zur Laufzeit

Zum Beispiel habe ich eine Bibliothek, die eine JSON-Antwort in mehreren Klassen analysiert werden muss. Die Bibliothek verfügt über folgende Schnittstelle:

JsonParser.java:

public interface JsonParser { 
    <T> T fromJson(String json, Class<T> type); 
    <T> String toJson(T object); 
} 

Diese Klasse hat mehrere Implementierungen, dh GsonJsonParser, JacksonJsonParser, Jackson2JsonParser, und zur Zeit wird der Benutzer der Bibliothek erforderlich „Pick“ ihre Umsetzung zu basierend darauf verwendet werden, welche Bibliothek sie in ihr Projekt aufgenommen haben. Zum Beispiel:

JsonParser parser = new GsonJsonParser(); 
SomeService service = new SomeService(parser); 

Was würde Ich mag zu tun, wird dynamisch abholen, welche Bibliothek auf dem Classpath ist, und die richtige Instanz erstellen, so dass der Benutzer der Bibliothek, nicht daran zu denken, hat (oder müssen sogar wissen, die interne Implementierung einer anderen Klasse JSON analysiert).

Ich erwäge etwas Ähnliches wie die folgenden:

try { 
    Class.forName("com.google.gson.Gson"); 
    return new GsonJsonParser(); 
} catch (ClassNotFoundException e) { 
    // Gson isn't on classpath, try next implementation 
} 

try { 
    Class.forName("com.fasterxml.jackson.databind.ObjectMapper"); 
    return new Jackson2JsonParser(); 
} catch (ClassNotFoundException e) { 
    // Jackson 2 was not found, try next implementation 
} 

// repeated for all implementations 

throw new IllegalStateException("You must include either Gson or Jackson on your classpath to utilize this library"); 

dies eine geeignete Lösung sein würde? Es scheint ein bisschen wie ein Hack zu sein, und nutzt Ausnahmen, um den Fluss zu kontrollieren.

Gibt es einen besseren Weg, dies zu tun?

+3

Ich stimme zu, es ist ein * hack *. Wählen Sie eine JSON-Parser-Abhängigkeit aus und verwenden Sie sie? Ihr Ansatz könnte auch * unerwartet * nicht deterministisch ** werden; abhängig von den JARs (und möglicherweise der Reihenfolge der JARs im Klassenpfad) könnte sich das Programmverhalten ändern. –

+0

@ElliottFrisch Ich möchte das machen, aber zur Zeit haben die Anwendungen, die dies verwenden, einen dieser Parser, und ich versuche zu vermeiden, dass eine zweite Abhängigkeit entsteht, wenn sie anders ist als die, die ich gewählt habe zu verpacken (oder dieselbe Bibliothek, andere Version). – Casey

+0

@ElliottFrisch Es scheint, es ist nicht so ein Hack, siehe meine Antwort, wo ich Link zu Spring's Umsetzung des gleichen Problems gab. – Andremoniy

Antwort

10

Im Wesentlichen möchten Sie Ihre eigenen JsonParserFactory erstellen. Wir können sehen, wie es in den Spring Boot framework implementiert ist:

public static JsonParser getJsonParser() { 
    if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null)) { 
     return new JacksonJsonParser(); 
    } 
    if (ClassUtils.isPresent("com.google.gson.Gson", null)) { 
     return new GsonJsonParser(); 
    } 
    if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) { 
     return new YamlJsonParser(); 
    } 

    return new BasicJsonParser(); 
} 

Also Ihr Ansatz ist fast das gleiche wie diese, mit Ausnahme der Verwendung der ClassUtils.isPresent method.

+2

Danke für Ihre Antwort. Es sieht so aus [genau das macht ClassUtils] (https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/util/ClassUtils. Java # L325-L334). Also vielleicht war ich nicht komplett offline. – Casey

+0

@Casey du hast absolut recht, ich habe die Tatsache übersehen, dass 'ClassUtils' selbst Teil von Spring ist, also habe ich meine Antwort bearbeitet. – Andremoniy

5

Wenn nur eine der Implementierungen (GsonJsonParser, JacksonJsonParser, Jackson2JsonParser) zur Laufzeit vorliegen würde, und es gibt keine andere Wahl, dann würden Sie Class.forName() verwenden.

Obwohl Sie damit klüger umgehen können. Zum Beispiel können Sie alle Klassen in eine Set<String> setzen und dann über sie hinwegschleifen. Wenn einer von ihnen eine Ausnahme auslöst, können Sie einfach fortfahren, und die andere nicht, Sie können Ihre Operationen ausführen.

Ja, es ist ein Hack, und Ihr Code würde Bibliothek abhängig werden. Wenn es eine Chance geben könnte, dass Sie alle drei Implementierungen Ihrer JsonParser in Ihren Klassenpfad aufnehmen können und eine Logik verwenden, um zu definieren, welche Implementierung Sie verwenden müssen; das wäre ein viel besserer Ansatz.

Wenn dies nicht möglich ist, können Sie weiter mit oben.


Auch anstelle von einfachen Class.forName(String name) verwenden, können Sie eine bessere Option Class.forName(String name, boolean initialize, ClassLoader loader) verwenden, die keine statischen Initialisierungen nicht ausgeführt wird (falls vorhanden in der Klasse).

Wo initialize = false und loader = [class].getClass().getClassLoader()

4

Der einfache Ansatz ist der, den SLF4J verwendet: Erstellen Sie eine separate Wrapper-Bibliothek pro zugrunde liegenden JSON-Bibliothek (GSON, Jackson, etc.) mit einer com.mypackage.JsonParserImpl Klasse, die an die zugrunde liegende Bibliothek delegiert. Fügen Sie den entsprechenden Wrapper neben der zugrunde liegenden Bibliothek in den Klassenpfad ein. Dann können Sie die aktuelle Implementierung wie erhalten:

public JsonParser getJsonParser() { 
    // needs try block 
    // also, you probably want to cache 
    return Class.forName("com.mypackage.JsonParserImpl").newInstance() 
} 

Dieser Ansatz verwendet den Klassenlader zum Suchen des JSON-Parsers. Es ist das einfachste und benötigt keine Abhängigkeiten oder Frameworks von Drittanbietern. Ich sehe keine Nachteile gegenüber Spring, Service Provider oder einer anderen Methode zur Lokalisierung von Ressourcen.


Verwenden Sie alternativ die Service Provider API, wie Daniel Pryden vorschlägt. Zu diesem Zweck erstellen Sie immer noch eine separate Wrapper-Bibliothek für die zugrunde liegende JSON-Bibliothek. Jede Bibliothek enthält eine Textdatei am Speicherort "META-INF/services/com.mypackage.JsonParser", deren Inhalt der vollständig qualifizierte Name der Implementierung von JsonParser in dieser Bibliothek ist. Dann wird Ihr getJsonParser Methode würde wie folgt aussehen:

public JsonParser getJsonParser() { 
    return ServiceLoader.load(JsonParser.class).iterator().next(); 
} 

IMO dieser Ansatz unnötig komplexer als die erste.

+0

Interessante Lösung, klingt definitiv wie eine bessere und wartungsfreundlichere Option als die, die ich vorgeschlagen habe. Ich habe die API des Dienstanbieters getestet und es scheint wirklich nicht komplizierter zu sein, nur ein bisschen nervig, diese Datei in META-INF jedes Wrappers hinzufügen zu müssen. Ich mag es, dass es den Klassen erlaubt, einen anderen Namen zu behalten, d. H. "GsonJsonParser" und "JacksonJsonParser" anstelle von "JsonParserImpl", aber das ist rein semantisch, nehme ich an ... – Casey

Verwandte Themen