Writing Around Advice
The first 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.*;
import io.micronaut.core.type.MutableArgumentValue;
import javax.inject.Singleton;
import java.util.*;
@Singleton
public class NotNullInterceptor implements MethodInterceptor<Object, Object> { (1)
@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(); (2)
if (nullParam.isPresent()) {
throw new IllegalArgumentException("Null parameter [" + nullParam.get().getKey() + "] not allowed"); (3)
} else {
return context.proceed(); (4)
}
}
}
MethodInterceptor Example
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import io.micronaut.core.type.MutableArgumentValue
import javax.inject.Singleton
@Singleton
class NotNullInterceptor implements MethodInterceptor<Object, Object> { (1)
@Override
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() (2)
if (nullParam.isPresent()) {
throw new IllegalArgumentException("Null parameter [" + nullParam.get().getKey() + "] not allowed") (3)
} else {
return context.proceed() (4)
}
}
}
MethodInterceptor Example
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import io.micronaut.core.type.MutableArgumentValue
import javax.inject.Singleton
import java.util.Objects
import java.util.Optional
@Singleton
class NotNullInterceptor : MethodInterceptor<Any, Any> { (1)
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() (2)
return if (nullParam.isPresent) {
throw IllegalArgumentException("Null parameter [" + nullParam.get().key + "] not allowed") (3)
} else {
context.proceed() (4)
}
}
}
1 | An interceptor implements the MethodInterceptor interface |
2 | The passed MethodInvocationContext is used to find the first parameter that is null |
3 | If a null parameter is found an exception is thrown |
4 | 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. |
To put the new MethodInterceptor
to work the next step is to define an annotation that will trigger the MethodInterceptor
:
Around Advice Annotation Example
import io.micronaut.context.annotation.Type;
import io.micronaut.aop.Around;
import java.lang.annotation.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Retention(RUNTIME) (1)
@Target({ElementType.TYPE, ElementType.METHOD}) (2)
@Around (3)
@Type(NotNullInterceptor.class) (4)
public @interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import io.micronaut.context.annotation.Type
import java.lang.annotation.Documented
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.Target
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Documented
@Retention(RUNTIME) (1)
@Target([ElementType.TYPE, ElementType.METHOD]) (2)
@Around (3)
@Type(NotNullInterceptor.class) (4)
@interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import io.micronaut.context.annotation.Type
import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy.RUNTIME
@Documented
@Retention(RUNTIME) (1)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) (2)
@Around (3)
@Type(NotNullInterceptor::class) (4)
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 |
4 | The @Type annotation is used to configure which type implements the advice (in this case the previously defined NotNullInterceptor ) |
With the interceptor and annotation implemented you can then simply apply the annotation to the target classes:
Around Advice Usage Example
@Singleton
public class NotNullExample {
@NotNull
void doWork(String taskName) {
System.out.println("Doing job: " + taskName);
}
}
Around Advice Usage Example
@Singleton
class NotNullExample {
@NotNull
void doWork(String taskName) {
println("Doing job: " + taskName)
}
}
Around Advice Usage Example
@Singleton
open class NotNullExample {
@NotNull
open fun doWork(taskName: String?) {
println("Doing job: $taskName")
}
}
Whenever the type NotNullExample
is injected into any class, a compile time generated proxy will instead be injected that decorates the appropriate method calls with the @NotNull
advice defined earlier. You can verify that the advice works by writing a test. The following test uses a JUnit ExpectedException
rule to verify the appropriate exception is thrown when an 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:
ApplicationContext applicationContext = ApplicationContext.run()
NotNullExample exampleBean = applicationContext.getBean(NotNullExample.class)
exampleBean.doWork(null)
then:
IllegalArgumentException ex = thrown()
ex.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 is done 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 and should not be in the same codebase since you don’t want the test to be compiled before the advice itself is compiled. |