3.9 Bean Replacement
One significant difference between Micronaut’s Dependency Injection system and Spring is the way beans can be replaced.
In a Spring application, beans have names and can effectively be overridden simply by creating a bean with the same name, regardless of the type of the bean. Spring also has the notion of bean registration order, hence in Spring Boot you have @AutoConfigureBefore
and @AutoConfigureAfter
that control how beans override each other.
This strategy leads to difficult to debug problems, for example:
Bean loading order changes, leading to unexpected results
A bean with the same name overrides another bean with a different type
To avoid these problems, Micronaut’s DI has no concept of bean names or load order. Beans have a type and a Qualifier. You cannot override a bean of a completely different type with another.
A useful benefit of Spring’s approach is that it allows overriding existing beans to customize behaviour. In order to support the same ability, Micronaut’s DI provides an explicit @Replaces annotation, which integrates nicely with support for Conditional Beans and clearly documents and expresses the intention of the developer.
Any existing bean can be replaced by another bean that declares @Replaces. For example, consider the following class:
JdbcBookService
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {
DataSource dataSource;
public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}
JdbcBookService
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {
DataSource dataSource
JdbcBookService
@Singleton
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
class JdbcBookService(internal var dataSource: DataSource) : BookService {
You can define a class in src/test/java
that replaces this class just for your tests:
Using @Replaces
@Replaces(JdbcBookService.class) (1)
@Singleton
public class MockBookService implements BookService {
Map<String, Book> bookMap = new LinkedHashMap<>();
@Override
public Book findBook(String title) {
return bookMap.get(title);
}
}
Using @Replaces
@Replaces(JdbcBookService.class) (1)
@Singleton
class MockBookService implements BookService {
Map<String, Book> bookMap = [:]
@Override
Book findBook(String title) {
bookMap.get(title)
}
}
Using @Replaces
@Replaces(JdbcBookService::class) (1)
@Singleton
class MockBookService : BookService {
var bookMap: Map<String, Book> = LinkedHashMap()
override fun findBook(title: String): Book? {
return bookMap[title]
}
}
1 | The MockBookService declares that it replaces JdbcBookService |
Factory Replacement
The @Replaces
annotation also supports a factory
argument. That argument allows the replacement of factory beans in their entirety or specific types created by the factory.
For example, it may be desired to replace all or part of the given factory class:
BookFactory
@Factory
public class BookFactory {
@Singleton
Book novel() {
return new Book("A Great Novel");
}
@Singleton
TextBook textBook() {
return new TextBook("Learning 101");
}
}
BookFactory
@Factory
class BookFactory {
@Singleton
Book novel() {
new Book('A Great Novel')
}
@Singleton
TextBook textBook() {
new TextBook('Learning 101')
}
}
BookFactory
@Factory
class BookFactory {
@Singleton
internal fun novel(): Book {
return Book("A Great Novel")
}
@Singleton
internal fun textBook(): TextBook {
return TextBook("Learning 101")
}
}
To replace a factory in its entirety, it is necessary that your factory methods match the return types of all of the methods in the replaced factory. |
In this example, the BookFactory#textBook()
will not be replaced because this factory does not have a factory method that returns a TextBook.
CustomBookFactory
@Factory
@Replaces(factory = BookFactory.class)
public class CustomBookFactory {
@Singleton
Book otherNovel() {
return new Book("An OK Novel");
}
}
CustomBookFactory
@Factory
@Replaces(factory = BookFactory.class)
class CustomBookFactory {
@Singleton
Book otherNovel() {
new Book('An OK Novel')
}
}
CustomBookFactory
@Factory
@Replaces(factory = BookFactory::class)
class CustomBookFactory {
@Singleton
internal fun otherNovel(): Book {
return Book("An OK Novel")
}
}
It may be the case that you don’t wish for the factory methods to be replaced, except for a select few. For that use case, you can apply the @Replaces
annotation on the method and denote the factory that it should apply to.
TextBookFactory
@Factory
public class TextBookFactory {
@Singleton
@Replaces(value = TextBook.class, factory = BookFactory.class)
TextBook textBook() {
return new TextBook("Learning 305");
}
}
TextBookFactory
@Factory
class TextBookFactory {
@Singleton
@Replaces(value = TextBook.class, factory = BookFactory.class)
TextBook textBook() {
new TextBook('Learning 305')
}
}
TextBookFactory
@Factory
class TextBookFactory {
@Singleton
@Replaces(value = TextBook::class, factory = BookFactory::class)
internal fun textBook(): TextBook {
return TextBook("Learning 305")
}
}
The BookFactory#novel()
method will not be replaced because the TextBook class is defined in the annotation.
Default Implementation
When exposing an API, it may be desirable for the default implementation of an interface to not be exposed as part of the public API. Doing so would prevent users from being able to replace the implementation because they would not be able to reference the class. The solution is to annotate the interface with DefaultImplementation to indicate which implementation should be replaced if a user creates a bean that @Replaces(YourInterface.class)
.
For example consider:
A public API contract
import io.micronaut.context.annotation.DefaultImplementation;
@DefaultImplementation(DefaultResponseStrategy.class)
public interface ResponseStrategy {
}
import io.micronaut.context.annotation.DefaultImplementation
@DefaultImplementation(DefaultResponseStrategy)
interface ResponseStrategy {
}
import io.micronaut.context.annotation.DefaultImplementation
@DefaultImplementation(DefaultResponseStrategy::class)
interface ResponseStrategy
The default implementation
import javax.inject.Singleton;
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}
import javax.inject.Singleton
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}
import javax.inject.Singleton
@Singleton
internal class DefaultResponseStrategy : ResponseStrategy
The custom implementation
import io.micronaut.context.annotation.Replaces;
import javax.inject.Singleton;
@Singleton
@Replaces(ResponseStrategy.class)
public class CustomResponseStrategy implements ResponseStrategy {
}
import io.micronaut.context.annotation.Replaces
import javax.inject.Singleton
@Singleton
@Replaces(ResponseStrategy)
class CustomResponseStrategy implements ResponseStrategy {
}
import io.micronaut.context.annotation.Replaces
import javax.inject.Singleton
@Singleton
@Replaces(ResponseStrategy::class)
class CustomResponseStrategy : ResponseStrategy
In the above example, the CustomResponseStrategy
will replace the DefaultResponseStrategy
because the DefaultImplementation annotation points to the DefaultResponseStrategy
.