Frage Erstellen Sie die Implementierung der Java-Klasse dynamisch basierend auf den zur Laufzeit bereitgestellten Abhängigkeiten


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.

Zum Beispiel habe ich eine Bibliothek, die eine JSON-Antwort benötigt, um in mehreren Klassen geparst zu werden. Die Bibliothek hat 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, d.h. GsonJsonParser, JacksonJsonParser, Jackson2JsonParserDerzeit muss der Benutzer der Bibliothek die zu verwendende Implementierung basierend auf der Bibliothek auswählen, die sie in ihr Projekt aufgenommen haben. Beispielsweise:

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

Was ich gerne machen würde, ist dynamisch auszuwählen, welche Bibliothek auf dem Klassenpfad ist, und die richtige Instanz zu erstellen, so dass der Benutzer der Bibliothek nicht darüber nachdenken muss (oder sogar die interne Implementierung von Eine andere Klasse analysiert JSON).

Ich überlege mir etwas Ähnliches wie das Folgende:

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");

Wäre das eine angemessene Lösung? 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?


21
2018-05-15 15:05


Ursprung


Antworten:


Im Wesentlichen möchten Sie Ihre eigenen erstellen JsonParserFactory. Wir können sehen, wie es in der Frühlingsstiefel-Rahmen:

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();
}

Ihre Herangehensweise ist fast die selbe, abgesehen von der Verwendung der ClassUtils.isPresent Methode.


10
2018-05-15 15:19



Das klingt nach einem perfekten Fall für die Service Provider Schnittstelle (SPI) Muster. Besuche die java.util.ServiceLoader Dokumentation für ein Beispiel, wie man es implementiert.


9
2018-05-15 15:40



Wenn nur eine der Implementierungen (GsonJsonParser, JacksonJsonParser, Jackson2JsonParser) wäre zur Laufzeit vorhanden und es gibt keine andere Möglichkeit, dann müsste man sie benutzen Class.forName().

Obwohl Sie damit klüger umgehen können. Zum Beispiel können Sie alle Klassen in ein Set<String> und dann Schleife über sie. 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 wird abhängig von der Bibliothek. 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 mit dem obigen Schritt fortfahren.


Auch anstelle von plain Class.forName(String name)Sie können eine bessere Option verwenden Class.forName(String name, boolean initialize, ClassLoader loader) die keine statischen Initialisierer ausführen wird (falls in Ihrer Klasse vorhanden).

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


5
2018-05-15 15:23



Der einfache Ansatz ist der, den SLF4J verwendet: Erstellen Sie eine separate Wrapper-Bibliothek für die zugrunde liegende JSON-Bibliothek (GSON, Jackson usw.) mit einem 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 folgt 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, um den JSON-Parser zu lokalisieren. 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. Dann dein getJsonParser Methode würde wie folgt aussehen:

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

IMO dieser Ansatz ist unnötig komplexer als der erste.


4
2018-05-15 18:54