阅读(4582) (0)

Micronaut Around Advice

2023-02-23 11:09:06 更新

您可能想要应用的最常见类型的通知是“Around”通知,它可以让您修饰方法的行为。

Writing Around Advice

第一步是定义一个将触发 MethodInterceptor 的注解:

Around Advice 注释示例

 Java Groovy  Kotlin 
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 {
}
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 {
}
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. 注解的保留策略应该是RUNTIME

  2. 通常,您希望能够在类或方法级别应用建议,因此目标类型是 TYPE 和 METHOD

  3. 添加 @Around 注解是为了告诉 Micronaut 这个注解是 Around advice

定义 Around 建议的下一步是实现 MethodInterceptor。例如,以下拦截器不允许具有空值的参数:

方法拦截器示例

 Java Groovy  Kotlin 
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)
    }
}
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)
    }
}
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. @InterceptorBean 注解用于指示拦截器与什么注解相关联。请注意,@InterceptorBean 是使用 @Singleton 的默认范围进行元注释的,因此如果您想要创建一个新的拦截器并将其与每个拦截的 bean 相关联,您应该使用 @Prototype 对拦截器进行注释。

  2. 拦截器实现 MethodInterceptor 接口。

  3. 传递的 MethodInvocationContext 用于查找第一个为 null 的参数

  4. 如果发现空参数,则抛出异常

  5. 否则调用 proceed() 以继续方法调用。

Micronaut AOP 拦截器不使用反射来提高性能并减少堆栈跟踪大小,从而改进调试。

将注释应用于目标类以使新的 MethodInterceptor 起作用:

环绕建议用法示例

 Java Groovy  Kotlin 
import jakarta.inject.Singleton;

@Singleton
public class NotNullExample {

    @NotNull
    void doWork(String taskName) {
        System.out.println("Doing job: " + taskName);
    }
}
import jakarta.inject.Singleton

@Singleton
class NotNullExample {

    @NotNull
    void doWork(String taskName) {
        println "Doing job: $taskName"
    }
}
import jakarta.inject.Singleton

@Singleton
open class NotNullExample {

    @NotNull
    open fun doWork(taskName: String?) {
        println("Doing job: $taskName")
    }
}

每当将 NotNullExample 类型注入到类中时,就会注入一个编译时生成的代理,该代理会使用之前定义的 @NotNull 通知来装饰方法调用。您可以通过编写测试来验证该建议是否有效。以下测试验证当参数为 null 时是否抛出了预期的异常:

Around Advice Test

 Java Groovy  Kotlin 
@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);
    }
}
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()
}
@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()
}

由于 Micronaut 注入发生在编译时,通常在编译上述测试时,建议应打包在类路径上的依赖 JAR 文件中。它不应该在同一个代码库中,因为您不希望在编译建议本身之前编译测试。

自定义代理生成

Around 注释的默认行为是在编译时生成一个代理,该代理是被代理类的子类。换句话说,在前面的示例中,将生成 NotNullExample 类的编译时子类,其中代理方法装饰有拦截器处理,并且通过调用 super 来调用原始行为。

这种行为更有效,因为只需要一个 bean 实例,但是根据用例,您可能希望改变这种行为。 @Around 注释支持各种允许您更改此行为的属性,包括:

  • proxyTarget(默认为 false)- 如果设置为 true,代理将委托给原始 bean 实例,而不是调用 super 的子类

  • hotswap(默认为 false)- 与 proxyTarget=true 相同,但另外代理实现 HotSwappableInterceptedProxy,它将每个方法调用包装在 ReentrantReadWriteLock 中,并允许在运行时交换目标实例。

  • lazy(默认为 false)- 默认情况下,Micronaut 在创建代理时急切地初始化代理目标。如果设置为 true,则代理目标将针对每个方法调用延迟解析。

@Factory Beans 上的 AOP 建议

当应用于 Bean 工厂时,AOP 建议的语义与常规 bean 不同,应用以下规则:

  1. 在 @Factory bean 的类级别应用的 AOP 建议将建议应用于工厂本身,而不应用于使用 @Bean 注释定义的任何 bean。

  2. 应用于用 bean 范围注释的方法的 AOP 建议将 AOP 建议应用于工厂生产的 bean。

考虑以下两个示例:

AOP Advice at the type level of a @Factory

 Java Groovy  Kotlin 
@Timed
@Factory
public class MyFactory {

    @Prototype
    public MyBean myBean() {
        return new MyBean();
    }
}
@Timed
@Factory
class MyFactory {

    @Prototype
    MyBean myBean() {
        new MyBean()
    }
}
@Timed
@Factory
open class MyFactory {

    @Prototype
    open fun myBean(): MyBean {
        return MyBean()
    }
}

上面的示例记录了创建 MyBean bean 所花费的时间。

现在考虑这个例子:

AOP Advice at the method level of a @Factory

 Java Groovy  Kotlin 
@Factory
public class MyFactory {

    @Prototype
    @Timed
    public MyBean myBean() {
        return new MyBean();
    }
}
@Factory
class MyFactory {

    @Prototype
    @Timed
    MyBean myBean() {
        new MyBean()
    }
}
@Factory
open class MyFactory {

    @Prototype
    @Timed
    open fun myBean(): MyBean {
        return MyBean()
    }
}

上面的示例记录了执行 MyBean bean 的公共方法所花费的时间,而不是创建 bean 所花费的时间。

这种行为的基本原理是您有时可能希望将建议应用于工厂,而在其他时候将建议应用于工厂生产的 bean。

请注意,目前无法将方法级别的建议应用于 @Factory bean,并且工厂的所有建议都必须在类型级别应用。您可以通过将未应用建议的方法定义为非公开的方法来控制哪些方法应用了建议。