Создавать реализацию класса Java динамически на основе предоставленных зависимостей во время выполнения
Я пытаюсь определить лучший способ создать новый экземпляр класса, основанный на том, какие классы доступны в пути к классам во время выполнения.
Например, у меня есть библиотека, которая требует, чтобы ответ JSON обрабатывался несколькими классами. Библиотека имеет следующий интерфейс:
JsonParser.java
:
public interface JsonParser {
<T> T fromJson(String json, Class<T> type);
<T> String toJson(T object);
}
Этот класс имеет несколько реализаций, то есть GsonJsonParser
, JacksonJsonParser
, Jackson2JsonParser
, и в настоящее время пользователь библиотеки должен "выбрать" свою реализацию, которая будет использоваться на основе той библиотеки, в которую они были включены их проект. Например:
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");
Будет ли это подходящим решением? Это похоже на хак, а также использует исключения для управления потоком.
Есть ли лучший способ сделать это?
Ответы
Ответ 1
По существу, вы хотите создать свой собственный JsonParserFactory
. Мы видим, как он реализован в Spring Структура загрузки:
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
Это звучит как идеальный случай для шаблона интерфейса поставщика услуг (SPI). Ознакомьтесь с документацией java.util.ServiceLoader
для примера того, как ее реализовать.
Ответ 3
Если только одна из реализаций (GsonJsonParser
, JacksonJsonParser
, Jackson2JsonParser
) будет присутствовать во время выполнения, и другой опции нет, тогда вам нужно будет использовать Class.forName()
.
Хотя вы можете справиться с этим умнее.
Например, вы можете поместить все классы в Set<String>
, а затем перебрать их. Если какой-либо из них вызывает исключение, вы можете просто продолжить, а тот, который этого не делает, вы можете выполнять свои операции.
Да, это взломать, и ваш код станет зависимым от библиотеки. Если есть вероятность, что вы можете включить все три реализации JsonParsers в ваш путь к классам и использовать логику, чтобы определить, какую реализацию вы должны использовать; это будет гораздо лучший подход.
Если это невозможно, вы можете продолжить работу выше.
Кроме того, вместо простого Class.forName(String name)
вы можете использовать лучший вариант Class.forName(String name, boolean initialize, ClassLoader loader)
, который НЕ запускает никаких статических инициализаторов (если они присутствуют в вашем классе).
Где initialize = false
и loader = [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, поставщика услуг или любого другого метода поиска ресурсов.
Альтернативно используйте API-интерфейс поставщика услуг, как предлагает Даниэль Приден. Для этого вы по-прежнему создаете отдельную библиотеку-оболочку для каждой базовой библиотеки JSON. Каждая библиотека включает текстовый файл в месте "META-INF/services/com.mypackage.JsonParser", чье содержимое является полнофункциональным именем реализации JsonParser
в этой библиотеке. Тогда ваш метод getJsonParser
будет выглядеть так:
public JsonParser getJsonParser() {
return ServiceLoader.load(JsonParser.class).iterator().next();
}
ИМО этот подход излишне сложнее первого.