Soru Java sınıfının, çalışma zamanında sağlanan bağımlılıklara dayalı olarak dinamik olarak uygulanması


Çalışma zamanında sınıf yolunda hangi sınıfların kullanılabileceğini temel alarak yeni bir sınıf örneği oluşturmanın en iyi yolunu bulmaya çalışıyorum.

Örneğin, birden çok sınıfta çözümlenecek bir JSON yanıtı gerektiren bir kitaplığım var. Kütüphane aşağıdaki arayüze sahiptir:

JsonParser.java:

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

Bu sınıfın birden çok uygulaması vardır, örn. GsonJsonParser, JacksonJsonParser, Jackson2JsonParserve şu anda, kütüphanenin kullanıcısı, projelerine dahil ettikleri kütüphaneye dayalı olarak uygulamalarını "seçmek" için gereklidir. Örneğin:

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

Yapmak istediğim şey, hangi kütüphanenin sınıf yolunda olduğunu dinamik olarak alması ve uygun örneği oluşturması, böylece kütüphanenin kullanıcısı hakkında düşünmek zorunda kalmaması (hatta iç uygulamanın başka bir sınıf JSON ayrıştırır).

Aşağıdakine benzer bir şey düşünüyorum:

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

Bu uygun bir çözüm olur mu? Bir hack gibi görünüyor, aynı zamanda akışı kontrol etmek için istisnalar kullanır.

Bunu yapmanın daha iyi bir yolu var mı?


21
2018-05-15 15:05


Menşei


Katılıyorum, bu bir kesmek. Bir JSON ayrıştırıcı bağımlılığı seçin ve kullanın? Yaklaşımınız da belki beklenmedik bir şekilde olmak belirli olmayan; JAR'lara (ve muhtemelen sınıf yolundaki JAR'ların sırasına) bağlı olarak, program davranışı değişebilir. - Elliott Frisch
@ElliottFrisch Bunu yapmak istiyorum, ancak şu anda bunu kullanan uygulamalar bu ayrıştırıcıların herhangi birine sahip ve ben karar verdiğimden farklı bir ikinci bir bağımlılık olmaktan kaçınmaya çalışıyorum paket (veya aynı kütüphane, farklı versiyon). - blacktide
@ElliottFrisch Görünüşe göre bu bir hack değil, Spring'in aynı konudaki uygulamasına bağlantı verdiğim cevabımı gör. - Andremoniy
@Andremoniy Evet, Bahar da yapıyor; hala bir kesmek. Hatta bunun bir zeki hack. Ancak, eğer Java 9+ dahili bir JSON ayrıştırıcısı eklerse, o zaman geriye dönük olur? - Elliott Frisch
@Casey Sınıf yolundaki sınıflar eksik olduğunda uygulamaları nasıl derlersiniz? - Tomáš Zato


Cevaplar:


Esasen kendi yaratmak istediğin JsonParserFactory. Nasıl uygulandığını görebiliriz. İlkbahar Çizme çerçevesi:

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

Böylece yaklaşımın kullanımı dışında yaklaşımınız neredeyse aynıdır. ClassUtils.isPresent yöntem.


10
2018-05-15 15:19



Cevabınız için teşekkürler. Benziyor ClassUtils'in yaptığı tam olarak budur. Belki de değildim tamamen off-base. - blacktide
@Casey kesinlikle haklısın, gerçeği kaçırdım ClassUtils kendisi Spring'in bir parçası, ben de cevabımı düzenledim. - Andremoniy


Bu mükemmel bir durum gibi geliyor Servis Sağlayıcı Arabirimi (SPI) Desen. Kontrol et java.util.ServiceLoader nasıl uygulanacağına dair bir örnek.


9
2018-05-15 15:40





Eğer uygulamalardan sadece biri (GsonJsonParser, JacksonJsonParser, Jackson2JsonParser) çalışma zamanında mevcut olacak ve başka seçenek yok, sonra kullanmak zorunda kalacaksınız Class.forName().

Bununla başa çıkabilmenize rağmen daha akıllı. Örneğin, tüm sınıfları Set<String> ve daha sonra bunların üzerine döngü. Bunlardan herhangi biri istisna atarsa, sadece devam edebilirsiniz, ve olmayan, sizin operasyonlarınızı yapabilirsiniz.

Evet, bu bir hack, ve kodunuz kütüphaneye bağımlı olacak. JsonParsers'ınızın her üç uygulamasını sınıf yolunuza ekleyebileceğiniz ve kullanmak istediğiniz uygulamayı tanımlamak için bir mantık kullanabiliyorsanız; Bu çok daha iyi bir yaklaşım olurdu.

Bu mümkün değilse yukarıdaki ile devam edebilirsiniz.


Ayrıca, düz kullanmak yerine Class.forName(String name)Daha iyi bir seçenek kullanabilirsiniz Class.forName(String name, boolean initialize, ClassLoader loader) herhangi bir statik başlatıcıyı (sınıfınızda varsa) çalıştırmaz.

Nerede initialize = false ve loader = [class].getClass().getClassLoader()


5
2018-05-15 15:23





Basit bir yaklaşım, bir SLF4J'nin kullandığıdır: bir temel JSON kütüphanesi (GSON, Jackson, vb.) İçin ayrı bir sarmalayıcı kütüphanesi oluşturun. com.mypackage.JsonParserImpl Temel kütüphaneye delege veren sınıf. Uygun sarmalayıcıyı, temel kitaplığın yanında sınıf yoluna yerleştirin. Sonra mevcut uygulamayı şu şekilde alabilirsiniz:

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

Bu yaklaşım JSON ayrıştırıcısını bulmak için sınıf yükleyiciyi kullanır. Bu en basit ve üçüncü taraf bağımlılıkları veya çerçeveleri gerektirmez. Spring'e, Hizmet Sağlayıcısına veya kaynak bulma yöntemine göre hiçbir sakınca görmüyorum.


Alternatif olarak, Daniel Pryden'in önerdiği gibi Servis Sağlayıcı API'sini kullanın. Bunu yapmak için, yine de temel JSON kitaplığı başına ayrı bir sarmalayıcı kitaplığı oluşturursunuz. Her kütüphane, içeriği "META-INF / services / com.mypackage.JsonParser" adresindeki bir metin dosyası içeriyor. JsonParser bu kütüphanede. O zaman senin getJsonParser yöntem şöyle görünecekti:

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

IMO bu yaklaşım, ilkinden gereksiz olarak daha karmaşıktır.


4
2018-05-15 18:54



İlginç bir çözüm, kesinlikle önerdiğimden daha iyi ve daha sürdürülebilir bir seçenek gibi görünüyor. Hizmet Sağlayıcı API'sini test ettim ve daha fazla karmaşık görünmüyor, sadece bu dosyayı eklemek zorunda kalmam biraz rahatsız edici META-INF her sarıcı. Bunun gibi sınıfların farklı bir isim tutmasını sağlar. GsonJsonParser ve JacksonJsonParser yerine JsonParserImplama bu tamamen semantik sanırım ... - blacktide