Writing Around Advice

The first step is to define an annotation that will trigger a MethodInterceptor:

Around Advice Annotation Example

  1. import io.micronaut.aop.Around;
  2. import java.lang.annotation.*;
  3. import static java.lang.annotation.ElementType.*;
  4. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  5. @Documented
  6. @Retention(RUNTIME) (1)
  7. @Target({TYPE, METHOD}) (2)
  8. @Around (3)
  9. public @interface NotNull {
  10. }

Around Advice Annotation Example

  1. import io.micronaut.aop.Around
  2. import java.lang.annotation.*
  3. import static java.lang.annotation.ElementType.*
  4. import static java.lang.annotation.RetentionPolicy.RUNTIME
  5. @Documented
  6. @Retention(RUNTIME) (1)
  7. @Target([TYPE, METHOD]) (2)
  8. @Around (3)
  9. @interface NotNull {
  10. }

Around Advice Annotation Example

  1. import io.micronaut.aop.Around
  2. import kotlin.annotation.AnnotationRetention.RUNTIME
  3. import kotlin.annotation.AnnotationTarget.CLASS
  4. import kotlin.annotation.AnnotationTarget.FILE
  5. import kotlin.annotation.AnnotationTarget.FUNCTION
  6. import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
  7. import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
  8. @MustBeDocumented
  9. @Retention(RUNTIME) (1)
  10. @Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) (2)
  11. @Around (3)
  12. annotation class NotNull
1The retention policy of the annotation should be RUNTIME
2Generally you want to be able to apply advice at the class or method level so the target types are TYPE and METHOD
3The @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

  1. import io.micronaut.aop.InterceptorBean;
  2. import io.micronaut.aop.MethodInterceptor;
  3. import io.micronaut.aop.MethodInvocationContext;
  4. import io.micronaut.core.annotation.Nullable;
  5. import io.micronaut.core.type.MutableArgumentValue;
  6. import jakarta.inject.Singleton;
  7. import java.util.Map;
  8. import java.util.Objects;
  9. import java.util.Optional;
  10. @Singleton
  11. @InterceptorBean(NotNull.class) (1)
  12. public class NotNullInterceptor implements MethodInterceptor<Object, Object> { (2)
  13. @Nullable
  14. @Override
  15. public Object intercept(MethodInvocationContext<Object, Object> context) {
  16. Optional<Map.Entry<String, MutableArgumentValue<?>>> nullParam = context.getParameters()
  17. .entrySet()
  18. .stream()
  19. .filter(entry -> {
  20. MutableArgumentValue<?> argumentValue = entry.getValue();
  21. return Objects.isNull(argumentValue.getValue());
  22. })
  23. .findFirst(); (3)
  24. if (nullParam.isPresent()) {
  25. throw new IllegalArgumentException("Null parameter [" + nullParam.get().getKey() + "] not allowed"); (4)
  26. }
  27. return context.proceed(); (5)
  28. }
  29. }

MethodInterceptor Example

  1. import io.micronaut.aop.InterceptorBean
  2. import io.micronaut.aop.MethodInterceptor
  3. import io.micronaut.aop.MethodInvocationContext
  4. import io.micronaut.core.annotation.Nullable
  5. import io.micronaut.core.type.MutableArgumentValue
  6. import jakarta.inject.Singleton
  7. @Singleton
  8. @InterceptorBean(NotNull) (1)
  9. class NotNullInterceptor implements MethodInterceptor<Object, Object> { (2)
  10. @Nullable
  11. @Override
  12. Object intercept(MethodInvocationContext<Object, Object> context) {
  13. Optional<Map.Entry<String, MutableArgumentValue<?>>> nullParam = context.parameters
  14. .entrySet()
  15. .stream()
  16. .filter({entry ->
  17. MutableArgumentValue<?> argumentValue = entry.value
  18. return Objects.isNull(argumentValue.value)
  19. })
  20. .findFirst() (3)
  21. if (nullParam.present) {
  22. throw new IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") (4)
  23. }
  24. return context.proceed() (5)
  25. }
  26. }

MethodInterceptor Example

  1. import io.micronaut.aop.InterceptorBean
  2. import io.micronaut.aop.MethodInterceptor
  3. import io.micronaut.aop.MethodInvocationContext
  4. import java.util.Objects
  5. import jakarta.inject.Singleton
  6. @Singleton
  7. @InterceptorBean(NotNull::class) (1)
  8. class NotNullInterceptor : MethodInterceptor<Any, Any> { (2)
  9. override fun intercept(context: MethodInvocationContext<Any, Any>): Any? {
  10. val nullParam = context.parameters
  11. .entries
  12. .stream()
  13. .filter { entry ->
  14. val argumentValue = entry.value
  15. Objects.isNull(argumentValue.value)
  16. }
  17. .findFirst() (3)
  18. return if (nullParam.isPresent) {
  19. throw IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") (4)
  20. } else {
  21. context.proceed() (5)
  22. }
  23. }
  24. }
1The @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).
2An interceptor implements the MethodInterceptor interface.
3The passed MethodInvocationContext is used to find the first parameter that is null
4If a null parameter is found an exception is thrown
5Otherwise 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

  1. import jakarta.inject.Singleton;
  2. @Singleton
  3. public class NotNullExample {
  4. @NotNull
  5. void doWork(String taskName) {
  6. System.out.println("Doing job: " + taskName);
  7. }
  8. }

Around Advice Usage Example

  1. import jakarta.inject.Singleton
  2. @Singleton
  3. class NotNullExample {
  4. @NotNull
  5. void doWork(String taskName) {
  6. println "Doing job: $taskName"
  7. }
  8. }

Around Advice Usage Example

  1. import jakarta.inject.Singleton
  2. @Singleton
  3. open class NotNullExample {
  4. @NotNull
  5. open fun doWork(taskName: String?) {
  6. println("Doing job: $taskName")
  7. }
  8. }

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

  1. @Rule
  2. public ExpectedException thrown = ExpectedException.none();
  3. @Test
  4. public void testNotNull() {
  5. try (ApplicationContext applicationContext = ApplicationContext.run()) {
  6. NotNullExample exampleBean = applicationContext.getBean(NotNullExample.class);
  7. thrown.expect(IllegalArgumentException.class);
  8. thrown.expectMessage("Null parameter [taskName] not allowed");
  9. exampleBean.doWork(null);
  10. }
  11. }

Around Advice Test

  1. void "test not null"() {
  2. when:
  3. def applicationContext = ApplicationContext.run()
  4. def exampleBean = applicationContext.getBean(NotNullExample)
  5. exampleBean.doWork(null)
  6. then:
  7. IllegalArgumentException e = thrown()
  8. e.message == 'Null parameter [taskName] not allowed'
  9. cleanup:
  10. applicationContext.close()
  11. }

Around Advice Test

  1. @Test
  2. fun testNotNull() {
  3. val applicationContext = ApplicationContext.run()
  4. val exampleBean = applicationContext.getBean(NotNullExample::class.java)
  5. val exception = shouldThrow<IllegalArgumentException> {
  6. exampleBean.doWork(null)
  7. }
  8. exception.message shouldBe "Null parameter [taskName] not allowed"
  9. applicationContext.close()
  10. }
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.