Using the SPI engine to create extensions

    The architecture of most Java (and not only) applications today provides for the possibility of expanding functionality through various kinds of magical effects on the code. Recently, it has also become possible if you use some kind of fashionable framework or IoC container . But what if the application is long-lived and too complex to translate it to use any framework?

    In the last application that I worked with, at that time an unknown SPI bike mechanism was implemented , which looked in text files for META-INF / services /and from there took the name of the desired class that implements this interface, then this class was used as an extension. After searching the Internet, I found out that the Service Provider Interface (SPI) is a software mechanism for supporting plug-in components and that this mechanism has been used for quite some time in the Java Runtime Environment (JRE) , for example, in Java Database Connectivity (JDBC) :
    ps = Service.providers(java.sql.Driver.class);
    try {
      while (ps.hasNext()) {
        ps.next();
      }
    } catch (Throwable t) {
      // Do nothing
    }
    


    Thanks to this code, applications no longer need the construction of Class.forName ()(although they will work with it), JDBC drivers will be loaded automatically the first time you access the methods of the DriverManager class .

    The SPI mechanism is also used in 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.

    How it works?


    The whole point is the separation of logic into service (Service) and providers (Service Providers). Links to providers are stored in extension jars in a text file (UTF-8) META-INF / services /, in each line, the fully qualified class name of the provider. Blank lines and comments (starting with the # character) are ignored. Restrictions on providers: they must implement the interface or inherit from the service class and have a default constructor (zero-argument public constructor).

    The main application for obtaining a list of providers can use the java.util.ServiceLoader utility , which is part of the Java SE 6 API , which works as follows: User code requests a configuration loader for a particular service, the loader loads providers from the configuration as needed and saves them in the cache . It is also possible to clear the cache and reload the configuration. Earlier versions of Java SE have a similar utility.




    sun.misc.Service , works on the same principle, but is part of Sun Oracle's proprietary software and may be removed in future Java SE releases.

    Usage example


    For example, we have a program that searches for music on a computer and displays the result sorted by name on the screen.
    public class MusicFinder {
      public static List getMusic() {
        //some code
      }
    }
    public class ReportRenderer {
      public void generateReport() {
        final List music = findMusic();
        for (String composition : music) {
          System.out.println(composition);
        }
      }
      public List findMusic() {
        final List 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();
      }
    }
    


    At some point in time, we realized the importance of this program for society and decided to share it with our friends. Friends used the service and decided that something was missing. Can output to a separate file? But then you have to rewrite all this cool code. No need, you can use the SPI mechanism.

    For example, create a plugin for our super program:
    public class FileReportRenderer extends ReportRenderer {
      @Override
      public void generateReport() {
        final List 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();
        }
      }
    }
    


    Put the following in META-INF / services / com.example.ReportRenderer :
    com.example.FileReportRenderer
    


    Let's make the source program extensible:
    public class ReportRenderer {
      //...
      public static ReportRenderer getInstance() {
        final Iterator providers = ServiceLoader.load(ReportRenderer.class).iterator();
        if (providers.hasNext()) {
          return providers.next();
        }
        return new ReportRenderer();
      }
      //...
    }
    


    At startup, the application, as before, will display all the music found on the screen. But if we put the newly created extension jar in the classpath , we will end up with a music.txt file containing the search results.

    Now it's time to play around with MusicFinder . Let's make it extensible too. To do this, change the class to the interface:
    public interface MusicFinder {
      List getMusic();
    }
    


    Add the implementation in the main module:
    public class DummyMusicFinder implements MusicFinder {
      public List getMusic() {
        return Collections.singletonList("From DummyMusicFinder...");
      }
    }
    


    Extension support in ReportRenderer :
    public class ReportRenderer {
      //...
      public List findMusic() {
        final List music = new ArrayList();
        for (final MusicFinder finder : ServiceLoader.load(MusicFinder.class)) {
          music.addAll(finder.getMusic());
        }
        Collections.sort(music);
        return music;
      }
      //...
    }
    


    As in the case of ReportRenderer, add the text file META-INF / services / com.example.MusicFinder , containing:
    com.example.DummyMusicFinder
    


    Again, the result of the first program has not changed. Now the extension. Here we will make two implementations of MusicFinder :
    public class ExtendedMusicFinder implements MusicFinder {
      public List getMusic() {
        return Collections.singletonList("From ExtendedMusicFinder...");
      }
    }
    public class MyMusicFinder implements MusicFinder {
      public List getMusic() {
        return Collections.singletonList("From MyMusicFinder...");
      }
    }
    


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


    Well, that’s all, the program supporting the extensions is ready, now with the extension in the classpath , it will list:
    From DummyMusicFinder ...
    From ExtendedMusicFinder ...
    From MyMusicFinder ...
    


    Sample sources can be found here .

    Conclusion


    The above example is far from perfect, and I do not pretend to be the author of the world's coolest music search engine. I also do not call for the fanatical use of this mechanism, since it is not applicable everywhere, and I consider the use of an IoC container to be a more beautiful solution, but still this approach may be useful in some places. Thank you for taking the time to read the article.

    Literature


    Plug-in
    the Service Provider Interface
    the Service Provider
    the Service Provider Interface: Creating Company the Extensible the Java the Applications
    the Service the Loader

    Also popular now: