Pull to refresh

Comments 14

Интересно, не знал про такую штуку. Но полностью согласен с вашим финальным комментарием про IoC. Что-то как-то некомфортно себя чувствовать, когда какой-то хитрый код что-то за меня делает незаметно.

Непонятно, где использование этого механизма оправданно. Даже при работе с JDBC обычно грузишь название драйвера из пропертей, чтобы поменять можно было легко, поправив один текстовый файл. А тут, получается, надо будет проект пересобирать.

Сдается мне, этот SPI агент Сатаны, чтобы всех нас утащить в jar-hell. Есть у кого идеи, как можно удобно использовать этот SPI? У меня ничего в голове не появилось после 10 минут обдумывания.
пример где это можно было бы использовать: liquibase (вместо этого автор сделал свой собственный костыль на основе манифеста). Скажем, у liquibase есть свой собственный класс для работы с MySQL. Работает это так: код хочет создать новую таблицу в БД, запрашивает у сервис локатора все доступные имплементации интерфейса СоздаваторТаблиц, идет по ним и у каждой интересуется, умеет ли она работать с MySQL. Находит одну и с ее помощью создает таблицу. Как я сказал все работает прямо out-of-the-box. Однако, код не умеет устанавливать engine. И в моей личной базе данных плодит MyISAM таблицы, что мне не нравится. Благодаря механизму расширений, я создаю свой джарничек, который кладу в класспаф, в нем правильно настраиваю окружение (манифест/сервисы) и пишу свою собственную имплементацию, которая твикает sql так, как мне нужно: pastebin.com/PRxFuWTa. Чтобы избежать проблем с jar-hell автор использует метод getPriority, чтобы выбирать между иначе равнозначными имплементациями.
Еще похожий пример — slf4j, используется свой костыль.

Еще может быть полезно, как я уже в статье говорил, если приложение достаточно старое и громоздкое и очень сложно перейти на использование IoC-контейнера, например если построено на синглтонах и фабриках. Вот здесь этот механизм как раз кстати, и волки целы, и овцы сыты и код практически не надо переделывать, и расширения писать удобно.
кроме всего прочего, IoC — это доп либа, а этот механизм доступен прямо с jvm — нет никаких лишних зависимостей.
Стандартный ServiceLoader не пригоден для использования — в нем нельзя (или по крайней мере очень трудно) подменить возвращаемые сервисы, а без этого не возможно тестировать методы. Поэтому нужно писать свою реализацию.
имхо ServiceLoader используется на этапе «сборки» графа объектов и для него нужно писать интеграционные тесты из серии «собрали джарник, запустили — работает», а не юнит-тесты, т.к. не очень понятно, что именно тут можено про-юнит-тестировать.
ServiceLoader тем и хорош, что его можно использовать везде. Вот только тестировать после этого нельзя.
Все очень просто — если надо во время тестов подменить провайдер, нужно просто подложить в тест класпас нужный файлик, ну или как вариант подложить свой фэйковый класлоадер.
А если у нас 2 теста и каждому из них нужна своя мок-реализация сервиса?
С фейковым класслоадером тоже есть проблемы. Общаться с тестируемым объектом нужно через рефлекшен — ибо он не совместим с обычным классом (класслоадер-то другой!). Написать фейковый класслоадер тоже весьма нетривиально.
На каждую архитектуру приложения нужна своя особенная архитектура тестов, и она тоже важна. У большинства фреймворков есть готовые реализации архитектуры для тестов, у голой джавы нет, что тут поделаешь, приходится писать свою, но все равно нерешаемых задач нет.
Mock classloader гуглится по сочетанию «mock classloader», их там дофига
Кроме прочего SPI штатно используется для определения текущего XML парсера. Очень кривое решение, с теми же XML парсерами от него больше проблем чем пользы (грузится тот, который раньше указан в classpath-е, чтобы это изменить приходится прибегать к разного рода шаманству).
К тому же, SPI-механизм не поддерживает lazy загрузку и, как следствие, жаден до ресурсов. Например перебор доступных реализаций JDBC загрузит в jvm все найденные драйверы — если их много, то это тьма памяти и времени. А нужен-то обычно всего один.
В коде

public class ReportRenderer {
  //...

  public List<String> findMusic() {
    final List<String> music = new ArrayList<String>();
    for (final MusicFinder finder : ServiceLoader.load(MusicFinder.class)) {
      music.addAll(finder.getMusic());
    }
    Collections.sort(music);
    return music;
  }

  //...
}


ServiceLoader будет всякий раз создавать итератор и заполнять внутренний кэш на итерировании. Если в ClassLoader не добавляются динамически новые ресурсы с META-INF/services/..., то было бы более производительно создать ServiceLoader один раз (например, записав его в статическое финальное поле), и просто итерировать по нему в других методах.

Если все-таки в ClassLoader в рантайме добавляются ресурсы, то можно при добавлении такого ресурса вызывать ServiceLoader.reload(), чтобы очистить кэш уже загруженных инстансов.

Единственная проблема — в том, что ServiceLoader не потокобезопасен. Однако проблему можно решить, создав какой-нибудь метод типа
public static void visitAllMusicFinders(Consumer<MusicFinder> consumer) {
  synchronized (serviceLoader) {
    for (final MusicFinder musicFinder : serviceLoader) {
        consumer.accept(musicFinder);
    }
  }
}
Спасибо! Через 8 лет статья оказалась полезной. Нагуглена по java SPI mechanism :)
Sign up to leave a comment.

Articles

Change theme settings