5.4 Bean Life Cycle Advice

Sometimes you may need to apply advice to a bean’s lifecycle. There are 3 types of advice that are applicable in this case:

  • Interception of the construction of the bean

  • Interception of the bean’s @PostConstruct invocation

  • Interception of a bean’s @PreDestroy invocation

Micronaut supports these 3 use cases by allowing the definition of additional @InterceptorBinding meta-annotations.

Consider the following annotation definition:

AroundConstruct example

  1. import io.micronaut.aop.*;
  2. import io.micronaut.context.annotation.Prototype;
  3. import java.lang.annotation.*;
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @AroundConstruct (1)
  6. @InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT) (2)
  7. @InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) (3)
  8. @Prototype (4)
  9. public @interface ProductBean {
  10. }

AroundConstruct example

  1. import io.micronaut.aop.*
  2. import io.micronaut.context.annotation.Prototype
  3. import java.lang.annotation.*
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @AroundConstruct (1)
  6. @InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT) (2)
  7. @InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) (3)
  8. @Prototype (4)
  9. @interface ProductBean {
  10. }

AroundConstruct example

  1. import io.micronaut.aop.AroundConstruct
  2. import io.micronaut.aop.InterceptorBinding
  3. import io.micronaut.aop.InterceptorBindingDefinitions
  4. import io.micronaut.aop.InterceptorKind
  5. import io.micronaut.context.annotation.Prototype
  6. @Retention(AnnotationRetention.RUNTIME)
  7. @AroundConstruct (1)
  8. @InterceptorBindingDefinitions(
  9. InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT), (2)
  10. InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) (3)
  11. )
  12. @Prototype (4)
  13. annotation class ProductBean
1The @AroundConstruct annotation is added to indicate that interception of the constructor should occur
2An @InterceptorBinding definition is used to indicate that @PostConstruct interception should occur
3An @InterceptorBinding definition is used to indicate that @PreDestroy interception should occur
4The bean is defined as @Prototype so a new instance is required for each injection point

Note that if you do not need @PostConstruct and @PreDestroy interception you can simply remove those bindings.

The @ProductBean annotation can then be used on the target class:

Using an AroundConstruct meta-annotation

  1. import io.micronaut.context.annotation.Parameter;
  2. import jakarta.annotation.PreDestroy;
  3. @ProductBean (1)
  4. public class Product {
  5. private final String productName;
  6. private boolean active = false;
  7. public Product(@Parameter String productName) { (2)
  8. this.productName = productName;
  9. }
  10. public String getProductName() {
  11. return productName;
  12. }
  13. public boolean isActive() {
  14. return active;
  15. }
  16. public void setActive(boolean active) {
  17. this.active = active;
  18. }
  19. @PreDestroy (3)
  20. void disable() {
  21. active = false;
  22. }
  23. }

Using an AroundConstruct meta-annotation

  1. import io.micronaut.context.annotation.Parameter
  2. import jakarta.annotation.PreDestroy
  3. @ProductBean (1)
  4. class Product {
  5. final String productName
  6. boolean active = false
  7. Product(@Parameter String productName) { (2)
  8. this.productName = productName
  9. }
  10. @PreDestroy (3)
  11. void disable() {
  12. active = false
  13. }
  14. }

Using an AroundConstruct meta-annotation

  1. import io.micronaut.context.annotation.Parameter
  2. import jakarta.annotation.PreDestroy
  3. @ProductBean (1)
  4. class Product(@param:Parameter val productName: String ) { (2)
  5. var active: Boolean = false
  6. @PreDestroy
  7. fun disable() { (3)
  8. active = false
  9. }
  10. }
1The @ProductBean annotation is defined on a class of type Product
2The @Parameter annotation indicates that this bean requires an argument to complete constructions
3Any @PreDestroy or @PostConstruct methods are executed last in the interceptor chain

Now you can define ConstructorInterceptor beans for constructor interception and MethodInterceptor beans for @PostConstruct or @PreDestroy interception.

The following factory defines a ConstructorInterceptor that intercepts construction of Product instances and registers them with a hypothetical ProductService validating the product name first:

Defining a constructor interceptor

  1. import io.micronaut.aop.*;
  2. import io.micronaut.context.annotation.Factory;
  3. @Factory
  4. public class ProductInterceptors {
  5. private final ProductService productService;
  6. public ProductInterceptors(ProductService productService) {
  7. this.productService = productService;
  8. }
  9. @InterceptorBean(ProductBean.class)
  10. ConstructorInterceptor<Product> aroundConstruct() { (1)
  11. return context -> {
  12. final Object[] parameterValues = context.getParameterValues(); (2)
  13. final Object parameterValue = parameterValues[0];
  14. if (parameterValue == null || parameterValues[0].toString().isEmpty()) {
  15. throw new IllegalArgumentException("Invalid product name");
  16. }
  17. String productName = parameterValues[0].toString().toUpperCase();
  18. parameterValues[0] = productName;
  19. final Product product = context.proceed(); (3)
  20. productService.addProduct(product);
  21. return product;
  22. };
  23. }
  24. }

Defining a constructor interceptor

  1. import io.micronaut.aop.*
  2. import io.micronaut.context.annotation.Factory
  3. @Factory
  4. class ProductInterceptors {
  5. private final ProductService productService
  6. ProductInterceptors(ProductService productService) {
  7. this.productService = productService
  8. }
  9. @InterceptorBean(ProductBean.class)
  10. ConstructorInterceptor<Product> aroundConstruct() { (1)
  11. return { context ->
  12. final Object[] parameterValues = context.parameterValues (2)
  13. final Object parameterValue = parameterValues[0]
  14. if (parameterValue == null || parameterValues[0].toString().isEmpty()) {
  15. throw new IllegalArgumentException("Invalid product name")
  16. }
  17. String productName = parameterValues[0].toString().toUpperCase()
  18. parameterValues[0] = productName
  19. final Product product = context.proceed() (3)
  20. productService.addProduct(product)
  21. return product
  22. }
  23. }
  24. }

Defining a constructor interceptor

  1. import io.micronaut.aop.*
  2. import io.micronaut.context.annotation.Factory
  3. @Factory
  4. class ProductInterceptors(private val productService: ProductService) {
  5. @InterceptorBean(ProductBean::class)
  6. fun aroundConstruct(): ConstructorInterceptor<Product> { (1)
  7. return ConstructorInterceptor { context: ConstructorInvocationContext<Product> ->
  8. val parameterValues = context.parameterValues (2)
  9. val parameterValue = parameterValues[0]
  10. require(!(parameterValue == null || parameterValues[0].toString().isEmpty())) { "Invalid product name" }
  11. val productName = parameterValues[0].toString().toUpperCase()
  12. parameterValues[0] = productName
  13. val product = context.proceed() (3)
  14. productService.addProduct(product)
  15. product
  16. }
  17. }
  18. }
1A new @InterceptorBean is defined that is a ConstructorInterceptor
2The constructor parameter values can be retrieved and modified as needed
3The constructor can be invoked with the proceed() method

Defining MethodInterceptor instances that interceptor the @PostConstruct and @PreDestroy methods is no different from defining interceptors for regular methods. Note however that you can use the passed MethodInvocationContext to identify what kind of interception is occurring and adapt the code accordingly like in the following example:

Defining a constructor interceptor

  1. @InterceptorBean(ProductBean.class) (1)
  2. MethodInterceptor<Product, Object> aroundInvoke() {
  3. return context -> {
  4. final Product product = context.getTarget();
  5. switch (context.getKind()) {
  6. case POST_CONSTRUCT: (2)
  7. product.setActive(true);
  8. return context.proceed();
  9. case PRE_DESTROY: (3)
  10. productService.removeProduct(product);
  11. return context.proceed();
  12. default:
  13. return context.proceed();
  14. }
  15. };
  16. }

Defining a constructor interceptor

  1. @InterceptorBean(ProductBean.class) (1)
  2. MethodInterceptor<Product, Object> aroundInvoke() {
  3. return { context ->
  4. final Product product = context.getTarget()
  5. switch (context.kind) {
  6. case InterceptorKind.POST_CONSTRUCT: (2)
  7. product.setActive(true)
  8. return context.proceed()
  9. case InterceptorKind.PRE_DESTROY: (3)
  10. productService.removeProduct(product)
  11. return context.proceed()
  12. default:
  13. return context.proceed()
  14. }
  15. }
  16. }

Defining a constructor interceptor

  1. @InterceptorBean(ProductBean::class)
  2. fun aroundInvoke(): MethodInterceptor<Product, Any> { (1)
  3. return MethodInterceptor { context: MethodInvocationContext<Product, Any> ->
  4. val product = context.target
  5. return@MethodInterceptor when (context.kind) {
  6. InterceptorKind.POST_CONSTRUCT -> { (2)
  7. product.active = true
  8. context.proceed()
  9. }
  10. InterceptorKind.PRE_DESTROY -> { (3)
  11. productService.removeProduct(product)
  12. context.proceed()
  13. }
  14. else -> context.proceed()
  15. }
  16. }
  17. }
1A new @InterceptorBean is defined that is a MethodInterceptor
2@PostConstruct interception is handled
3@PreDestroy interception is handled