Java service provider interface

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

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

В последнем приложении, с которым я работал, был реализован на тот момент неизвестный мне велосипед SPI механизм, который искал в джарках текстовые файлы вида и брал оттуда название нужного класса, реализующего этот интерфейс, далее этот класс использовался как расширение. Поискав в интернете, узнал, что 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() (хотя и с ней будут работать), JDBC драйверы будут подгружены автоматически при первом обращении к методам класса DriverManager.

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

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

Читайте также:  React html element type

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

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

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

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

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

public class MusicFinder < public static ListgetMusic() < //some code >> public class ReportRenderer < public void generateReport() < final Listmusic = findMusic(); for (String composition : music) < System.out.println(composition); >> public List findMusic() < final Listmusic = 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 Listmusic = 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(); >> > 
com.example.FileReportRenderer
public class ReportRenderer < //. public static ReportRenderer getInstance() < final Iteratorproviders = ServiceLoader.load(ReportRenderer.class).iterator(); if (providers.hasNext()) < return providers.next(); >return new ReportRenderer(); > //. > 

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

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

public interface MusicFinder < ListgetMusic(); > 
public class DummyMusicFinder implements MusicFinder < public ListgetMusic() < return Collections.singletonList("From DummyMusicFinder. "); >> 
public class ReportRenderer < //. public ListfindMusic() < final Listmusic = new ArrayList(); 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 ListgetMusic() < return Collections.singletonList("From ExtendedMusicFinder. "); >> public class MyMusicFinder implements MusicFinder < public ListgetMusic() < return Collections.singletonList("From MyMusicFinder. "); >> 
com.example.MyMusicFinder com.example.ExtendedMusicFinder

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

From DummyMusicFinder. From ExtendedMusicFinder. From MyMusicFinder.

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

Заключение

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

Источник

Service Provider Interface (SPI)

What’s Service Provider Interface (SPI)? It’s an interface which is meant to be used by java.util.ServiceLoader . It’s sort of a generalization of Factory Method pattern. You can create objects needed without new operator.

How to use Service Provider Interface?

4 components are needed to make use of Service Provider Interface:

  1. Service Provider Interface itself (which is just an interface of Java)
  2. Classes that implement the interface
  3. Files under META-INF/services of the Java project
  4. Service Access API ( java.util.ServiceLoader )

With Service Provider Interface, we can specify service classes to use just by tweaking a file under META-INF/services . We don’t need to modify implementation itself.

Let’s take a look at a concrete example (By the way, you can download the example here).

Service Provider Interface

Here’s an example of Service Provider Interface:

public interface Greeting  public String hello(); > 

There’s nothing mysterious. It’s just an interface of Java.

Classes that implement the interface

Let’s imagine that we have 2 classes that implements the Greeting interface that we’ve defined above.

public class GreetingEn implements Greeting  public String hello()  return "Hello."; > > 
public class GreetingJa implements Greeting  public String hello()  return "こんにちは"; > > 

They’re straightforward and self-explanatory enough 😉

Files under META-INF/services of the Java project

Actual file for this example is located: META-INF/services/Greeting . The file name Greeting needs to match with the name of the interface defined previously.

Let’s put records as follows:

Each line corresponds to the class that we expect the Service Access API to load.

Service Access API ( java.util.ServiceLoader )

java.util.ServiceLoader class works as an accessor of the classes for Service Provider Interface. See the following example:

import java.util.ServiceLoader; public class Main  public static void main(String[] args)  // ServiceLoader reads "META-INF/services/Greeting" and finds classes that matches the records // (and the classes must implement Greeting) ServiceLoaderGreeting> loader = ServiceLoader.load(Greeting.class); for (Greeting greeting: loader)  System.out.println(greeting.getClass()); System.out.println(greeting.hello()); > > > 

Here’s the result of the code above:

$ java *.java $ java Main class GreetingJa こんにちは class GreetingEn Hello. 

It’s apparent from the result that the iterator in the following block loads objects of both GreetingJa and GreetingEn . It’s because they’re listed in META-INF/services/Greeting .

 for (Greeting greeting: loader)  System.out.println(greeting.getClass()); System.out.println(greeting.hello()); > 

Summary

Let’s say, we want to provide a service in multiple regions. We may need to localize the service a little bit based on the regions. Do we want to build software for the service for each region? Probably not. Service Provider Interface can be one of the solutions in that situation. Once it’s built, we just need to replace the files under /META-INF/services as needed.

Yes, we can achieve the same thing with DI containers. One advantage of using java.util.ServiceLoader though is that this class is “Java Native” since Java 6, which means we don’t need to rely on 3rd party tools.

Relentlessly prune bullshit!

Источник

Оцените статью