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

  1. @Singleton
  2. @Requires(beans = DataSource.class)
  3. @Requires(property = "datasource.url")
  4. public class JdbcBookService implements BookService {
  5. DataSource dataSource;
  6. public JdbcBookService(DataSource dataSource) {
  7. this.dataSource = dataSource;
  8. }

JdbcBookService

  1. @Singleton
  2. @Requires(beans = DataSource.class)
  3. @Requires(property = "datasource.url")
  4. class JdbcBookService implements BookService {
  5. DataSource dataSource

JdbcBookService

  1. @Singleton
  2. @Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
  3. 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

  1. @Replaces(JdbcBookService.class) (1)
  2. @Singleton
  3. public class MockBookService implements BookService {
  4. Map<String, Book> bookMap = new LinkedHashMap<>();
  5. @Override
  6. public Book findBook(String title) {
  7. return bookMap.get(title);
  8. }
  9. }

Using @Replaces

  1. @Replaces(JdbcBookService.class) (1)
  2. @Singleton
  3. class MockBookService implements BookService {
  4. Map<String, Book> bookMap = [:]
  5. @Override
  6. Book findBook(String title) {
  7. bookMap.get(title)
  8. }
  9. }

Using @Replaces

  1. @Replaces(JdbcBookService::class) (1)
  2. @Singleton
  3. class MockBookService : BookService {
  4. var bookMap: Map<String, Book> = LinkedHashMap()
  5. override fun findBook(title: String): Book? {
  6. return bookMap[title]
  7. }
  8. }
1The 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

  1. @Factory
  2. public class BookFactory {
  3. @Singleton
  4. Book novel() {
  5. return new Book("A Great Novel");
  6. }
  7. @Singleton
  8. TextBook textBook() {
  9. return new TextBook("Learning 101");
  10. }
  11. }

BookFactory

  1. @Factory
  2. class BookFactory {
  3. @Singleton
  4. Book novel() {
  5. new Book('A Great Novel')
  6. }
  7. @Singleton
  8. TextBook textBook() {
  9. new TextBook('Learning 101')
  10. }
  11. }

BookFactory

  1. @Factory
  2. class BookFactory {
  3. @Singleton
  4. internal fun novel(): Book {
  5. return Book("A Great Novel")
  6. }
  7. @Singleton
  8. internal fun textBook(): TextBook {
  9. return TextBook("Learning 101")
  10. }
  11. }
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

  1. @Factory
  2. @Replaces(factory = BookFactory.class)
  3. public class CustomBookFactory {
  4. @Singleton
  5. Book otherNovel() {
  6. return new Book("An OK Novel");
  7. }
  8. }

CustomBookFactory

  1. @Factory
  2. @Replaces(factory = BookFactory.class)
  3. class CustomBookFactory {
  4. @Singleton
  5. Book otherNovel() {
  6. new Book('An OK Novel')
  7. }
  8. }

CustomBookFactory

  1. @Factory
  2. @Replaces(factory = BookFactory::class)
  3. class CustomBookFactory {
  4. @Singleton
  5. internal fun otherNovel(): Book {
  6. return Book("An OK Novel")
  7. }
  8. }

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

  1. @Factory
  2. public class TextBookFactory {
  3. @Singleton
  4. @Replaces(value = TextBook.class, factory = BookFactory.class)
  5. TextBook textBook() {
  6. return new TextBook("Learning 305");
  7. }
  8. }

TextBookFactory

  1. @Factory
  2. class TextBookFactory {
  3. @Singleton
  4. @Replaces(value = TextBook.class, factory = BookFactory.class)
  5. TextBook textBook() {
  6. new TextBook('Learning 305')
  7. }
  8. }

TextBookFactory

  1. @Factory
  2. class TextBookFactory {
  3. @Singleton
  4. @Replaces(value = TextBook::class, factory = BookFactory::class)
  5. internal fun textBook(): TextBook {
  6. return TextBook("Learning 305")
  7. }
  8. }

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

  1. import io.micronaut.context.annotation.DefaultImplementation;
  2. @DefaultImplementation(DefaultResponseStrategy.class)
  3. public interface ResponseStrategy {
  4. }
  1. import io.micronaut.context.annotation.DefaultImplementation
  2. @DefaultImplementation(DefaultResponseStrategy)
  3. interface ResponseStrategy {
  4. }
  1. import io.micronaut.context.annotation.DefaultImplementation
  2. @DefaultImplementation(DefaultResponseStrategy::class)
  3. interface ResponseStrategy

The default implementation

  1. import javax.inject.Singleton;
  2. @Singleton
  3. class DefaultResponseStrategy implements ResponseStrategy {
  4. }
  1. import javax.inject.Singleton
  2. @Singleton
  3. class DefaultResponseStrategy implements ResponseStrategy {
  4. }
  1. import javax.inject.Singleton
  2. @Singleton
  3. internal class DefaultResponseStrategy : ResponseStrategy

The custom implementation

  1. import io.micronaut.context.annotation.Replaces;
  2. import javax.inject.Singleton;
  3. @Singleton
  4. @Replaces(ResponseStrategy.class)
  5. public class CustomResponseStrategy implements ResponseStrategy {
  6. }
  1. import io.micronaut.context.annotation.Replaces
  2. import javax.inject.Singleton
  3. @Singleton
  4. @Replaces(ResponseStrategy)
  5. class CustomResponseStrategy implements ResponseStrategy {
  6. }
  1. import io.micronaut.context.annotation.Replaces
  2. import javax.inject.Singleton
  3. @Singleton
  4. @Replaces(ResponseStrategy::class)
  5. class CustomResponseStrategy : ResponseStrategy

In the above example, the CustomResponseStrategy will replace the DefaultResponseStrategy because the DefaultImplementation annotation points to the DefaultResponseStrategy.