Writing Around Advice
The first step is to define an annotation that will trigger a MethodInterceptor:
Around Advice Annotation Example
import io.micronaut.aop.Around;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Retention(RUNTIME) (1)
@Target({TYPE, METHOD}) (2)
@Around (3)
public @interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import java.lang.annotation.*
import static java.lang.annotation.ElementType.*
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Documented
@Retention(RUNTIME) (1)
@Target([TYPE, METHOD]) (2)
@Around (3)
@interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.FILE
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
@MustBeDocumented
@Retention(RUNTIME) (1)
@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) (2)
@Around (3)
annotation class NotNull
1 | The retention policy of the annotation should be RUNTIME |
2 | Generally you want to be able to apply advice at the class or method level so the target types are TYPE and METHOD |
3 | The @Around annotation is added to tell Micronaut that the annotation is Around advice |
The next step to defining Around advice is to implement a MethodInterceptor. For example the following interceptor disallows parameters with null
values:
MethodInterceptor Example
import io.micronaut.aop.InterceptorBean;
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.type.MutableArgumentValue;
import jakarta.inject.Singleton;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@Singleton
@InterceptorBean(NotNull.class) (1)
public class NotNullInterceptor implements MethodInterceptor<Object, Object> { (2)
@Nullable
@Override
public Object intercept(MethodInvocationContext<Object, Object> context) {
Optional<Map.Entry<String, MutableArgumentValue<?>>> nullParam = context.getParameters()
.entrySet()
.stream()
.filter(entry -> {
MutableArgumentValue<?> argumentValue = entry.getValue();
return Objects.isNull(argumentValue.getValue());
})
.findFirst(); (3)
if (nullParam.isPresent()) {
throw new IllegalArgumentException("Null parameter [" + nullParam.get().getKey() + "] not allowed"); (4)
}
return context.proceed(); (5)
}
}
MethodInterceptor Example
import io.micronaut.aop.InterceptorBean
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import io.micronaut.core.annotation.Nullable
import io.micronaut.core.type.MutableArgumentValue
import jakarta.inject.Singleton
@Singleton
@InterceptorBean(NotNull) (1)
class NotNullInterceptor implements MethodInterceptor<Object, Object> { (2)
@Nullable
@Override
Object intercept(MethodInvocationContext<Object, Object> context) {
Optional<Map.Entry<String, MutableArgumentValue<?>>> nullParam = context.parameters
.entrySet()
.stream()
.filter({entry ->
MutableArgumentValue<?> argumentValue = entry.value
return Objects.isNull(argumentValue.value)
})
.findFirst() (3)
if (nullParam.present) {
throw new IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") (4)
}
return context.proceed() (5)
}
}
MethodInterceptor Example
import io.micronaut.aop.InterceptorBean
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import java.util.Objects
import jakarta.inject.Singleton
@Singleton
@InterceptorBean(NotNull::class) (1)
class NotNullInterceptor : MethodInterceptor<Any, Any> { (2)
override fun intercept(context: MethodInvocationContext<Any, Any>): Any? {
val nullParam = context.parameters
.entries
.stream()
.filter { entry ->
val argumentValue = entry.value
Objects.isNull(argumentValue.value)
}
.findFirst() (3)
return if (nullParam.isPresent) {
throw IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") (4)
} else {
context.proceed() (5)
}
}
}
1 | The @InterceptorBean annotation is used to indicate what annotation the interceptor is associated with. Note that the javax.inject.Singleton annotation is optional, and a new interceptor would be created for each instance (prototype scope). |
2 | An interceptor implements the MethodInterceptor interface. |
3 | The passed MethodInvocationContext is used to find the first parameter that is null |
4 | If a null parameter is found an exception is thrown |
5 | Otherwise proceed() is called to proceed with the method invocation. |
Micronaut AOP interceptors use no reflection which improves performance and reducing stack trace sizes, thus improving debugging. |
Apply the annotation to target classes to put the new MethodInterceptor
to work:
Around Advice Usage Example
import jakarta.inject.Singleton;
@Singleton
public class NotNullExample {
@NotNull
void doWork(String taskName) {
System.out.println("Doing job: " + taskName);
}
}
Around Advice Usage Example
import jakarta.inject.Singleton
@Singleton
class NotNullExample {
@NotNull
void doWork(String taskName) {
println "Doing job: $taskName"
}
}
Around Advice Usage Example
import jakarta.inject.Singleton
@Singleton
open class NotNullExample {
@NotNull
open fun doWork(taskName: String?) {
println("Doing job: $taskName")
}
}
Whenever the type NotNullExample
is injected into a class, a compile-time-generated proxy is injected that decorates method calls with the @NotNull
advice defined earlier. You can verify that the advice works by writing a test. The following test verifies that the expected exception is thrown when the argument is null
:
Around Advice Test
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testNotNull() {
try (ApplicationContext applicationContext = ApplicationContext.run()) {
NotNullExample exampleBean = applicationContext.getBean(NotNullExample.class);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Null parameter [taskName] not allowed");
exampleBean.doWork(null);
}
}
Around Advice Test
void "test not null"() {
when:
def applicationContext = ApplicationContext.run()
def exampleBean = applicationContext.getBean(NotNullExample)
exampleBean.doWork(null)
then:
IllegalArgumentException e = thrown()
e.message == 'Null parameter [taskName] not allowed'
cleanup:
applicationContext.close()
}
Around Advice Test
@Test
fun testNotNull() {
val applicationContext = ApplicationContext.run()
val exampleBean = applicationContext.getBean(NotNullExample::class.java)
val exception = shouldThrow<IllegalArgumentException> {
exampleBean.doWork(null)
}
exception.message shouldBe "Null parameter [taskName] not allowed"
applicationContext.close()
}
Since Micronaut injection happens at compile time, generally the advice should be packaged in a dependent JAR file that is on the classpath when the above test is compiled. It should not be in the same codebase since you don’t want the test to be compiled before the advice itself is compiled. |