Архитектура большинства 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-контейнера более красивым решением, но все же кое-где и кое-кому такой подход может оказаться полезным. Спасибо за уделенное на прочтение статьи время.