Search code examples
javajava-8singletonclassloaderserviceloader

Loading Singleton Instance of a Class using Java ServiceLoader


  1. I have an Interface in my project.

  2. End users can implement this interface and can create their concrete implementations.

  3. All the users have been advised to follow singleton design pattern while creating the implementations.

  4. Users would store the implementations in a properties file.

  5. I use Java Reflection to load the classes as shown below.

  6. The above method has been very bad for the performance and hence I have decided to move the loading part to Java ServiceLoader instead for various reason.

  7. But, ServiceLoader requires a public no-args constructor to load the classes which is quite a hurdle.

  8. Is there a way to achieve the class loading using Java ServiceLoader with private constructor in place, or can I make the singleton implementations safe with a public no-args constructor.

I have read the documentation, and googled the probable solutions but found nothing substantial.

How classes are loaded now:

Class klass = Class.forName(klassName);
Method method = klass.getDeclaredMethod("getInstance");
Object object = method.invoke(null);

I just want to get rid of this, properties file and move on to Java ServiceLoader like I have moved for other classes which were not singletons.


Solution

  • There are at least a few different approaches you could use.

    Only Load One Provider Instance

    Likely the best solution is to ensure you only ever load a single instance of the provider yourself. This relieves the provider implementations of the responsibility.

    public final class ServiceRegistry {
    
      private static List<Service> services;
    
      public static synchronized List<Service> getServices() {
        if (services == null) {
          // The 'stream()' method and 'ServiceLoader.Provider` interface were
          // added in Java 9. On older versions you can use the 'iterator()` method
          // to do something similar.
          services = ServiceLoader.load(Service.class).stream()
              .map(ServiceLoader.Provider::get)
              .toList(); // 'Stream::toList()' added in Java 16
        }
        return services;
      }
    
      private ServiceRegistry() {}
    }
    

    You would then pass around these instances of Service to wherever you need them.  

    Make the Service a Factory

    Another option is to use some indirection by making the service a factory.

    public interface ServiceFactory {
    
      Service getSingletonInstance();
    }
    

    Which you would load as:

    static List<Service> getServices() {
      // The 'stream()' method and 'ServiceLoader.Provider` interface were
      // added in Java 9. On older versions you can use the 'iterator()` method
      // to do something similar.
      return ServiceLoader.load(ServiceFactory.class).stream()
          .map(ServiceLoader.Provider::get)
          .map(ServiceFactory::getSingletonInstance)
          .toList(); // 'Stream::toList()' added in Java 16
    }
    

    Any number of ServiceFactory instances can be created. It's the implementation of the factory that's responsible for returning the same instance of Service every time.

    Providers Deployed as Modules

    If on Java 9+ and the providers are deployed in non-automatic modules, then those providers can define a static no-argument method named provider.

    package com.example.provider;
    
    import com.example.spi.Service;
    
    public class Provider implements Service {
    
      // can make creating the instance lazy if you want
      private static final Provider INSTANCE = new Provider();
    
      // return type must be assignable to Service
      public static Provider provider() {
        return INSTANCE;
      }
    
      private Provider() {} // constructor can now be private if desired
    
      // ... service methods ...
    }
    

    Module descriptor:

    module provider {
      provides com.example.spi.Service with 
          com.example.provider.Provider;
    }
    

    Note the provider method, if it exists, will take precedence over the no-argument constructor in this case.