在运行时基于提供的依赖动态创建Java类的实现

21
我将尝试确定在运行时基于类路径上可用的类创建一个新类实例的最佳方法。
例如,我有一个库需要解析多个类中的JSON响应。该库具有以下接口:
JsonParser.java:
public interface JsonParser {
    <T> T fromJson(String json, Class<T> type);
    <T> String toJson(T object);
}

这个类有多个实现,例如GsonJsonParserJacksonJsonParserJackson2JsonParser,目前,库的用户需要根据他们在项目中包含的库来“选择”要使用的实现。例如:

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

我想做的是动态地获取类路径上哪个库,并创建正确的实例,这样库的用户就不必考虑它(甚至不必知道另一个类解析JSON的内部实现)。
我考虑类似以下内容:
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");

这是否是一个合适的解决方案?它似乎有些像黑客,同时使用异常来控制流程。

有更好的方法吗?


3
我同意,这是一个“hack”。选择一个JSON解析器依赖并使用它?你的方法也可能变得出乎意料地不确定;根据JAR包(以及可能在类路径中的JAR包顺序),程序的行为可能会发生变化。 - Elliott Frisch
@ElliottFrisch 我想这样做,但目前将使用这个的应用程序中没有一个是这些解析器之一,我正试图避免如果它与我决定打包的那个不同(或者是同一库的不同版本),则会出现第二个依赖项。 - blacktide
@ElliottFrisch,看起来这不是什么黑客行为,请查看我的答案,我在其中提供了Spring实现相同问题的链接。 - Andremoniy
@Andremoniy 是的,Spring 也这样做;它仍然是一个hack。我甚至会承认它看起来是一个聪明的hack。但是,如果Java 9+添加了内置的JSON解析器,那么它就会追溯地成为什么? - Elliott Frisch
@Casey当类路径上缺少类时,如何使实现得以编译? - Tomáš Zato
@TomášZato,我将它们作为提供的依赖项包含在内。 - blacktide
4个回答

10

基本上,你想要创建自己的JsonParserFactory。我们可以看到在Spring Boot框架中是如何实现的:

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

所以你的方法与这个方法几乎相同,除了使用 ClassUtils.isPresent 方法。


2
感谢您的回答。看起来这正是ClassUtils正在做的事情。所以也许我并没有完全错。 - blacktide
@Casey,你说得完全正确,我错过了ClassUtils本身是Spring的一部分这个事实,所以我已经编辑了我的答案。 - Andremoniy

9

5
如果只有一个实现(GsonJsonParserJacksonJsonParserJackson2JsonParser)在运行时存在且没有其他选项,则必须使用Class.forName()
虽然你可以更聪明地处理它。例如,你可以将所有类放入Set<String>中,然后循环遍历它们。如果其中任何一个抛出异常,你可以继续下一个,而不是停止,直到找到不抛出异常的为止,然后执行你需要的操作。
是的,这是一个hack,你的代码将变得库相关。如果能够把三个实现都包含在classpath中,并使用逻辑来定义要使用哪个实现,那么这将是一个更好的方法。
如果这不可行,可以按照上述方法继续。
此外,你可以使用一个更好的选项Class.forName(String name, boolean initialize, ClassLoader loader),而不是使用普通的Class.forName(String name),它不会运行任何静态初始化器(如果在你的类中存在静态初始化器)。
其中initialize = falseloader = [class].getClass().getClassLoader()

4
简单的方法是SLF4J使用的方法:为每个基础JSON库(GSON,Jackson等)创建一个单独的包装器库,其中包含一个com.mypackage.JsonParserImpl类,该类委托给基础库。将适当的包装器放在与基础库相同的类路径中。然后,您可以像这样获取当前实现:
public JsonParser getJsonParser() {
    // needs try block
    // also, you probably want to cache
    return Class.forName("com.mypackage.JsonParserImpl").newInstance()
}

这种方法使用类加载器来定位JSON解析器。它是最简单的,不需要第三方依赖或框架。相对于Spring、服务提供者或任何其他定位资源的方法,我认为它没有任何缺点。
另一种方法是使用Service Provider API,就像Daniel Pryden建议的那样。要做到这一点,您仍然需要针对每个底层JSON库创建一个单独的包装库。每个库都包括一个文本文件,位于“META-INF/services/com.mypackage.JsonParser”位置,其内容是该库中JsonParser实现的完全限定名称。然后,您的getJsonParser方法将如下所示:
public JsonParser getJsonParser() {
    return ServiceLoader.load(JsonParser.class).iterator().next();
}

在我看来,这种方法比第一种方法更加复杂,而且并没有必要。


有趣的解决方案,肯定比我提出的更好、更易于维护。我测试了服务提供者 API,它似乎并不更复杂,只是稍微有点烦人,需要在每个包装器的 META-INF 中添加那个文件。我确实喜欢它允许类保持不同的名称,例如 GsonJsonParserJacksonJsonParser 而不是 JsonParserImpl,但这纯粹是语义上的吧... - blacktide

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接