Tuesday 5 August 2014

Spring Framework Annotation-based Configuration

With seemingly en masse transition of Java Spring framework users to annotation-based configuration, it sometimes can be quite frustrating to find yourself in a corner when a context configuration easily achievable with XML, can not be realized via annotations.
These are 2 examples:

  • configuring multiple service instances of the same class (not the prototype scope kind of multiplicity).
  • auto wiring of a service implementation based on a configuration parameter.
The first case:

Suppose there is a need to have 2 service beans of the same service implementation. (Of course, to have sense, the bean instances need to be distinct, for example by setting their instance variables to different values).
With an XML config, that can be easily achieved by declaring 2 beans with different ID values, for example:
<bean class=“DocumentServiceImpl” id=“documentService”/>
<bean class=“DocumentServiceImpl” id=“loggingDocumentService”>
    <property name=“shouldLogRequests” value=“true”/>
</bean>

Then, these beans can be configured for injection either in XML via the ref parameter:

<bean class=“DocumentServiceController”>
    <property name=“documentService” ref=“documentService”/>
    <property name=“loggingDocumentService” ref=“loggingDocumentService”/>
</bean>

Or alternatively, even autowiring like this:

public class DocumentServiceController {
 @Autowired
 @Qualifier("baseDocumentService")
 private DocumentService baseDocumentService;

 @Autowired
 @Qualifier("loggingDocumentService")
 private DocumentService loggingDocumentService;
}

The same simply cannot be done via type-level annotations (or, at least not as easily).
This is an annotation based configuration similar to the XML above:
@Service
public class BaseDocumentService implements DocumentService {
}

However, since the @Service annotation takes only a single String parameter, there is simply no way to instantiate a second bean of the same class assigning it a different name or id.

Even though this seems to be a conscious design choice of Spring framework architects (see below; note, the emphasis is the author's), it still can be maddeningly frustrating while looking for a solution.

From a Spring doc at 4.11.3 Fine-tuning annotation-based autowiring with qualifiers
For a fallback match, the bean name is considered as a default qualifier value. This means that the bean may be defined with an id "main" instead of the nested qualifier element, leading to the same matching result. However, note that while this can be used to refer to specific beans by name, @Autowired is fundamentally about type-driven injection with optional semantic qualifiers. This means that qualifier values, even when using the bean name fallback, always have narrowing semantics within the set of type matches; they do not semantically express a reference to a unique bean id. Good qualifier values would be "main" or "EMEA" or "persistent", expressing characteristics of a specific component - independent from the bean id (which may be auto-generated in case of an anonymous bean definition like the one above).

So, to comply with this design, the following approach should be used to achieve the goal of having multiple service bean instances of the same class:

  • Create a new implementation that extends the base service class.
  • Define a post construct method in this new class that sets parameters that would make a second instance to be different.

@Service(“loggingDocumentService”)
public class LoggingDocumentService extends DocumentServiceImpl {
   @PostConstruct
   public void postConstruct() {
       super.setShouldLogRequests(true);
   }
}

Okey, that is not too high price for switching to annotations-based configuration. It actually may promote a better object design, i.e. using subclassing to extend the behaviour of a class rather than using an instance variable and if-else statements for controlling its logic (though it’s not always possible).

Let’s now look at the second scenario.
Under this scenario, there are two different implementations of the same interface (see example below).
Suppose there is also a controller that should be configured via an environment property to use a particular service implementation. For instance, setting an environment configuration property, say document.service.caching.enabled=true, should result in Spring injecting the service implementation that provides document caching capabilities.

public class BaseDocumentService implements DocumentService {
}
public class CachingDocumentService extends BaseDocumentService {
}

public class DocumentServiceController {
    private DocumentService documentService;
}

When using XML configuration, this can be easily achieved by, by way of illustration, using a SpEL expression:

<bean class="BaseDocumentService" id="baseDocumentService" />
<bean class="CachingDocumentService" id="cachingDocumentService" />
<bean class="DocumentServiceController" id="documentServiceController">
    <property name="documentService" ref="#{'${document.service.caching.enabled}'=='yes' ? 'cachingDocumentService' : 'baseDocumentService'}" />
</bean>

With annotations-based Spring configuration, we would need to annotate an instance variable in the controller using the @Qualifier annotation:

@Controller
public class DocumentServiceController {
    @Autowired
    @Qualifier("documentService")
    private DocumentService documentService;
}

Had the @Qualifier annotation accepted property placeholders, that would be the end of the story.
Unfortunately, Spring architects decided not to resolve placeholders in the @Qualifier. Neither there is support for SpEL expressions.
Good news is that it's still possible to solve this task, bad news is that the solution is quite verbose.

First, we would need to implement a FactoryBean<T> interface:

@Component("documentServiceFactory")
@DependsOn({"baseDocumentService", "cachingDocumentService"})
public class DocumentServiceFactory implements FactoryBean<DocumentService> {
    @Autowired
    @Value("${document.service.caching.enabled}")
    private boolean enableDocumentCaching;

    @Autowired
    @Qualifier("baseDocumentService")
    private DocumentService baseDocumentService;

    @Autowired
    @Qualifier("cachingDocumentService")
    private DocumentService cachingDocumentService;

    @Override
    public DocumentService getObject() throws Exception {
        return enableDocumentCaching ? cachingDocumentService : baseDocumentService;
    }

    @Override
    public Class<?> getObjectType() {
        return DocumentService.class;
    } 
}

Second, the qualifier on the service reference in the controller needs to specify the factory bean rather than a service bean. Note though, the type of the reference remains of the service interface (i.e. not of the factory):

@Controller
public class DocumentServiceController {
    @Autowired
    @Qualifier("documentServiceFactory")
    private DocumentService documentService;
}

A drawback of this solution is that at runtime there still going to be 2 beans in the memory while only one will be served by the factory to the controller. However, considering that service bean implementations should not take up too much memory since they need to be thread-safe (i.e. limited number of instance variables), that drawback should not represent a tangible problem.
And forerunning a potential question: Why would it be desired to have an annotation-only Spring configuration? True, typically in medium and large applications it's not practical. But in small programs, like a job or utility, the program becomes tidy when everything is configured through annotations. The other main usage  is for JUnit tests. It's impractical to bring up the whole context of a large application for running a JUnit, so instead of creating a myriad of test-specific contexts, it's much productive to have JUnits fully configurable via annotations.

No comments:

Post a Comment