3.10 Bean Replacement
One significant difference between Micronaut’s Dependency Injection system and Spring’s is the way beans are replaced.
In a Spring application, beans have names and are overridden 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
annotations that control how beans override each other.
This strategy leads to problems that are difficult to debug, 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. 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)
@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 entirely, your factory methods must match the return types of all methods in the replaced factory. |
In this example, BookFactory#textBook()
is not 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 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")
}
}
To replace one or more factory methods but retain the rest, apply the @Replaces
annotation on the method(s) and denote the factory to 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, factory = BookFactory)
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 to not expose the default implementation of an interface as part of the public API. Doing so prevents users from being able to replace the implementation because they will not be able to reference the class. The solution is to annotate the interface with DefaultImplementation to indicate which implementation to replace 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 jakarta.inject.Singleton;
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}
import jakarta.inject.Singleton
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}
import jakarta.inject.Singleton
@Singleton
internal class DefaultResponseStrategy : ResponseStrategy
The custom implementation
import io.micronaut.context.annotation.Replaces;
import jakarta.inject.Singleton;
@Singleton
@Replaces(ResponseStrategy.class)
public class CustomResponseStrategy implements ResponseStrategy {
}
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton
@Singleton
@Replaces(ResponseStrategy)
class CustomResponseStrategy implements ResponseStrategy {
}
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton
@Singleton
@Replaces(ResponseStrategy::class)
class CustomResponseStrategy : ResponseStrategy
In the above example, the CustomResponseStrategy
replaces the DefaultResponseStrategy
because the DefaultImplementation annotation points to the DefaultResponseStrategy
.