Pull to refresh

Использование SPI механизма для создания расширений

Reading time5 min
Views28K
Архитектура большинства Java(и не только) приложений сегодня предусматривает возможность расширения функционала посредством различного рода магических воздействий на код. В последнее время это также стало возможно, если использовать какой-нибудь модный фреймворк или IoC-контейнер. Но что делать, если приложение долгоживущее и слишком сложное для того, чтобы переводить его на использование какого либо фреймворка?

В последнем приложении, с которым я работал, был реализован на тот момент неизвестный мне велосипед SPI механизм, который искал в джарках текстовые файлы вида META-INF/services/<qualified interface name> и брал оттуда название нужного класса, реализующего этот интерфейс, далее этот класс использовался как расширение. Поискав в интернете, узнал, что Service Provider Interface(SPI) представляет собой программный механизм для поддержки сменных компонентов и что этот механизм уже довольно давно используется в Java Runtime Environment(JRE), например в Java Database Connectivity(JDBC):
ps = Service.providers(java.sql.Driver.class);
try {
  while (ps.hasNext()) {
    ps.next();
  }
} catch (Throwable t) {
  // Do nothing
}


Благодаря этому коду приложения больше не нуждаются в конструкции Class.forName(<driver class>) (хотя и с ней будут работать), JDBC драйверы будут подгружены автоматически при первом обращении к методам класса DriverManager.

SPI механизм также используется в Java Cryptography Extension(JCE), Java Naming and Directory Service(JNDI), Java API for XML Processing(JAXP), Java Business Integration(JBI), Java Sound, Java Image I/O.

Как это работает?


Весь смысл в разделении логики на сервис(Service) и провайдеры(Service Providers). Ссылки на провайдеры сохраняются в джарках расширений в текстовом файле(UTF-8) META-INF/services/<qualified service class>, в каждой строке полное имя класса провайдера. Пустые строки и комментарии(начинающиеся с символа #) игнорируются. Ограничения на провайдеры: они должны реализовывать интерфейс либо наследоваться от класса сервиса и иметь конструктор по умолчанию(zero-argument public constructor).

Основное приложение для получения списка провайдеров может воспользоваться входящей в состав Java SE 6 API утилитой java.util.ServiceLoader, которая работает по следующему принципу:


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

В более ранних версиях Java SE есть аналогичная утилита sun.misc.Service, работает по тому же принципу, но является частью проприетарного ПО Sun Oracle и может быть удалена в следующих релизах Java SE.

Пример использования


Например, у нас есть программа, которая ищет музыку на компе и выводит отсортированный по имени результат на экран.
public class MusicFinder {

  public static List<String> getMusic() {
    //some code
  }
}

public class ReportRenderer {

  public void generateReport() {
    final List<String> music = findMusic();
    for (String composition : music) {
      System.out.println(composition);
    }
  }

  public List<String> findMusic() {
    final List<String> music = MusicFinder.getMusic();
    Collections.sort(music);
    return music;
  }

  public static ReportRenderer getInstance() {
    return new ReportRenderer();
  }

  public static void main(final String[] args) {
    final ReportRenderer renderer = ReportRenderer.getInstance();
    renderer.generateReport();
  }
}


В некоторый момент времени мы осознали всю значимость этой программы для общества и решили поделиться ей со своими друзьями. Друзья попользовались сервисом и решили, что чего-то не хватает. Может выводить в отдельный файл? Но тогда придется переписывать весь этот клевый код. Не придется, можно воспользоваться SPI механизмом.

Например, создадим плагин для нашей супер-программы:
public class FileReportRenderer extends ReportRenderer {

  @Override
  public void generateReport() {
    final List<String> music = findMusic();
    try {
      final FileWriter writer = new FileWriter("music.txt");
      for (String composition : music) {
        writer.append(composition);
        writer.append("\n");
      }
      writer.flush();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}


Поместим в META-INF/services/com.example.ReportRenderer следующее:
com.example.FileReportRenderer


Сделаем исходную программу расширяемой:
public class ReportRenderer {
  //...

  public static ReportRenderer getInstance() {
    final Iterator<ReportRenderer> providers = ServiceLoader.load(ReportRenderer.class).iterator();
    if (providers.hasNext()) {
      return providers.next();
    }

    return new ReportRenderer();
  }

  //...
}


При запуске приложение, как и прежде, будет выводить всю найденную музыку на экран. Но если мы поместим только что созданную джарку расширения в classpath, то мы получим в результате файлик music.txt, содержащий результаты поиска.

Теперь пришло время поиграться с MusicFinder-ом. Сделаем его тоже расширяемым. Для этого поменяем класс на интерфейс:
public interface MusicFinder {

  List<String> getMusic();
}


Добавим в основном модуле реализацию:
public class DummyMusicFinder implements MusicFinder {
  public List<String> getMusic() {
    return Collections.singletonList("From DummyMusicFinder...");
  }
}


Поддержка расширений в ReportRenderer:
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;
  }

  //...
}


Как и в случае с ReportRenderer добавим текстовый файл META-INF/services/com.example.MusicFinder, содержащий:
com.example.DummyMusicFinder


Опять же результат выполнения первой программы не поменялся. Теперь расширение. Здесь сделаем две реализации MusicFinder-а:
public class ExtendedMusicFinder implements MusicFinder {
  public List<String> getMusic() {
    return Collections.singletonList("From ExtendedMusicFinder...");
  }
}

public class MyMusicFinder implements MusicFinder {
  public List<String> getMusic() {
    return Collections.singletonList("From MyMusicFinder...");
  }
}


META-INF/service/com.example.MusicFinder:
com.example.MyMusicFinder
com.example.ExtendedMusicFinder


Ну, вот и все, программа поддерживающая расширения готова, теперь с расширением в classpath, она выдаст список:
From DummyMusicFinder...
From ExtendedMusicFinder...
From MyMusicFinder...


Исходники примера можно найти здесь.

Заключение


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

Литература


Плагин
Service Provider Interface
Service Provider
Service Provider Interface: Creating Extensible Java Applications
Service Loader
Tags:
Hubs:
Total votes 31: ↑30 and ↓1+29
Comments14

Articles