Monosoul's Dev Blog A blog to write down dev-related stuff I face
How to customize dependency injection in Spring

How to customize dependency injection in Spring (Part 2)

I believe that the decorator pattern is one of the most powerful design patterns out there. It is the rule “composition > inheritance” at it’s best: it makes your classes easily testable, it allows to decompose huge classes into smaller ones, and it makes your code reusable and modular. In fact, Spring Framework relies heavily on this pattern. Unfortunately, I think that Spring doesn’t provide a really convenient way to configure decorators with it. This is the second part of how to customize dependency injection in Spring, and today we’re going to see how to make Spring’s DI respect decorators.

In the first part of this article, we’ve developed a service that provides fortunes and horoscopes. We’re going to continue using it as a reference for this part as well.

Also, if you haven’t seen it yet, I strongly recommend watching Evgeny’s Borisov “Spring the Ripper” series part 1 and part 2 before going forward.

Introduction

In my team we use the decorator pattern a lot. Aspect oriented programming (AOP) is based upon this pattern. Proxy classes created by Spring are basically decorators, so most probably you’re also using decorators implicitly if you use Spring. Though, if you want to define decorators explicitly and you use package scan to configure the application context, you’ll face an issue. You will have multiple implementations of the same interface and Spring will throw the NoUniqueBeanDefinitionException because of that. I.e. Spring won’t be able to figure out where and how to inject the beans. There are multiple ways of solving this issue and later we’ll have a look at them, but first let’s see how our app will change.

As of now interfaces FortuneTeller and HoroscopeTeller have only 1 implementation each. We’re going to add 2 more for each of them:

FortuneTeller and HoroscopeTeller

where:

  • Caching... – will be a caching decorator;
  • Logging... – will be a logging decorator.

How to define the decorators

Now, let’s have a look at the options we have.

Java config with the top-level decorator

One of the options – is to use Java config again. In that case we will define the beans as methods of the config class, where dependencies required by the bean will have to be defined as the method’s arguments. Which means that if we’ll change the bean’s constructor – we’ll have to change the config as well. The benefits of such option are:

  • Low coupling between the decorators. The connection between them will be defined in the config, so they wouldn’t “know” anything about each other themselves.
  • All the modifications to the decorators’ order would be localized in the config.

In our case the config will look like this:

DomainConfig.java
@Configuration public class DomainConfig { @Bean public FortuneTeller fortuneTeller( final Map<FortuneRequest, FortuneResponse> cache, final FortuneResponseRepository fortuneResponseRepository, final Function<FortuneRequest, PersonalData> personalDataExtractor, final PersonalDataRepository personalDataRepository ) { return new LoggingFortuneTeller( new CachingFortuneTeller( new Globa(fortuneResponseRepository, personalDataExtractor, personalDataRepository), cache ) ); } @Bean public HoroscopeTeller horoscopeTeller( final Map<ZodiacSign, Horoscope> cache, final HoroscopeRepository horoscopeRepository ) { return new LoggingHoroscopeTeller( new CachingHoroscopeTeller( new Gypsy(horoscopeRepository), cache ) ); } }
Code language: Java (java)

As you can see, there’s only one bean defined for each of the interfaces, while both methods have all of the bean’s dependencies as their arguments. This way of defining decorators is pretty clear.

Qualifier

Another option – is to use @Qualifier annotation. This will be a more declarative way of defining decorators than Java config, but in that case we will have to specify the exact bean on which the current bean depends. Meaning that we will have high coupling between the beans. Which, in turn, means that in case if we will decide to change the decorators’ order, the change will be spread across multiple classes. For example if we will decide to add a new decorator between 2 other decorators, then we will have to change at least 2 classes (apart from the new decorator itself).

LoggingFortuneTeller.java
@Primary @Component public final class LoggingFortuneTeller implements FortuneTeller { private final FortuneTeller internal; private final Logger logger; public LoggingFortuneTeller( @Qualifier("cachingFortuneTeller") @NonNull final FortuneTeller internal ) { this.internal = internal; this.logger = getLogger(internal.getClass()); }
Code language: Java (java)

As you can see from the example of the logging FortuneTeller decorator: since it is a top-level decorator (this is the bean that should be injected in every class depending on the FortuneTeller interface), it is marked as @Primary. While the constructor argument internal has @Qualifier annotation on it, pointing to the bean named cachingFortuneTeller. In our case, the caching decorator would be injected there.

Custom qualifier

Starting with the version 2.5, Spring offers a way to define your own qualifier annotation. It might look as follows.

First, we will define an enum with the decorator types:

public enum DecoratorType { LOGGING, CACHING, NOT_DECORATOR }
Code language: Java (java)

Next we will define our own qualifier annotation:

@Qualifier @Retention(RUNTIME) public @interface Decorator { DecoratorType value() default NOT_DECORATOR; }
Code language: Java (java)

Please, notice that for the annotation to work it should either be annotated with @Qualifier, or you should define CustomAutowireConfigurer bean, to which you will pass the annotation class.

The decorators themselves in case of using a custom qualifier will look this way:

CachingFortuneTeller.java
@Decorator(CACHING) @Component public final class CachingFortuneTeller implements FortuneTeller { private final FortuneTeller internal; private final Map<FortuneRequest, FortuneResponse> cache; public CachingFortuneTeller( @Decorator(NOT_DECORATOR) final FortuneTeller internal, final Map<FortuneRequest, FortuneResponse> cache ) { this.internal = internal; this.cache = cache; }
Code language: Java (java)

The caching decorator is in the middle of the decorators chain, that’s why it has @Decorator annotation on it, pointing out that it’s a caching decorator. At the same time, the constructor’s argument internal has the same annotation, but pointing out that a non-decorator should be injected there, i.e. it should be the default implementation of FortuneTeller – in our case it’s Globa.

In comparison to the Spring’s qualifier, this options has it’s own pros and cons. The disadvantage here is that you have to put the qualifier annotation not only on the class itself, but also on it’s constructor arguments. On the other hand, the beans doesn’t know each other’s name now, they only know the decorator type, meaning that there’s less coupling now.

DecoratorAutowireCandidateResolver

The last option – is to write our own resolver. Exciting! This is why we’re all here! ๐Ÿ™‚ I would like to have a way to define decorators like in Java config, but without having to describe all the bean’s dependencies there. For example, it could be a custom bean in the config, describing the decorators order. Like this:

DomainConfig.java
@Configuration public class DomainConfig { @Bean public OrderConfig<FortuneTeller> fortuneTellerOrderConfig() { return () -> asList( LoggingFortuneTeller.class, CachingFortuneTeller.class, Globa.class ); } @Bean public OrderConfig<HoroscopeTeller> horoscopeTellerOrderConfig() { return () -> asList( LoggingHoroscopeTeller.class, CachingHoroscopeTeller.class, Gypsy.class ); } }
Code language: Java (java)

Looks good to me! We get all of the advantages of Java config – it’s explicit and localized, while also getting rid of it’s verbosity. Let’s see what do we need to make it work!

First, we would need a way to define the decorators order. It might be an interface with a single method that would return an ordered list of classes. Something like this:

@FunctionalInterface public interface OrderConfig<T> { List<Class<? extends T>> getClasses(); }
Code language: Java (java)

BeanDefinitionRegistryPostProcessor

We will also need to implement BeanDefinitionRegistryPostProcessor. It extends BeanFactoryPostProcessor, but being invoked before it and, according to the documentation, can be used to register new bean definitions. It’s not like you can’t use BeanFactoryPostProcessor here, it just feels like it’s more correct to use a registry prost processor here.

That’s what it will do:

  • Iterate through all bean definitions.
  • Remove bean definitions for the classes defined in OrderConfigs. We need that because those classes might have Spring’s stereotype annotation and there might be bean definitions created for them during package scan.
  • Create new bean definitions for the classes defined in OrderConfigs. The new definitions will have an annotation pointing to the parent bean (decorator) of the class.

BeanFactoryPostProcessor

We’re not going to disregard BeanFactoryPostProcessor as well. It is used to manipulate bean definitions before the beans are initialized. I suppose, this class is mostly known as a victim of Spring the Ripper. ๐Ÿ™‚

Evgeny Borisov

The only thing it’s going to do – is register our implementation of AutowireCandidateResolver in the bean factory:

DecoratorAutowireCandidateResolverConfigurer.java
@Component class DecoratorAutowireCandidateResolverConfigurer implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(final ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { Assert.state(configurableListableBeanFactory instanceof DefaultListableBeanFactory, "BeanFactory needs to be a DefaultListableBeanFactory"); val beanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory; beanFactory.setAutowireCandidateResolver( new DecoratorAutowireCandidateResolver(beanFactory.getAutowireCandidateResolver()) ); } }
Code language: Java (java)

DecoratorAutowireCandidateResolver

The resolver itself will look as follows:

DecoratorAutowireCandidateResolver.java
@RequiredArgsConstructor public final class DecoratorAutowireCandidateResolver implements AutowireCandidateResolver { private final AutowireCandidateResolver resolver; @Override public boolean isAutowireCandidate(final BeanDefinitionHolder bdHolder, final DependencyDescriptor descriptor) { val dependentType = descriptor.getMember().getDeclaringClass(); val dependencyType = descriptor.getDependencyType(); val candidateBeanDefinition = (AbstractBeanDefinition) bdHolder.getBeanDefinition(); if (dependencyType.isAssignableFrom(dependentType)) { val candidateQualifier = candidateBeanDefinition.getQualifier(OrderQualifier.class.getTypeName()); if (candidateQualifier != null) { return dependentType.getTypeName().equals(candidateQualifier.getAttribute("value")); } } return resolver.isAutowireCandidate(bdHolder, descriptor); }
Code language: Java (java)

First, it gets the dependent type and dependency type from the dependency descriptor:

val dependentType = descriptor.getMember().getDeclaringClass(); val dependencyType = descriptor.getDependencyType();
Code language: Java (java)

Then it gets the bean definition from the argument bdHolder:

val candidateBeanDefinition = (AbstractBeanDefinition) bdHolder.getBeanDefinition();
Code language: Java (java)

After that it compares the dependent type to the dependency type. This is how we check that we’re dealing with a decorator:

dependencyType.isAssignableFrom(dependentType)
Code language: Java (java)

If they didn’t match, then we’re delegating the call to the internal resolver since we’re not dealing with a decorator in that case.

If they matched, then it tries to get the annotation pointing to the class of the parent decorator:

val candidateQualifier = candidateBeanDefinition.getQualifier(OrderQualifier.class.getTypeName());
Code language: Java (java)

And if the annotation is there – then it compares the class from the annotation to the dependent class:

if (candidateQualifier != null) { return dependentType.getTypeName().equals(candidateQualifier.getAttribute("value")); }
Code language: Java (java)

If they matched – then we’ve found a decorator bean satisfying the dependency description. If they didn’t – we return false.

Other options

  • It might be possible to do the same thing by extending the functionality of ConfigurationClassBeanDefinitionReader.
  • Instead of a custom annotation (OrderQualifier) pointing to the class of a parent decorator bean, it is possible to invert the logic here: put the @Qualifier annotation onto the arguments of the decorator’s constructor, but in that case we’d have to explicitly describe all arguments of the constructor and I wanted to avoid that.

Spring-orderconfig library

As an outcome of this article, I’ve created a library – spring-orderconfig. To use it with Spring Boot you just need to add it to the classpath of your app:

Using maven
<dependency> <groupId>com.github.monosoul</groupId> <artifactId>spring-orderconfig</artifactId> <version>0.0.2</version> </dependency>
Code language: HTML, XML (xml)
Using Kotlin DSL with Gradle
dependencies { implementation("com.github.monosoul:spring-orderconfig:0.0.2") }
Code language: Gradle (gradle)

It does pretty much the same thing we did here, but without having you to write a single line of extra code.

Summary

In this article we used decorators to understand how dependency injection in Spring works and see how to customize it. Though, you can apply the knowledge about the classes I showed here to customize the way Spring instantiates beans in general. Let’s reiterate through the options to configure decorators that we have:

  • Java config – it is imperative, pretty verbose and will have to be changed along with the decorators’ dependencies. On the other hand it is simple, clear and creates low coupling between the classes.
  • @Qualifier – it is declarative, short, but creates high coupling.
  • A custom qualifier – it is also declarative, creates less coupling than the regular @Qualifier, but slightly more verbose.
  • Our own implementation of autowire candidate resolver – it is imperative, creates low coupling and less verbose than the regular Java config, but you’ll have to write it.
  • Spring-orderconfig lib – pretty much the previous option, but already written. ๐Ÿ™‚

The source code is available in my GitHub repo: https://github.com/monosoul/spring-di-customization

P.S.

This article is a translation of the same-themed article I wrote in Russian back in 2019. If you speak Russian, you might prefer the original version of it.

Like it? Share it!

Leave a comment

Your email address will not be published.