5.2 Introduction Advice
Introduction advice is distinct from Around advice in that it involves providing an implementation instead of decorating.
Examples of introduction advice include things like GORM or Spring Data that will both automatically implement persistence logic for you.
Micronaut’s Client annotation is another example of introduction advice where Micronaut will, at compile time, implement HTTP client interfaces for you.
The way you implement Introduction advice is very similar to how you implement Around advice.
You start off by defining an annotation that will power the introduction advice. As an example, say you want to implement advice that will return a stubbed value for every method in an interface (a common requirement in testing frameworks). Consider the following @Stub
annotation:
Introduction Advice Annotation Example
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import io.micronaut.aop.Introduction;
import io.micronaut.context.annotation.Bean;
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;
@Introduction (1)
@Type(StubIntroduction.class) (2)
@Bean (3)
@Documented
@Retention(RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD})
public @interface Stub {
String value() default "";
}
Introduction Advice Annotation Example
import io.micronaut.aop.Introduction
import io.micronaut.context.annotation.Bean
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
@Introduction (1)
@Type(StubIntroduction.class) (2)
@Bean (3)
@Documented
@Retention(RUNTIME)
@Target([ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD])
@interface Stub {
String value() default ""
}
Introduction Advice Annotation Example
import io.micronaut.aop.Introduction
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Type
import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy.RUNTIME
@Introduction (1)
@Type(StubIntroduction::class) (2)
@Bean (3)
@Documented
@Retention(RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
annotation class Stub(val value: String = "")
1 | The introduction advice is annotated with Introduction |
2 | The Type annotation is used to refer to the implementor of the advice. In this case StubIntroduction |
3 | The Bean annotation is added so that all types annotated with @Stub become beans |
The StubIntroduction
class referred to in the previous example must then implement the MethodInterceptor interface, just like around advice.
The following is an example implementation:
StubIntroduction
import io.micronaut.aop.*;
import javax.inject.Singleton;
@Singleton
public class StubIntroduction implements MethodInterceptor<Object,Object> { (1)
@Override
public Object intercept(MethodInvocationContext<Object, Object> context) {
return context.getValue( (2)
Stub.class,
context.getReturnType().getType()
).orElse(null); (3)
}
}
StubIntroduction
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import javax.inject.Singleton
@Singleton
class StubIntroduction implements MethodInterceptor<Object,Object> { (1)
@Override
Object intercept(MethodInvocationContext<Object, Object> context) {
return context.getValue( (2)
Stub.class,
context.getReturnType().getType()
).orElse(null) (3)
}
}
StubIntroduction
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import javax.inject.Singleton
@Singleton
class StubIntroduction : MethodInterceptor<Any, Any> { (1)
override fun intercept(context: MethodInvocationContext<Any, Any>): Any? {
return context.getValue<Any>( (2)
Stub::class.java,
context.returnType.type
).orElse(null) (3)
}
}
1 | The class is annotated with @Singleton and implements the MethodInterceptor interface |
2 | The value of the @Stub annotation is read from the context and an attempt made to convert the value to the return type |
3 | Otherwise null is returned |
To now use this introduction advice in an application you simply annotate your abstract classes or interfaces with @Stub
:
StubExample
@Stub
public interface StubExample {
@Stub("10")
int getNumber();
LocalDateTime getDate();
}
StubExample
@Stub
interface StubExample {
@Stub("10")
int getNumber()
LocalDateTime getDate()
}
StubExample
@Stub
interface StubExample {
@get:Stub("10")
val number: Int
val date: LocalDateTime?
}
All abstract methods will delegate to the StubIntroduction
class to be implemented.
The following test demonstrates the behaviour or StubIntroduction
:
Testing Introduction Advice
StubExample stubExample = applicationContext.getBean(StubExample.class);
assertEquals(10, stubExample.getNumber());
assertNull(stubExample.getDate());
Testing Introduction Advice
when:
StubExample stubExample = applicationContext.getBean(StubExample.class)
then:
stubExample.getNumber() == 10
stubExample.getDate() == null
Testing Introduction Advice
val stubExample = applicationContext.getBean(StubExample::class.java)
stubExample.number.toLong().shouldBe(10)
stubExample.date.shouldBe(null)
Note that if the introduction advice cannot implement the method the proceed
method of the MethodInvocationContext should be called. This gives the opportunity for other introduction advice interceptors to implement the method, otherwise a UnsupportedOperationException
will be thrown if no advice can implement the method.
In addition, if multiple introduction advice are present you may wish to override the getOrder()
method of MethodInterceptor to control the priority of advise.
The following sections cover core advice types that are built into Micronaut and provided by the framework.