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
invocationInterception 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
import io.micronaut.aop.*;
import io.micronaut.context.annotation.Prototype;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@AroundConstruct (1)
@InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT) (2)
@InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) (3)
@Prototype (4)
public @interface ProductBean {
}
AroundConstruct example
import io.micronaut.aop.*
import io.micronaut.context.annotation.Prototype
import java.lang.annotation.*
@Retention(RetentionPolicy.RUNTIME)
@AroundConstruct (1)
@InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT) (2)
@InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) (3)
@Prototype (4)
@interface ProductBean {
}
AroundConstruct example
import io.micronaut.aop.AroundConstruct
import io.micronaut.aop.InterceptorBinding
import io.micronaut.aop.InterceptorBindingDefinitions
import io.micronaut.aop.InterceptorKind
import io.micronaut.context.annotation.Prototype
@Retention(AnnotationRetention.RUNTIME)
@AroundConstruct (1)
@InterceptorBindingDefinitions(
InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT), (2)
InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) (3)
)
@Prototype (4)
annotation class ProductBean
1 | The @AroundConstruct annotation is added to indicate that interception of the constructor should occur |
2 | An @InterceptorBinding definition is used to indicate that @PostConstruct interception should occur |
3 | An @InterceptorBinding definition is used to indicate that @PreDestroy interception should occur |
4 | The 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
import io.micronaut.context.annotation.Parameter;
import jakarta.annotation.PreDestroy;
@ProductBean (1)
public class Product {
private final String productName;
private boolean active = false;
public Product(@Parameter String productName) { (2)
this.productName = productName;
}
public String getProductName() {
return productName;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
@PreDestroy (3)
void disable() {
active = false;
}
}
Using an AroundConstruct meta-annotation
import io.micronaut.context.annotation.Parameter
import jakarta.annotation.PreDestroy
@ProductBean (1)
class Product {
final String productName
boolean active = false
Product(@Parameter String productName) { (2)
this.productName = productName
}
@PreDestroy (3)
void disable() {
active = false
}
}
Using an AroundConstruct meta-annotation
import io.micronaut.context.annotation.Parameter
import jakarta.annotation.PreDestroy
@ProductBean (1)
class Product(@param:Parameter val productName: String ) { (2)
var active: Boolean = false
@PreDestroy
fun disable() { (3)
active = false
}
}
1 | The @ProductBean annotation is defined on a class of type Product |
2 | The @Parameter annotation indicates that this bean requires an argument to complete constructions |
3 | Any @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
import io.micronaut.aop.*;
import io.micronaut.context.annotation.Factory;
@Factory
public class ProductInterceptors {
private final ProductService productService;
public ProductInterceptors(ProductService productService) {
this.productService = productService;
}
@InterceptorBean(ProductBean.class)
ConstructorInterceptor<Product> aroundConstruct() { (1)
return context -> {
final Object[] parameterValues = context.getParameterValues(); (2)
final Object parameterValue = parameterValues[0];
if (parameterValue == null || parameterValues[0].toString().isEmpty()) {
throw new IllegalArgumentException("Invalid product name");
}
String productName = parameterValues[0].toString().toUpperCase();
parameterValues[0] = productName;
final Product product = context.proceed(); (3)
productService.addProduct(product);
return product;
};
}
}
Defining a constructor interceptor
import io.micronaut.aop.*
import io.micronaut.context.annotation.Factory
@Factory
class ProductInterceptors {
private final ProductService productService
ProductInterceptors(ProductService productService) {
this.productService = productService
}
@InterceptorBean(ProductBean.class)
ConstructorInterceptor<Product> aroundConstruct() { (1)
return { context ->
final Object[] parameterValues = context.parameterValues (2)
final Object parameterValue = parameterValues[0]
if (parameterValue == null || parameterValues[0].toString().isEmpty()) {
throw new IllegalArgumentException("Invalid product name")
}
String productName = parameterValues[0].toString().toUpperCase()
parameterValues[0] = productName
final Product product = context.proceed() (3)
productService.addProduct(product)
return product
}
}
}
Defining a constructor interceptor
import io.micronaut.aop.*
import io.micronaut.context.annotation.Factory
@Factory
class ProductInterceptors(private val productService: ProductService) {
@InterceptorBean(ProductBean::class)
fun aroundConstruct(): ConstructorInterceptor<Product> { (1)
return ConstructorInterceptor { context: ConstructorInvocationContext<Product> ->
val parameterValues = context.parameterValues (2)
val parameterValue = parameterValues[0]
require(!(parameterValue == null || parameterValues[0].toString().isEmpty())) { "Invalid product name" }
val productName = parameterValues[0].toString().toUpperCase()
parameterValues[0] = productName
val product = context.proceed() (3)
productService.addProduct(product)
product
}
}
}
1 | A new @InterceptorBean is defined that is a ConstructorInterceptor |
2 | The constructor parameter values can be retrieved and modified as needed |
3 | The 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
@InterceptorBean(ProductBean.class) (1)
MethodInterceptor<Product, Object> aroundInvoke() {
return context -> {
final Product product = context.getTarget();
switch (context.getKind()) {
case POST_CONSTRUCT: (2)
product.setActive(true);
return context.proceed();
case PRE_DESTROY: (3)
productService.removeProduct(product);
return context.proceed();
default:
return context.proceed();
}
};
}
Defining a constructor interceptor
@InterceptorBean(ProductBean.class) (1)
MethodInterceptor<Product, Object> aroundInvoke() {
return { context ->
final Product product = context.getTarget()
switch (context.kind) {
case InterceptorKind.POST_CONSTRUCT: (2)
product.setActive(true)
return context.proceed()
case InterceptorKind.PRE_DESTROY: (3)
productService.removeProduct(product)
return context.proceed()
default:
return context.proceed()
}
}
}
Defining a constructor interceptor
@InterceptorBean(ProductBean::class)
fun aroundInvoke(): MethodInterceptor<Product, Any> { (1)
return MethodInterceptor { context: MethodInvocationContext<Product, Any> ->
val product = context.target
return@MethodInterceptor when (context.kind) {
InterceptorKind.POST_CONSTRUCT -> { (2)
product.active = true
context.proceed()
}
InterceptorKind.PRE_DESTROY -> { (3)
productService.removeProduct(product)
context.proceed()
}
else -> context.proceed()
}
}
}
1 | A new @InterceptorBean is defined that is a MethodInterceptor |
2 | @PostConstruct interception is handled |
3 | @PreDestroy interception is handled |