Alessandro C
Alessandro C

Reputation: 3560

How to use a dynamic implementation of an interface in Spring?

This question is intended to make an answer for a useful issue.

Suppose we have a Spring application with a @Controller, an interface and different implementations of that interface.

We want that the @Controller use the interface with the proper implementation, based on the request that we receive.

Here is the @Controller:

@Controller
public class SampleController {
    @RequestMapping(path = "/path/{service}", method = RequestMethod.GET)
    public void method(@PathVariable("service") String service){
        // here we have to use the right implementation of the interface
    }
}

Here is the interface:

public interface SampleInterface {
    public void sampleMethod(); // a sample method
}

Here is one of the possibile implementation:

public class SampleInterfaceImpl implements SampleInterface {
    public void sampleMethod() {
        // ...
    }
}

And here is another one:

Here is one of the possibile implementation:

public class SampleInterfaceOtherImpl implements SampleInterface {
    public void sampleMethod() {
        // ...
    }
}

Below I'll show the solution that I've found to use one of the implementations dynamically based on the request.

Upvotes: 1

Views: 3258

Answers (4)

kryger
kryger

Reputation: 13181

I'm not convinced with your solution because there's an implicit link between an HTTP parameter value and a bean qualifier. Innocent change of the bean name would result in a disaster that could be tricky to debug. I would encapsulate all the necessary information in one place to ensure any changes only need to be done in a single bean:

@Controller
public class SampleController {
    @Autowired
    private SampleInterfaceImpl basic;

    @Autowired
    private SampleInterfaceOtherImpl other;

    Map<String, SampleInterface> services;

    @PostConstruct
    void init() {
        services = new HashMap()<>;
        services.put("Basic", basic);
        services.put("Other", other);
    }

    @RequestMapping(path = "/path/{service}", method = RequestMethod.GET)
    public void method(@PathVariable("service") String service){
        SampleInterface sample = services.get(service);
        // remember to handle the case where there's no corresponding service
        sample.sampleMethod();
    }
}

Also, dependency on the ApplicationContext object will make it more complicated to test.

NB. to make it more robust I'd use enums instead of the "Basic" and "Other" strings.


However, if you know you'll only have two types of the service to choose from, this would be the "keep it simple stupid" way:

@Controller
public class SampleController {
    @Autowired
    private SampleInterfaceImpl basic;

    @Autowired
    private SampleInterfaceOtherImpl other;

    @RequestMapping(path = "/path/Basic", method = RequestMethod.GET)
    public void basic() {
        basic.sampleMethod();
    }

    @RequestMapping(path = "/path/Other", method = RequestMethod.GET)
    public void other() {
        other.sampleMethod();
    }
}

Upvotes: 0

Maxvader
Maxvader

Reputation: 113

Honestly I don't think the idea of exposing internal implementation details in the URL just to avoid writing some lines of code is good. The solution proposed by @kriger at least adds one indirection step using a key / value approach.

I would prefer to create a Factory Bean (to be even more enterprise oriented even an Abstract Factory Pattern) that will choose which concrete implementation to use. In this way you will be able to choose the interface in a separate place (the factory method) using any custom logic you wish. And you will be able to decouple the service URL from the concrete implementation (which is not very safe).

If you are creating a very simple service your solution will work, but in an enterprise environment the use of patterns is vital to ensure maintenability and scalability.

Upvotes: 0

Martin Frey
Martin Frey

Reputation: 10075

I solved that problem like this:

  • Let the interface implement a method supports(...) and inject a List<SampleInterface> into your controller.
  • create a method getCurrentImpl(...) in the controller to resolve it with the help of supports
  • since Spring 4 the autowired list will be ordered if you implement the Ordered interface or use the annotation @Order.

This way you have no need for using the ApplicationContext explicitly.

Upvotes: 0

Alessandro C
Alessandro C

Reputation: 3560

The solution I've found is this one.

First, we have to autowire the ApplicationContext in the @Controller.

@Autowired
private ApplicationContext appContext;

Second, we have to use the @Service annotation in the implementations of the interface. In the example, I give them the names "Basic" and "Other".

@Service("Basic")
public class SampleInterfaceImpl implements SampleInterface {
    public void sampleMethod() {
        // ...
    }
}

@Service("Other")
public class SampleInterfaceOtherImpl implements SampleInterface {
    public void sampleMethod() {
        // ...
    }
}

Next, we have to obtain the implementation in the @Controller. Here's one possible way:

@Controller
public class SampleController {
    @Autowired
    private ApplicationContext appContext;

    @RequestMapping(path = "/path/{service}", method = RequestMethod.GET)
    public void method(@PathVariable("service") String service){
        SampleInterface sample = appContext.getBean(service, SampleInterface.class);
        sample.sampleMethod();
    }
}

In this way, Spring injects the right bean in a dynamic context, so the interface is resolved with the properly inmplementation.

Upvotes: 4

Related Questions