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

  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)
  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 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

  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)
  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. }

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

  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, factory = BookFactory)
  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 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

  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 jakarta.inject.Singleton;
  2. @Singleton
  3. class DefaultResponseStrategy implements ResponseStrategy {
  4. }
  1. import jakarta.inject.Singleton
  2. @Singleton
  3. class DefaultResponseStrategy implements ResponseStrategy {
  4. }
  1. import jakarta.inject.Singleton
  2. @Singleton
  3. internal class DefaultResponseStrategy : ResponseStrategy

The custom implementation

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

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