Service Locator Design Pattern in Java

The service locator design pattern is used when we want to locate various services using JNDI lookup. Considering high cost of looking up JNDI for a service, Service Locator pattern makes use of caching technique. For the first time, a service is required, Service Locator looks up in JNDI and caches the service object. Further lookup or same service via Service Locator is done in its cache which improves the performance of application to great extent The Service Locator Pattern is to transparently locate business components and services in a uniform manner.
Spring Framework internally uses Service Locator pattern to manage Dependency Injection.These days, Service Locator isn’t of much use anymore, except in very small proprietary projects. For the rest, use a good dependency injection framework like spring or guice.
Let's discuss how Service Locator Pattern transparently locates business components and services in a uniform manner.
Reading theory is always boring so maybe we can jump to source code section to understand this pattern step by step implementation.
This pattern is divided into a number of sections for simplicity like problem, forces, solution etc.
Table of contents
Problem
Forces
Solution
Structure - Class Diagram, Sequence Diagram
Participants and Responsibilities
Implementation
Consequences
Applicability
References

Problem

(Problem section describes the design issues faced by the developer)
You want to transparently locate business components and services in a uniform manner.

Forces

(This section describes Lists the reasons and motivations that affect the problem and the solution. The list of forces highlights the reasons why one might choose to use the pattern and provides a justification for using the pattern)
  • You want to use the JNDI API to look up and use business components, such as enterprise beans and JMS components, and services such as data sources.
  • You want to centralize and reuse the implementation of lookup mechanisms for J2EE application clients.
  • You want to encapsulate vendor dependencies for registry implementations and hide the dependency and complexity from the clients.
  • You want to avoid performance overhead related to initial context creation and service lookups.
  • You want to reestablish a connection to a previously accessed enterprise bean instance, using its Handle object.

Solution

(Here solution section describes the solution approach briefly and the solution elements in detail)
Use a Service Locator to implement and encapsulate service and component lookup. A Service Locator hides the implementation details of the lookup mechanism and encapsulates related dependencies.
Application clients can reuse the Service Locator to reduce code complexity, provide a single point of control, and improve performance by providing a caching facility.

Structure

Let's use UML class diagram to show the basic structure of the solution and the UML Sequence diagram in this section present the dynamic mechanisms of the solution. 
Below is the class diagram representing the relationships for the Service Locator pattern.

Class diagram

Sequence Diagram

Participants and Responsibilities

Client - Client represents a client of the Service Locator that needs to locate and access a component or service in the business or integration tier.
ServiceLocator - The ServiceLocator encapsulates the API lookup (naming) services, vendor dependencies, lookup complexities, and business object creation, and provides a simple interface to clients. This reduces the client’s complexity and increases reuse.
Cache - Cache represents an optional ServiceLocator to hold onto references that have been previously looked up. The sole purpose of using the Cache is to optimize the ServiceLocator by reducing redundant lookups.
InitialContext - The InitialContext object is the starting point in the lookup and creation process. Service providers provide the context object, which varies depending on the type of Target looked up by the ServiceLocator.
Target - Target represents the service or component, in the business or integration tiers, that the Client is looking up using the ServiceLocator.
RegistryService - RegistryService represents the registry implementation that holds the references to the services or components that are registered as service providers for Clients.

Implementation

(This section includes example implementations and code listings for the patterns and the strategies)
Let's create source code step by step with reference to the class diagram.
Step 1 : This is parent service interface which we will use to create our services. All services will have a service name, uniqueId, and execution workflow.
public interface Service {

  /*
   * The human readable name of the service
   */
  String getName();

  /*
   * Unique ID of the particular service
   */
  int getId();

  /*
   * The workflow method that defines what this service does
   */
  void execute();
}
Step 2: This is a single service implementation of a sample service. This is the actual service that will process the request. The reference for this service is to be looked upon in the JNDI server that can be set in the web.xml deployment descriptor.
public class ServiceImpl implements Service {

  private final String serviceName;
  private final int id;

  /**
   * Constructor
   */
  public ServiceImpl(String serviceName) {
    // set the service name
    this.serviceName = serviceName;

    // Generate a random id to this service object
    this.id = (int) Math.floor(Math.random() * 1000) + 1;
  }

  @Override
  public String getName() {
    return serviceName;
  }

  @Override
  public int getId() {
    return id;
  }

  @Override
  public void execute() {
    System.out.println("Service " + getName() + " is now executing with id " + getId());
  }
}
Step 3: Create InitialContext for JNDI lookup.
For JNDI lookup of services from the web.xml. Will match the name of the service name that is being requested and return a newly created service object with the name.
public class InitContext {

  /**
   * Perform the lookup based on the service name. The returned object will need to be casted into a
   * {@link Service}
   *
   * @param serviceName a string
   * @return an {@link Object}
   */
  public Object lookup(String serviceName) {
    if (serviceName.equals("jndi/serviceA")) {
      System.out.println("Looking up service A and creating new service for A");
      return new ServiceImpl("jndi/serviceA");
    } else if (serviceName.equals("jndi/serviceB")) {
      System.out.println("Looking up service B and creating new service for B");
      return new ServiceImpl("jndi/serviceB");
    } else {
      return null;
    }
  }
}
Step 4: Create Cache.
The service cache implementation which will cache services that are being created. On the first hit, the cache will be empty and thus any service that is being requested, will be created fresh and then placed into the cache map. On next hit, if same service name will be requested, it will be returned from the cache.
public class ServiceCache {

  private final Map<String, Service> serviceCache;

  public ServiceCache() {
    serviceCache = new HashMap<>();
  }

  /**
   * Get the service from the cache. null if no service is found matching the name
   *
   * @param serviceName a string
   * @return {@link Service}
   */
  public Service getService(String serviceName) {
    Service cachedService = null;
    for (String serviceJndiName : serviceCache.keySet()) {
      if (serviceJndiName.equals(serviceName)) {
        cachedService = serviceCache.get(serviceJndiName);
        System.out.println("(cache call) Fetched service " + cachedService.getName() + "("
            + cachedService.getId() + ") from cache... !");
      }
    }
    return cachedService;
  }

  /**
   * Adds the service into the cache map
   *
   * @param newService a {@link Service}
   */
  public void addService(Service newService) {
    serviceCache.put(newService.getName(), newService);
  }
}
Step 5: Create Service Locator.
The service locator module will fetch service from the cache, otherwise creates a fresh service and update cache.
public final class ServiceLocator {

  private static ServiceCache serviceCache = new ServiceCache();

  private ServiceLocator() {
  }
  public static Service getService(String serviceJndiName) {
    Service serviceObj = serviceCache.getService(serviceJndiName);
    if (serviceObj != null) {
      return serviceObj;
    } else {
      /*
       * If we are unable to retrive anything from cache, then lookup the service and add it in the
       * cache map
       */
      InitContext ctx = new InitContext();
      serviceObj = (Service) ctx.lookup(serviceJndiName);
      if (serviceObj != null) { // Only cache a service if it actually exists
        serviceCache.addService(serviceObj);
      }
      return serviceObj;
    }
  }
}
Step 6: Let's test this pattern using the main method.
public class TestServiceLocatorPattern{

  /**
   * Program entry point
   * 
   * @param args command line args
   */
  public static void main(String[] args) {
    Service service = ServiceLocator.getService("jndi/serviceA");
    service.execute();
    service = ServiceLocator.getService("jndi/serviceB");
    service.execute();
    service = ServiceLocator.getService("jndi/serviceA");
    service.execute();
    service = ServiceLocator.getService("jndi/serviceA");
    service.execute();
  }
}

Applicability

The service locator pattern is applicable whenever we want to locate/fetch various services using JNDI which, typically, is a redundant and expensive lookup. 
The service Locator pattern addresses this expensive lookup by making use of caching techniques ie. for the very first time a particular service is requested, the service Locator looks up in JNDI, fetched the relevant service and then finally caches this service object. Now, further lookups of the same service via Service Locator is done in its cache which improves the performance of the application to a great extent.

Typical Use Case

  • when network hits are expensive and time-consuming
  • lookups of services are done quite frequently
  • a large number of services are being used

Consequences

  • Abstracts complexity
  • Provides uniform service access to clients
  • Facilitates adding EJB business components
  • Improves network performance
  • Improves client performance by caching

References

Related Patterns

Comments