Service Locator Pattern in Spring

Service Locator Pattern in Spring

3 Comments

In this article, we’ll to learn how to implement the service locator Design Pattern in Spring.

  • We’ll begin with a brief introduction.
  • Next, we’ll introduce an example that benefits from using the pattern.
  • And finally, we’ll work through an implementation in Spring.

Introduction: Service Locator Pattern

When we begin working with the Spring framework, we run into concepts such as Inversion of Control (IoC), and dependency injection (constructor, setter and field injection) as a way to achieve IoC. Additionally, Spring provides a form of IoC via the service locator pattern.

The service locator pattern has the same goal as dependency injection. It removes the dependency that a client has on the concrete implementation. The following quote from Martin Fowler’s article summaries the core idea:
    “The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need. So a service locator for this application would have a method that returns a ‘service’ when one is needed.”

Design Components

The following class diagram represents the relationship between the design components of service locator:
service-locator-pattern-overview

  • Client: Consumer that requires the service at runtime.
  • Service Locator: Service locator is responsible for returning the service on-demand to the client. It abstracts the lookup or creation of the service.
  • Initial Context: It creates, registers and caches the service. This is the starting point of the lookup and creation.
  • Service Factory: The service factory provides lifecycle management for the service with support to create, lookup, or remove a service.
  • Service: Concrete implementation of the service desired by the client.

We can use the service locator pattern to decouple the client from the concrete implementation when the dependency is on demand or requires a lookup at runtime. For a more rigorous treatment of the service locator pattern, refer Core J2EE Patterns – service locator.

Example

Let’s assume that we have an application that ingests data from various sources. We have to parse different content types such as Comma-separated Values (CSV), Javascript Object Notation (JSON), and so on. As per the product roadmap, we need to support at least JSON and CSV for the minimal viable product (MVP).

After much deliberation, we arrive at a design that seems reasonable. First, we declare an enumeration called ContentType.

public enum ContentType {

  JSON,
  CSV
}

Next, we’ll define an interface called Parser and add a method parse that takes a file as input and returns a list of records.

public interface Parser {

  List parse(Reader r);
}

Now, let’s provide implementations for the different content types by extending the Parser interface. These implementation classes are annotated with @Component and can be autowired in the service to invoke the parser.

@Component
public class CSVParser implements Parser { 

  @Override
  public List parse(Reader r) { .. }
}

@Component
public class JSONParser implements Parser {

  @Override
  public List parse(Reader r) { .. }
}

Finally, we’ll implement the client that invokes the parsers, based on the content type.

@Service
public class Service {

  private Parser csvParser, jsonParser;

  @Autowired
  public Service(Parser csvParser, Parser jsonParser) {
    this.csvParser = csvParser;
    this.jsonParser = jsonParser;
  }

  public List getAll(ContentType contentType) {
    ..
    
    switch (contentType) {
      
      case CSV:
        return csvParser.parse(reader);
 
      case JSON:
        return jsonParser.parse(reader);

      ..
    }
  }

  ..
}

We are ready to ship the feature. If only we were that lucky!

Back to Drawing Board

The product manager has come up with a new requirement to support Extensible Markup Language (XML) content as well. And of course, we need to implement this feature before we can release the MVP. Instead of going back and hammering away at the keyboard, we decide to take a step back. We have a quick chat with the product manager and realize that we would need to keep adding support for new content types.

We review our design and look at our code. The problems are becoming clear now:

  1. The switch (or an equivalent if-else) block will soon become unwieldy, with every new content type.
  2. The service (client) is aware of all the concrete implementations of Parser. It needs to be updated every time a new parser is added.

The client has a tight coupling to the different parsers. Although the Spring container handles creation, the client is still responsible for:

  • maintaining a registry of all the available parsers and
  • looking up the correct implementation at runtime.

So, we need an abstraction that can provide a registry and ability to look up an implementation. Also, it should leverage Spring to avoid complicating things further.

Service Locator in Spring

Spring’s ServiceLocatorFactoryBean is a FactoryBean implementation that takes a service locator interface, Service Factory in service locator parlance, and returns a service requested by the client. It encapsulates all the design components of the service locator pattern and provides a clean API to the client to fetch objects on demand.

First, let’s define our service locator interface ParserFactory. It has a method that takes a content type argument and returns objects of type Parser.

public interface ParserFactory {

  Parser getParser(ContentType contentType);
}

Next we’ll configure the ServiceLocatorFactoryBean to use  ParserFactory as the service locator interface.

@Configuration
public class ParserConfig {

  @Bean("parserFactory")
  public FactoryBean serviceLocatorFactoryBean() {
    ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
    factoryBean.setServiceLocatorInterface(ParserFactory.class);
    return factoryBean;
  }
}

Behind The Scenes

The ServiceLocatorFactoryBean is a FactoryBean. It is used as a factory for Parser objects, and not as a bean itself. The Spring container creates a dynamic proxy at runtime that delegates to the underlying BeanFactory for returning the beans.

The sequence diagram below shows us the story behind the abstraction:
service-locator-in-spring

Before we refactor our service to leverage the ParserFactory, let’s talk about the factory method getParser. A factory method argument must be a string, but can also be an integer or an enumeration. In the latter cases, the bean factory resolves the bean by stringify-ing (for example via toString) the value.
So, we’ll align the bean name of our parsers such that the bean factory can resolve them via the ContentType enumeration.

@Component("CSV")
public class CSVParser implements Parser { .. }

@Component("JSON")
public class JSONParser implements Parser { .. }

@Component("XML")
public class XMLParser implements Parser { .. }

Note that now we have extended the application to parse XML.

public enum ContentType {

  JSON,
  CSV,
  XML
}

Finally, we’ll clean up the Service.

@Service
public class Service {

  private ParserFactory parserFactory;

  @Autowired
  public Service(ParserFactory parserFactory) {
    this.parserFactory = parserFactory;
  }

  public List getAll(ContentType contentType) {
    ..
    
    return parserFactory
        .getParser(contentType)    // gets the desired bean by content type
        .parse(reader);
  }

  ..
}

Let’s see if we have made any progress.

  • With our new approach, we are able to remove the registry from the client. We have autowired ParserFactory that takes care of providing the parsers on demand.
  • There’s no switch block! Hence, the client is no longer responsible for looking up a parser.

We have successfully achieved our objectives. We have also future-proofed our design as we can add new parsers without modifying the client.

The complete code is available on GitHub.

Bonus: Customizing the Bean Name

If we wish to have better control over the bean names, we can simply override the toString method of the enumeration and provide custom names.

public enum ContentType {

  JSON(TypeConstants.JSON_PARSER),

  CSV(TypeConstants.CSV_PARSER),

  XML(TypeConstants.XML_PARSER);

  private final String parserName;

  ContentType(String parserName) {
    this.parserName = parserName;
  }
  
  @Override
  public String toString() {
    return this.parserName;
  }

  public interface TypeConstants {
    
    String CSV_PARSER = "csvParser";
    String JSON_PARSER = "jsonParser";
    String XML_PARSER = "xmlParser"; 
  }
}

Also, we’ll refer the same in the implementation classes.

@Component(TypeConstants.CSV_PARSER)
public class CSVParser implements Parser { .. }

@Component(TypeConstants.JSON_PARSER)
public class JSONParser implements Parser { .. }

@Component(TypeConstants.XML_PARSER)
public class XMLParser implements Parser { .. }

Summary

We have implemented a nifty way to extend Spring’s inversion of control by using the service locator pattern. It helped us address a use case where dependency injection didn’t offer an optimal solution. That said, dependency injection is still the preferred option and service locator should not be used to replace dependency injection in most situations.

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like