In this post, I am going to discuss the ServiceLoader API introduced in JSE 6. This is an API to load services where the definition and implementation of the services are decoupled. For example, I might define a service via an interface named Logger which has a method named log(String s). I might later write an implementation named FileLog which will log the messages to a file. Later on, someone else might write a DBLog which will log the messages to a database. Here, there are different implementations of the service available and as an user, I can choose to use one of the available implementations.
The important point to note is that, the person who defines the service need not be the one who provides implementations of the services. There are several such usages in JDK itself - for example, the ResultSet interface just defines the methods - different DB vendors provide implementations of this interface.
Now, the question is, how to load and use the implementations without a direct dependency on the implementations? That is where the ServiceLoader API comes in. Let us see a simple example to see how this works:
Let us first define the interface which is in a separate project:
public interface ServiceInterface {
public String serviceMethod();
}
Now, let us write the implementation in a different project (this project would refer the interface definition project in its library or in the case of Maven will need to declare a dependency):
public class ServiceProviderImpl1 implements ServiceInterface {
@Override
public String serviceMethod() {
return "Sun";
}
}
The above class is a sample implementation named 'impl1'. Likewise, let us write a different implementation (in a different project which again depends on the interface project):
public class ServiceProviderImpl2 implements ServiceInterface {
@Override
public String serviceMethod() {
return "Moon";
}
}
We have two different implementations of the interface now.
Problem:
Let us try and develop the client which will actually use one of the implementations:
import java.util.ServiceLoader;
import <blog package>;
public class App {
public static void main( String[] args ) {
ServiceInterface si;
si = new ???();
si.serviceMethod();
}
}
In OOPS, the general guideline is to program to interfaces. So, we declare a variable of the type ServiceInterface (and not of implementation). And then when creating an object, we have to create an instance of one of the implementations. Now, whichever implementation we decide to use, we have to hardcode the class name here (in place of the question mark).
What if we want to change the implementation tomorrow? We will be forced to make a code change.
Note: In the above case, this client project has to include both the interface and impl1 in the referenced libraries (dependencies in Maven).
Solution:
The solution is to use the ServiceLoader API. First let us look at the client code:
import java.util.ServiceLoader;
import com.blogspot.javanbswing.sp.api.ServiceInterface;
public class ClientTest {
public static void main( String[] args ) {
ServiceLoader<ServiceInterface> serviceLoader
= ServiceLoader.load(ServiceInterface.class);
ServiceInterface api = serviceLoader.iterator().next();
System.out.println("from " + api.serviceMethod());
}
}
Now, we are using the ServiceLoader API. This has a load method which will search for a implementation and use it. This method actually returns a collection of available implementations. But in our case, as we have included only the impl1 dependency, the search will return that implementation. And that will be the one used.
But, how does the API search and find the implementation? For this, the implementation jars should follow a mechanism called as provider-configuration file. This file is a simple text file which should be placed under META-INF/services folder. The file should be named exactly as the fully qualified name of the service interface.
For example, in our case, the service interface name is 'ServiceInterface'. And the fully qualified name is 'com.blogspot.javanbswing.sp.api.ServiceInterface'. We should create a file with this name. This file should in turn contain the fully qualified class name of the implementation. For example in impl1, the implementation class name is 'ServiceProviderImpl1' and the fully qualified name is 'com.blogspot.javanbswing.sp.impl1.ServiceProviderImpl1'. This is the only text that should be present in this file. Same should be done for impl2.
So, when we include the 'impl1' as the dependency for the client project, it will look for this file under the 'META-INF/services' folder in the jar and then pickup the implementation class name from within the text file.
Now, run the client program and you can see that 'Sun' is displayed. But, how do we switch the implementation? Simply change the dependency to impl2 (in the client's pom.xml). Now, run the program again and you should see 'Moon' displayed.
In production, we will simply place the implementation jar based on what we want (place either the impl1.jar or impl2.jar in the classpath and we are done). So, without any code change in the client program, we can switch to a different implementation. Simple and cool!
The complete source code including the client is available in my github repo.
The important point to note is that, the person who defines the service need not be the one who provides implementations of the services. There are several such usages in JDK itself - for example, the ResultSet interface just defines the methods - different DB vendors provide implementations of this interface.
Now, the question is, how to load and use the implementations without a direct dependency on the implementations? That is where the ServiceLoader API comes in. Let us see a simple example to see how this works:
Let us first define the interface which is in a separate project:
public interface ServiceInterface {
public String serviceMethod();
}
Now, let us write the implementation in a different project (this project would refer the interface definition project in its library or in the case of Maven will need to declare a dependency):
public class ServiceProviderImpl1 implements ServiceInterface {
@Override
public String serviceMethod() {
return "Sun";
}
}
The above class is a sample implementation named 'impl1'. Likewise, let us write a different implementation (in a different project which again depends on the interface project):
public class ServiceProviderImpl2 implements ServiceInterface {
@Override
public String serviceMethod() {
return "Moon";
}
}
We have two different implementations of the interface now.
Problem:
Let us try and develop the client which will actually use one of the implementations:
import java.util.ServiceLoader;
import <blog package>;
public class App {
public static void main( String[] args ) {
ServiceInterface si;
si = new ???();
si.serviceMethod();
}
}
In OOPS, the general guideline is to program to interfaces. So, we declare a variable of the type ServiceInterface (and not of implementation). And then when creating an object, we have to create an instance of one of the implementations. Now, whichever implementation we decide to use, we have to hardcode the class name here (in place of the question mark).
What if we want to change the implementation tomorrow? We will be forced to make a code change.
Note: In the above case, this client project has to include both the interface and impl1 in the referenced libraries (dependencies in Maven).
Solution:
The solution is to use the ServiceLoader API. First let us look at the client code:
import java.util.ServiceLoader;
import com.blogspot.javanbswing.sp.api.ServiceInterface;
public class ClientTest {
public static void main( String[] args ) {
ServiceLoader<ServiceInterface> serviceLoader
= ServiceLoader.load(ServiceInterface.class);
ServiceInterface api = serviceLoader.iterator().next();
System.out.println("from " + api.serviceMethod());
}
}
Now, we are using the ServiceLoader API. This has a load method which will search for a implementation and use it. This method actually returns a collection of available implementations. But in our case, as we have included only the impl1 dependency, the search will return that implementation. And that will be the one used.
But, how does the API search and find the implementation? For this, the implementation jars should follow a mechanism called as provider-configuration file. This file is a simple text file which should be placed under META-INF/services folder. The file should be named exactly as the fully qualified name of the service interface.
For example, in our case, the service interface name is 'ServiceInterface'. And the fully qualified name is 'com.blogspot.javanbswing.sp.api.ServiceInterface'. We should create a file with this name. This file should in turn contain the fully qualified class name of the implementation. For example in impl1, the implementation class name is 'ServiceProviderImpl1' and the fully qualified name is 'com.blogspot.javanbswing.sp.impl1.ServiceProviderImpl1'. This is the only text that should be present in this file. Same should be done for impl2.
So, when we include the 'impl1' as the dependency for the client project, it will look for this file under the 'META-INF/services' folder in the jar and then pickup the implementation class name from within the text file.
Now, run the client program and you can see that 'Sun' is displayed. But, how do we switch the implementation? Simply change the dependency to impl2 (in the client's pom.xml). Now, run the program again and you should see 'Moon' displayed.
In production, we will simply place the implementation jar based on what we want (place either the impl1.jar or impl2.jar in the classpath and we are done). So, without any code change in the client program, we can switch to a different implementation. Simple and cool!
The complete source code including the client is available in my github repo.
Good article
ReplyDeleteThanks Deiveehan.
DeleteGreat! This is a good information of the computer services articles and really like your site, Please keep sharing more and more information.
ReplyDeleteCross Border Trucking & Vendor Assembly services
Thanks John.
Delete