Java ServiceLoader с несколькими загрузчиками классов

Каковы наилучшие методы использования ServiceLoader в среде с несколькими ClassLoaders? В документации рекомендуется создать и сохранить один экземпляр службы при инициализации:

private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);

Это приведет к инициализации ServiceLoader с использованием текущего загрузчика классов. Предположим теперь, что этот фрагмент содержится в классе, загруженном с использованием совместно используемого загрузчика классов в веб-контейнере, и несколько веб-приложений хотят определить свои собственные реализации служб. Они не попадут в вышеупомянутый код, возможно, даже будет возможно, что загрузчик будет инициализирован с использованием первого загрузчика классов контекста webapps и предоставит неверную реализацию другим пользователям.

Всегда создание нового ServiceLoader кажется бесполезным, так как он должен каждый раз перечислять и анализировать служебные файлы. Изменить: Это может быть даже большая проблема с производительностью, как показано в этом ответе на реализацию java XPath.

Как другие библиотеки справляются с этим? Они кэшируют реализации для каждого загрузчика классов, каждый раз они пересматривают свою конфигурацию или просто игнорируют эту проблему и работают только для одного загрузчика классов?

Ответ 1

Мне лично не нравится ServiceLoader при любых обстоятельствах. Это медленно и бесполезно расточительно, и мало что можно сделать для его оптимизации.

Я также считаю это немного ограниченным - вам действительно нужно уйти с вашего пути, если вы хотите сделать больше, чем поиск по типу.

xbean-finder ResourceFinder

  • ResourceFinder - это автономный Java файл, способный заменить использование ServiceLoader. Повторное использование копии/вставки не представляет проблемы. Это один java файл и ASL 2.0 лицензирован и доступен из Apache.

Прежде чем наше внимание будет слишком коротким, вот как он может заменить ServiceLoader

ResourceFinder finder = new ResourceFinder("META-INF/services/");
List<Class<? extends Plugin>> impls = finder.findAllImplementations(Plugin.class);

Это найдет все реализации META-INF/services/org.acme.Plugin в вашем пути к классам.

Обратите внимание, что он фактически не создает экземпляры всех экземпляров. Выберите один (ы), который вы хотите, и вы один newInstance() вызов от экземпляра.

Почему это приятно?

  • Насколько сложно вызывать newInstance() с правильной обработкой исключений? Не сложно.
  • Свободно создавать экземпляры только тех, которые вы хотите, приятно.
  • Теперь вы можете поддерживать конструкторы args!

Сужение области поиска

Если вы хотите просто проверить определенные URL-адреса, вы можете сделать это легко:

URL url = new File("some.jar").toURI().toURL();
ResourceFinder finder = new ResourceFinder("META-INF/services/", url);

Здесь будет выполняться поиск только "some.jar" при любом использовании этого экземпляра ResourceFinder.

Также есть класс удобства, называемый UrlSet, который может очень легко выбирать URL-адреса из класса.

ClassLoader webAppClassLoader = Thread.currentThread().getContextClassLoader(); 
UrlSet urlSet = new UrlSet(webAppClassLoader);
urlSet = urlSet.exclude(webAppClassLoader.getParent());
urlSet = urlSet.matching(".*acme-.*.jar");

List<URL> urls = urlSet.getUrls();

Альтернативные стили "службы"

Предположим, вы хотели применить концепцию типа ServiceLoader для перепроектирования обработки URL-адресов и поиска/загрузки java.net.URLStreamHandler для определенного протокола.

Вот как вы могли бы расположить службы в своем пути к классам:

  • META-INF/java.net.URLStreamHandler/foo
  • META-INF/java.net.URLStreamHandler/bar
  • META-INF/java.net.URLStreamHandler/baz

Где foo - это простой текстовый файл, который содержит имя реализации службы так же, как и раньше. Теперь скажите, что кто-то создает URL foo://.... Мы можем быстро найти реализацию, используя:

ResourceFinder finder = new ResourceFinder("META-INF/");
Map<String, Class<? extends URLStreamHandler>> handlers = finder.mapAllImplementations(URLStreamHandler.class);
Class<? extends URLStreamHandler> fooHandler = handlers.get("foo");

Альтернативные стили "службы" 2

Предположим, вы хотели поместить некоторую конфигурационную информацию в свой файл службы, поэтому она содержит больше, чем просто имя класса. Здесь альтернативный стиль, который разрешает услуги файлам свойств. По соглашению одним ключом были бы имена классов, а остальные ключи были бы пригодными для инъекций.

Итак, здесь red - файл свойств

  • META-INF/org.acme.Plugin/red
  • META-INF/org.acme.Plugin/blue
  • META-INF/org.acme.Plugin/green

Вы можете выглядеть так же, как и раньше.

ResourceFinder finder = new ResourceFinder("META-INF/");

Map<String,Properties> plugins = finder.mapAllProperties(Plugin.class.getName());
Properties redDefinition = plugins.get("red");

Здесь вы можете использовать эти свойства с помощью xbean-reflect, еще одной небольшой библиотеки, которая может предоставить вам IoC без рамки. Вы просто даете ему имя класса и некоторые пары значений имени, и он будет строить и вводить.

ObjectRecipe recipe = new ObjectRecipe(redDefinition.remove("className").toString());
recipe.setAllProperties(redDefinition);

Plugin red = (Plugin) recipe.create();
red.start();

Вот как это может выглядеть "написано" в длинной форме:

ObjectRecipe recipe = new ObjectRecipe("com.example.plugins.RedPlugin");
recipe.setProperty("myDateField","2011-08-29");
recipe.setProperty("myIntField","100");
recipe.setProperty("myBooleanField","true");
recipe.setProperty("myUrlField","http://www.stackoverflow.com");
Plugin red = (Plugin) recipe.create();
red.start();

Библиотека xbean-reflect - это шаг за пределами встроенного API JavaBeans, но немного лучше, не требуя от вас полного подхода к полнофункциональной инфраструктуре IoC, например Guice или Spring. Он поддерживает factory методы и конструкторы args и setter/field injection.

Почему ServiceLoader так ограничен?

Устаревший код в JVM повредит сам язык Java. Многие вещи обрезаются до костей, а затем добавляются в JVM, потому что вы не можете их обрезать. ServiceLoader является ярким примером этого. API ограничен, а реализация OpenJDK - около 500 строк, включая javadoc.

Там ничего нет, и заменить его легко. Если это не сработает для вас, не используйте его.

Объект класса

API в стороне, в чистой практичности сужение объема поиска URL-адресов является истинным решением этой проблемы. У серверов приложений достаточно много URL-адресов, не включая банки в вашем приложении. Например, Tomcat 7 на OSX имеет около 40 ~ URL-адресов только в стандартном ClassClassLoader (это является родительским для всех загрузчиков классов Webapp).

Чем больше ваш сервер приложений, тем дольше будет выполняться простой поиск.

Кэширование не помогает, если вы собираетесь искать более одной записи. Кроме того, он может добавить некоторые плохие утечки. Может быть реальным потерянным сценарием.

Ограничьте URL-адреса до 5 или 12, которые вам действительно интересны, и вы можете делать всевозможную загрузку службы и никогда не замечаете ее.

Ответ 2

Mu.

В системе 1x WebContainer ↔ Nx WebApplication, ServiceLoader, созданный в WebContainer, не будет выбирать классы, определенные в WebApplications, только те, которые находятся в контейнере. ServiceLoader, созданный в WebApplication, будет определять классы, определенные в приложении, в дополнение к тем, которые определены в контейнере.

Имейте в виду, что WebApplications нужно хранить отдельно, спроектированы таким образом, все будет нарушено, если вы попытаетесь обходить это, и они не являются способом и системой, доступными для расширения контейнера - если ваша библиотека является простой Jar, просто опустите его в соответствующую папку расширения контейнера.

Ответ 3

Пробовали ли вы использовать две версии аргументов, чтобы вы могли указать, какой класс загрузчик использовать? Т.е., java.util.ServiceLoader.load(класс, классLoader)

Ответ 4

Мне очень нравится, что Нейл ответил в ссылке, которую я добавил в своем комментарии. Из-за того, что у меня есть те же самые эксперименты в моем недавнем проекте.

"Еще одна вещь, которая стоит иметь в виду с ServiceLoader - попытаться абстрагировать механизм поиска. Механизм публикации довольно приятный, чистый и декларативный. Но поиск (через java.util.ServiceLoader) такой же уродливый, как и реализованный как сканер классов, который очень сильно ломается, если вы помещаете код в какую-либо среду (например, OSGi или Java EE), которая не имеет глобальной видимости. Если ваш код запутался в этом, вам будет трудно работать в OSGi позже. Лучше написать абстракцию, которую вы можете заменить, когда придет время".

Я действительно встретил эту проблему в среде OSGi, на самом деле это просто затмение в нашем проекте. Но я, к счастью, исправил это своевременно. Мое обходное решение использует один класс из плагина, который я хочу загрузить, и получить от него классLoader. Это будет действительное исправление. Я не использовал стандартный ServiceLoader, но мой процесс довольно похож, используйте свойства для определения классов плагинов, которые мне нужно загрузить. И я знаю, что есть еще один способ узнать каждый загрузчик классов plugin. Но, по крайней мере, мне не нужно это использовать.

Честное слово, мне не нравятся дженерики, используемые в ServiceLoader. Поскольку он ограничивает то, что один ServiceLoader может обрабатывать только классы для одного интерфейса. Ну, это действительно полезно? В моей реализации это не заставляет вас этим ограничением. Я просто использую одну реализацию загрузчика для загрузки всех классов плагинов. Я не вижу причины использовать два или более. Из-за того, что потребитель может узнать из конфигурационных файлов о связях между интерфейсами и реализацией.

Ответ 5

Этот вопрос кажется более сложным, чем я предполагал. Как я вижу это, есть 3 возможные стратегии для работы с ServiceLoaders.

  • Используйте статический экземпляр ServiceLoader и поддерживайте только классы загрузки из тот же загрузчик классов, что и тот, который содержит ссылку ServiceLoader. Эта будет работать, когда

    • Конфигурация и реализация службы находятся в общем загрузчике классов и все дочерние загрузчики классов используют одну и ту же реализацию. Пример в документации привязаны к варианту использования.

      или

    • Конфигурация и реализация помещаются в каждый дочерний загрузчик классов и развернуты вдоль каждого webapp в WEB-INF/lib.

    В этом случае развертывание службы в разделяемом загрузчике классов невозможно и пусть каждый webapp выбирает собственную реализацию службы.

  • Инициализировать ServiceLoader для каждого доступа, передающего загрузчик классов контекста текущий поток в качестве второго параметра. Этот подход используется как JAXP и JAXB apis, хотя они используют собственную FactoryFinder вместо ServiceLoader. Таким образом, можно связать синтаксический анализатор xml с помощью webapp и автоматически его забирают, например, DocumentBuilderFactory#newInstance.

    Этот поиск имеет влияние производительности, но в случае разбора xml время поиска реализации мало по сравнению с временем, необходимым для фактически проанализировать XML-документ. В библиотеке я предвижу factory сама по себе довольно проста, поэтому время поиска будет доминировать над производительностью.

  • Как-то кэшировать класс реализации с помощью класса classloader в качестве ключа. Я не совсем уверен, что это возможно во всех вышеперечисленных случаях без вызывая утечку памяти.

В заключение я, вероятно, проигнорирую эту проблему и требую, чтобы библиотека развертывается внутри каждого webapp, то есть варианта 1b выше.