3.8 Bean Factories

In many cases, you may want to make available as a bean a class that is not part of your codebase such as those provided by third-party libraries. In this case, you cannot annotate the compiled class. Instead, implement a @Factory.

A factory is a class annotated with the Factory annotation that provides one or more methods annotated with a bean scope annotation. Which annotation you use depends on what scope you want the bean to be in. See the section on bean scopes for more information.

The return types of methods annotated with a bean scope annotation are the bean types. This is best illustrated by an example:

  1. @Singleton
  2. class CrankShaft {
  3. }
  4. class V8Engine implements Engine {
  5. private final int cylinders = 8;
  6. private final CrankShaft crankShaft;
  7. public V8Engine(CrankShaft crankShaft) {
  8. this.crankShaft = crankShaft;
  9. }
  10. @Override
  11. public String start() {
  12. return "Starting V8";
  13. }
  14. }
  15. @Factory
  16. class EngineFactory {
  17. @Singleton
  18. Engine v8Engine(CrankShaft crankShaft) {
  19. return new V8Engine(crankShaft);
  20. }
  21. }
  1. @Singleton
  2. class CrankShaft {
  3. }
  4. class V8Engine implements Engine {
  5. final int cylinders = 8
  6. final CrankShaft crankShaft
  7. V8Engine(CrankShaft crankShaft) {
  8. this.crankShaft = crankShaft
  9. }
  10. @Override
  11. String start() {
  12. "Starting V8"
  13. }
  14. }
  15. @Factory
  16. class EngineFactory {
  17. @Singleton
  18. Engine v8Engine(CrankShaft crankShaft) {
  19. new V8Engine(crankShaft)
  20. }
  21. }
  1. @Singleton
  2. internal class CrankShaft
  3. internal class V8Engine(private val crankShaft: CrankShaft) : Engine {
  4. private val cylinders = 8
  5. override fun start(): String {
  6. return "Starting V8"
  7. }
  8. }
  9. @Factory
  10. internal class EngineFactory {
  11. @Singleton
  12. fun v8Engine(crankShaft: CrankShaft): Engine {
  13. return V8Engine(crankShaft)
  14. }
  15. }

In this case, a V8Engine is created by the EngineFactory class’ v8Engine method. Note that you can inject parameters into the method and they will be resolved as beans. The resulting V8Engine bean will be a singleton.

A factory can have multiple methods annotated with bean scope annotations, each one returning a distinct bean type.

If you take this approach you should not invoke other bean methods internally within the class. Instead, inject the types via parameters.
To allow the resulting bean to participate in the application context shutdown process, annotate the method with @Bean and set the preDestroy argument to the name of the method to be called to close the bean.

Programmatically Disabling Beans

Factory methods can throw DisabledBeanException to conditionally disable beans. Using @Requires should always be the preferred method to conditionally create beans; throwing an exception in a factory method should only be done if using @Requires is not possible.

For example:

  1. public interface Engine {
  2. Integer getCylinders();
  3. }
  4. @EachProperty("engines")
  5. public class EngineConfiguration implements Toggleable {
  6. private boolean enabled = true;
  7. private Integer cylinders;
  8. @NotNull
  9. public Integer getCylinders() {
  10. return cylinders;
  11. }
  12. public void setCylinders(Integer cylinders) {
  13. this.cylinders = cylinders;
  14. }
  15. @Override
  16. public boolean isEnabled() {
  17. return enabled;
  18. }
  19. public void setEnabled(boolean enabled) {
  20. this.enabled = enabled;
  21. }
  22. }
  23. @Factory
  24. public class EngineFactory {
  25. @EachBean(EngineConfiguration.class)
  26. public Engine buildEngine(EngineConfiguration engineConfiguration) {
  27. if (engineConfiguration.isEnabled()) {
  28. return engineConfiguration::getCylinders;
  29. } else {
  30. throw new DisabledBeanException("Engine configuration disabled");
  31. }
  32. }
  33. }
  1. interface Engine {
  2. Integer getCylinders()
  3. }
  4. @EachProperty("engines")
  5. class EngineConfiguration implements Toggleable {
  6. boolean enabled = true
  7. @NotNull
  8. Integer cylinders
  9. }
  10. @Factory
  11. class EngineFactory {
  12. @EachBean(EngineConfiguration)
  13. Engine buildEngine(EngineConfiguration engineConfiguration) {
  14. if (engineConfiguration.enabled) {
  15. (Engine){ -> engineConfiguration.cylinders }
  16. } else {
  17. throw new DisabledBeanException("Engine configuration disabled")
  18. }
  19. }
  20. }
  1. interface Engine {
  2. fun getCylinders(): Int
  3. }
  4. @EachProperty("engines")
  5. class EngineConfiguration : Toggleable {
  6. var enabled = true
  7. @NotNull
  8. val cylinders: Int? = null
  9. override fun isEnabled(): Boolean {
  10. return enabled
  11. }
  12. }
  13. @Factory
  14. class EngineFactory {
  15. @EachBean(EngineConfiguration::class)
  16. fun buildEngine(engineConfiguration: EngineConfiguration): Engine? {
  17. return if (engineConfiguration.isEnabled) {
  18. object : Engine {
  19. override fun getCylinders(): Int {
  20. return engineConfiguration.cylinders!!
  21. }
  22. }
  23. } else {
  24. throw DisabledBeanException("Engine configuration disabled")
  25. }
  26. }
  27. }

Injection Point

A common use case with factories is to take advantage of annotation metadata from the point at which an object is injected such that behaviour can be modified based on said metadata.

Consider an annotation such as the following:

  1. @Documented
  2. @Retention(RUNTIME)
  3. @Target(ElementType.PARAMETER)
  4. public @interface Cylinders {
  5. int value() default 8;
  6. }
  1. @Documented
  2. @Retention(RUNTIME)
  3. @Target(ElementType.PARAMETER)
  4. @interface Cylinders {
  5. int value() default 8
  6. }
  1. @MustBeDocumented
  2. @Retention(AnnotationRetention.RUNTIME)
  3. @Target(AnnotationTarget.VALUE_PARAMETER)
  4. annotation class Cylinders(val value: Int = 8)

The above annotation could be used to customize the type of engine we want to inject into a vehicle at the point at which the injection point is defined:

  1. @Singleton
  2. class Vehicle {
  3. private final Engine engine;
  4. Vehicle(@Cylinders(6) Engine engine) {
  5. this.engine = engine;
  6. }
  7. String start() {
  8. return engine.start();
  9. }
  10. }
  1. @Singleton
  2. class Vehicle {
  3. private final Engine engine
  4. Vehicle(@Cylinders(6) Engine engine) {
  5. this.engine = engine
  6. }
  7. String start() {
  8. return engine.start()
  9. }
  10. }
  1. @Singleton
  2. internal class Vehicle(@param:Cylinders(6) private val engine: Engine) {
  3. fun start(): String {
  4. return engine.start()
  5. }
  6. }

The above Vehicle class specifies an annotation value of @Cylinders(6) indicating an Engine of six cylinders is required.

To implement this use case, define a factory that accepts the InjectionPoint instance to analyze the defined annotation values:

  1. @Factory
  2. class EngineFactory {
  3. @Prototype
  4. Engine v8Engine(InjectionPoint<?> injectionPoint, CrankShaft crankShaft) { (1)
  5. final int cylinders = injectionPoint
  6. .getAnnotationMetadata()
  7. .intValue(Cylinders.class).orElse(8); (2)
  8. switch (cylinders) { (3)
  9. case 6:
  10. return new V6Engine(crankShaft);
  11. case 8:
  12. return new V8Engine(crankShaft);
  13. default:
  14. throw new IllegalArgumentException("Unsupported number of cylinders specified: " + cylinders);
  15. }
  16. }
  17. }
  1. @Factory
  2. class EngineFactory {
  3. @Prototype
  4. Engine v8Engine(InjectionPoint<?> injectionPoint, CrankShaft crankShaft) { (1)
  5. final int cylinders = injectionPoint
  6. .getAnnotationMetadata()
  7. .intValue(Cylinders.class).orElse(8) (2)
  8. switch (cylinders) { (3)
  9. case 6:
  10. return new V6Engine(crankShaft)
  11. case 8:
  12. return new V8Engine(crankShaft)
  13. default:
  14. throw new IllegalArgumentException("Unsupported number of cylinders specified: $cylinders")
  15. }
  16. }
  17. }
  1. @Factory
  2. internal class EngineFactory {
  3. @Prototype
  4. fun v8Engine(injectionPoint: InjectionPoint<*>, crankShaft: CrankShaft): Engine { (1)
  5. val cylinders = injectionPoint
  6. .annotationMetadata
  7. .intValue(Cylinders::class.java).orElse(8) (2)
  8. return when (cylinders) { (3)
  9. 6 -> V6Engine(crankShaft)
  10. 8 -> V8Engine(crankShaft)
  11. else -> throw IllegalArgumentException("Unsupported number of cylinders specified: $cylinders")
  12. }
  13. }
  14. }
1The factory method defines a parameter of type InjectionPoint.
2The annotation metadata is used to obtain the value of the @Cylinder annotation
3The value is used to construct an engine, throwing an exception if an engine cannot be constructed.
It is important to note that the factory is declared as @Prototype scope so the method is invoked for each injection point. If the V8Engine and V6Engine types are required to be singletons, the factory should use a Map to ensure the objects are only constructed once.

Beans from Fields

With Micronaut 3.0 or above it is also possible to produce beans from fields by declaring the @Bean annotation on a field.

Whilst generally this approach should be discouraged in favour for factory methods, which provide more flexibility it does simplify testing code. For example with bean fields you can easily produce mocks in your test code:

  1. import io.micronaut.context.annotation.*;
  2. import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
  3. import org.junit.jupiter.api.Test;
  4. import jakarta.inject.Inject;
  5. import static org.junit.jupiter.api.Assertions.assertEquals;
  6. @MicronautTest
  7. public class VehicleMockSpec {
  8. @Requires(beans=VehicleMockSpec.class)
  9. @Bean @Replaces(Engine.class)
  10. Engine mockEngine = () -> "Mock Started"; (1)
  11. @Inject Vehicle vehicle; (2)
  12. @Test
  13. void testStartEngine() {
  14. final String result = vehicle.start();
  15. assertEquals("Mock Started", result); (3)
  16. }
  17. }
  1. import io.micronaut.context.annotation.*
  2. import io.micronaut.test.extensions.spock.annotation.MicronautTest
  3. import spock.lang.Specification
  4. import jakarta.inject.Inject
  5. @MicronautTest
  6. class VehicleMockSpec extends Specification {
  7. @Requires(beans=VehicleMockSpec.class)
  8. @Bean @Replaces(Engine.class)
  9. Engine mockEngine = {-> "Mock Started" } as Engine (1)
  10. @Inject Vehicle vehicle (2)
  11. void "test start engine"() {
  12. given:
  13. final String result = vehicle.start()
  14. expect:
  15. result == "Mock Started" (3)
  16. }
  17. }
  1. import io.micronaut.context.annotation.Bean
  2. import io.micronaut.context.annotation.Replaces
  3. import io.micronaut.test.extensions.junit5.annotation.MicronautTest
  4. import org.junit.jupiter.api.Assertions
  5. import org.junit.jupiter.api.Test
  6. import jakarta.inject.Inject
  7. @MicronautTest
  8. class VehicleMockSpec {
  9. @get:Bean
  10. @get:Replaces(Engine::class)
  11. val mockEngine: Engine = object : Engine { (1)
  12. override fun start(): String {
  13. return "Mock Started"
  14. }
  15. }
  16. @Inject
  17. lateinit var vehicle : Vehicle (2)
  18. @Test
  19. fun testStartEngine() {
  20. val result = vehicle.start()
  21. Assertions.assertEquals("Mock Started", result) (3)
  22. }
  23. }
1A bean is defined from a field that replaces the existing Engine.
2The Vehicle is injected.
3The code asserts that the mock implementation is called.

Note that only public or package protected fields are supported on non-primitive types. If the field is static, private, or protected a compilation error will occur.