Sometimes we may have more than one implementation and/or instance of a service to which we need to route requests. Routing may be controlled by a number of different factors, such as the request type, request arguments, runtime configuration, etc.

An implementation of such routing might look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SomeService {

  void someMethod();

}

public class RoutingSomeService implements SomeService {

  private Map<String, SomeService> delegates = ...

  private String activeDelegateId = ...

  public void someMethod() {

    SomeService delegate = delegates.get(activeDelegateId);

    if (delegate != null) {

      delegate.someMethod();

    }

    else {

      // XXX: throw runtime exception???

    }

  }

}

The common elements here are:

  • A collection of delegate services
  • A mechanism (e.g. key) for identifying the appropriate delegate for a request

Using interfaces we can create a pattern for supporting different types of service selection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public interface ServiceSelector<T> {

  T getService(Method method, Object[] args);

}

/**

 * A ServiceSelector that routes requests to the active service specified in an external configuration.

 */

public class ConfigurableServiceSelector implements ServiceSelector<SomeService> {

  private final Map<String, SomeService> services = ...

  private final Properties configuration = ...

  public SomeService getService(Method method, Object[] args) {

    return services.get(configuration.getProperty("someService.activeId"));

  }

}

/**

 * A ServiceSelector that supports routing of different methods based on an external configuration.

 */

public class MethodServiceSelector implements ServiceSelector<SomeService> {

  private final Map<Method, SomeService> services = ...

  public SomeService getService(Method method, Object[] args) {

    return services.get(method);

  }

}

...

So our implementation might then look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RoutingSomeService implements SomeService {

  private ServiceSelector<SomeService> selector = ...

  public void someMethod() {

    SomeService delegate = selector.getService(getClass().getMethod("someMethod"), new Object[] {});

    if (delegate != null) {

      delegate.someMethod();

    }

    else {

      // XXX: throw runtime exception???

    }

}

As you can see this is actually quite an ugly piece of code. However, we can avoid writing this code altogether with the help of Java’s proxy support.

Ergo Proxy

The use of proxies allows us to avoid writing the boilerplate code for service delegation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

public class ServiceInvocationHandler implements InvocationHandler {

    private final ServiceSelector<?> selector;

    public ServiceInvocationHandler(ServiceSelector<?> selector) {

        this.selector = selector;

    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        return method.invoke(selector.getService(method, args), args);

    }

}

public class ServiceConsumer {

  private final SomeService service;

  public ServiceConsumer(SomeService service) {

    this.service = service;

  }

  public static ServiceConsumer newInstance(SomeService service) {

    return new ServiceConsumer(service);

  }

  public static ServiceConsumer newInstance(ServiceSelector<SomeService> selector) {

    final InvocationHandler invocationHandler = new ServiceInvocationHandler(selector);

    return new ServiceConsumer((SomeService) Proxy.newProxyInstance(SomeService.class.getClassLoader(), new Class<?>[] {SomeService.class}, invocationHandler);

  }

}

Conclusion

Defining a standard interface for routing service requests provides us with a consistent and re-usable way of managing routing rules. Using proxies also ensures that service contracts are maintained and our client code doesn’t need to change.