Thursday, 28 March 2013

Java ServiceLoader Example

Defining an API and developing the corresponding implementation has become an uber mainstream practice when developing large Java applications. Modularization is such an useful design principle, especially to avoid spaghetti code, for testing and debugging, and for re-implementation of old code.

Many developers seek to separate API interfaces and abstract classes from their implementation in separate packages. Yet, this almost always leads to cycles between java packages (api refers to impl, and vice-versa). There has been many frameworks, such as OSGi, supporting modularization, but the documentation has not always been good and complete. Many developers have struggled with class loading issues too.

Fortunately, Java has delivered the ServiceLoader utility since release 6. The example described in this post is available from Github in the Java-ServiceLoader directory. It is inspired from here.

Service & Implementation

We define a simple interface and its implementation:
package com.service.API;

public interface MyService {
    long getTime();
}


package com.service.API.Impl;

import com.service.API.MyService;

public final class MyServiceImpl implements MyService {

    public MyServiceImpl() { };

    @Override
    public long getTime() {
        return System.currentTimeMillis();
    }

}
The API and the implementation are located in different packages. Only the implementation refers to the API. The implementation must have a public parameterless constructor.

Service Loading

Next, we declare the implementation in a file having the fully qualified name of the API under the META-INF/services directory in the .jar:


The file contains fully qualified name of the implementation:

    com.service.API.Impl.MyServiceImpl

Next, we load the service using the ServiceLoader:
public class MyApp {

    public static <T> T loadService(Class<T> api) {

        T result = null;

        ServiceLoader<T> impl = ServiceLoader.load(api);

        for (T loadedImpl : impl) {
            result = loadedImpl;
            if ( result != null ) break;
        }

        if ( result == null ) throw new RuntimeException(
            "Cannot find implementation for: " + api);

        return result;

    }

    public static final MyService myService = loadService(MyService.class);

    public static void main(String[] args) {

        System.out.println("Time is: " + myService.getTime());

    }

}
The service implementation is loaded with the loadService static method. The main method prints the current time in milliseconds. It is that simple!

7 comments:

  1. i'm thinking whether the author is in fact java developer.

    Looking at the fully qualified names of the classes, such as com.service.API.Impl.MyServiceImpl makes me feel really uncomfortable (irritated, i would say) - as if there was a API class containing inner class called Impl, containing another inner class MyServiceImpl.

    Is there a reason why code conventions http://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html are disobeyed?

    ReplyDelete
  2. This is only a code example to illustrate a concept... Feel free to apply any code name conventions you like!

    ReplyDelete
  3. Hi Jérôme,
    You mention "The implementation must have a public parameterless constructor."

    Could you please explain why this is required?

    Thanks

    ReplyDelete
    Replies
    1. Sure. After Java loads the code for the implementation of the service API, it needs to create a new() instance of the implementing object. But, it does not know anything about potential parameters it could/should pass to the constructor. If the implementation did not have a parameterless constructor, calling new() would trigger a runtime exception. Hence, the need for this parameterless constructor so Java can call it when new() is called to create the implementing object instance.

      Delete
    2. Thanks for the explanation. A follow up question:
      Consider the javadoc:
      http://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html#jls-13.1-110-F.2.

      It says:
      "In addition, the constructor of a non-private inner member class must be compiled such that it has as its first parameter, an additional implicit parameter representing the immediately enclosing instance (§8.1.3)."

      So does that mean I cannot externalize a inner class. Please consider the following code:

      import java.io.*;

      class Y {
      class ABC {
      ABC() {
      System.out.println("ABC Constructor");
      }
      public void writeExternal(ObjectOutput out)
      throws IOException {
      System.out.println("ABC.writeExternal");
      }
      public void readExternal(ObjectInput in)
      throws IOException, ClassNotFoundException {
      System.out.println("ABC.readExternal");
      }
      }

      public void foo() throws IOException, ClassNotFoundException {
      System.out.println("Constructing objects:");
      ABC abc = new ABC();
      ObjectOutputStream o = new ObjectOutputStream(
      new FileOutputStream("InnerClass.out"));
      System.out.println("Saving objects:");
      o.writeObject(abc);
      o.close();
      // Now get them back:
      ObjectInputStream in = new ObjectInputStream(
      new FileInputStream("InnerClass.out"));
      System.out.println("Recovering abc:");
      // OOPS! Throws an exception:
      abc = (ABC)in.readObject();
      }
      }

      public class InnerClass {
      public static void main(String[] args) throws IOException, ClassNotFoundException {
      System.out.println("Hello World\n");
      Y y = new Y();
      System.out.println(y);
      y.foo();
      }
      }

      While running the program I get the following error:

      Hello World

      Y@6bc7c054
      Constructing objects:
      ABC Constructor
      Saving objects:
      Exception in thread "main" java.io.NotSerializableException: Y$ABC
      at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1180)
      at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:346)
      at Y.foo(InnerClass.java:24)
      at InnerClass.main(InnerClass.java:40)

      How do I externalize ABC.

      Thanks

      Delete
    3. I am sorry if the code and error message is not in format as it shows up in your blog. If there is a way, please do let me know.

      Delete
    4. As per described in the documentation you are pointing at, the ABC constructor does not have an instance of Y as a parameter... If you made ABC a static class of Y, you would not have an issue. You can also ask such questions on StackOverflow.com too.

      Delete