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