阅读(4416) (0)

Micronaut Bean 注释元数据

2023-02-23 10:59:15 更新

Java 的 AnnotatedElement API 提供的方法通常不提供在不加载注释本身的情况下内省注释的能力。它们也不提供任何内省注释构造型的能力(通常称为元注释;注释构造型是用另一个注释对一个注释进行注释的地方,本质上是继承它的行为)。

为了解决这个问题,许多框架生成运行时元数据或执行昂贵的反射来分析类的注释。

Micronaut 改为在编译时生成此注解元数据,从而避免了昂贵的反射并节省了内存。

BeanContext API 可用于获取对实现 AnnotationMetadata 接口的 BeanDefinition 的引用。

例如,以下代码获取所有带有特定构造型注释的 bean 定义:

按构造型查找 Bean 定义

BeanContext beanContext = ... // obtain the bean context
Collection<BeanDefinition> definitions =
    beanContext.getBeanDefinitions(Qualifiers.byStereotype(Controller.class))

for (BeanDefinition definition : definitions) {
    AnnotationValue<Controller> controllerAnn = definition.getAnnotation(Controller.class);
    // do something with the annotation
}

上面的示例找到所有用 @Controller 注释的 BeanDefinition 实例,无论 @Controller 是直接使用还是通过注释构造型继承。

请注意,getAnnotation 方法和该方法的变体返回 AnnotationValue 类型而不是 Java 注释。这是设计使然,您通常应该在读取注释值时尝试使用此 API,因为从性能和内存消耗的角度来看,合成代理实现更糟糕。

如果您需要对注释实例的引用,您可以使用 synthesize 方法,它创建一个实现注释接口的运行时代理:

合成注解实例

Controller controllerAnn = definition.synthesize(Controller.class);

但是,不推荐使用这种方法,因为它需要反射并由于使用运行时生成的代理而增加内存消耗,并且应该作为最后的手段使用,例如,如果您需要注释的实例来与第三方集成图书馆。

注解继承

Micronaut 将遵守 Java 的 AnnotatedElement API 中定义的关于注解继承的规则:

  • 使用 Inherited 进行元注释的注释将通过 AnnotationMetadata API 的 getAnnotation* 方法提供,而直接声明的注释则通过 getDeclaredAnnotation* 方法提供。

  • 未使用 Inherited 进行元注释的注释将不会包含在元数据中

Micronaut 与 AnnotatedElement API 的不同之处在于它将这些规则扩展到方法和方法参数,以便:

  • 任何用 Inherited 注释并出现在被子接口或类 B 覆盖的接口或超类 A 的方法上的注释都将继承到可通过 ExecutableMethod API 从 BeanDefinition 或 AOP 拦截器检索的 AnnotationMetadata 中。

  • 任何用 Inherited 注释并出现在被子接口或类 B 覆盖的接口或超类 A 的方法参数上的注释都将继承到可通过 Argument 接口从 ExecutableMethod API 的 getArguments 方法检索的 AnnotationMetadata 中。

通常,您可能希望覆盖的一般行为默认情况下不会继承,包括 Bean 范围、Bean 限定符、Bean 条件、验证规则等。

如果您希望在子类化时继承特定的范围、限定符或一组要求,那么您可以定义一个用@Inherited 注释的元注释。例如:

定义继承的元注解

 Java Groovy  Kotlin 
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.AnnotationMetadata;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Inherited // (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
public @interface SqlRepository {
    @AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) // (5)
    String value() default "";
}
import io.micronaut.context.annotation.AliasFor
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.AnnotationMetadata
import jakarta.inject.Named
import jakarta.inject.Singleton

import java.lang.annotation.Inherited
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

@Inherited // (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
@interface SqlRepository {
    @AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) // (5)
    String value() default "";
}
import io.micronaut.context.annotation.Requires
import jakarta.inject.Named
import jakarta.inject.Singleton
import java.lang.annotation.Inherited

@Inherited // (1)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
annotation class SqlRepository(
    val value: String = ""
)
  1. 注解声明为@Inherited

  2. Bean Conditions 将被子类继承

  3. Bean 限定符将由子类继承

  4. Bean Scopes 将被子类继承

  5. 你也可以给注解起别名,它们会被继承

使用此元注释,您可以将注释添加到超类:

在超类上使用继承的元注释

 Java Groovy  Kotlin 
@SqlRepository
public abstract class BaseSqlRepository {
}
@SqlRepository
abstract class BaseSqlRepository {
}
@SqlRepository
abstract class BaseSqlRepository

然后子类将继承所有注释:

在子类中继承注解

 Java Groovy  Kotlin 
import jakarta.inject.Named;
import javax.sql.DataSource;

@Named("bookRepository")
public class BookRepository extends BaseSqlRepository {
    private final DataSource dataSource;

    public BookRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
import jakarta.inject.Named
import javax.sql.DataSource

@Named("bookRepository")
class BookRepository extends BaseSqlRepository {
    private final DataSource dataSource

    BookRepository(DataSource dataSource) {
        this.dataSource = dataSource
    }
}
import jakarta.inject.Named
import javax.sql.DataSource

@Named("bookRepository")
class BookRepository(private val dataSource: DataSource) : BaseSqlRepository()

子类必须至少有一个 bean 定义注释,例如范围或限定符。

别名/映射注释

有时您可能希望将注释成员的值作为另一个注释成员的值的别名。为此,请使用@AliasFor 注释。

例如,一个常见的用例是注释定义了 value() 成员,但也支持其他成员。例如 @Client 注解:

@Client 注解

public @interface Client {

    /**
     * @return The URL or service ID of the remote service
     */
    @AliasFor(member = "id") (1)
    String value() default "";

    /**
     * @return The ID of the client
     */
    @AliasFor(member = "value") (2)
    String id() default "";
}
  1. value 成员也设置了 id 成员

  2. id 成员也设置了 value 成员

有了这些别名,无论您定义@Client("foo") 还是@Client(id="foo"),value 和 id 成员都将被设置,从而更容易解析和使用注释。

如果您无法控制注释,另一种方法是使用 AnnotationMapper。要创建 AnnotationMapper,请执行以下操作:

  • 实现 AnnotationMapper 接口

  • 定义一个 META-INF/services/io.micronaut.inject.annotation.AnnotationMapper 文件引用实现类

  • 将包含实现的 JAR 文件添加到 annotationProcessor 类路径(Kotlin 的 kapt)

因为 AnnotationMapper 实现必须在注释处理器类路径上,所以它们通常应该在一个包含很少外部依赖项的项目中,以避免污染注释处理器类路径。

以下是改进 JPA 实体的内省功能的示例 AnnotationMapper。

EntityIntrospectedAnnotationMapper 映射器示例

public class EntityIntrospectedAnnotationMapper implements NamedAnnotationMapper {
    @NonNull
    @Override
    public String getName() {
        return "javax.persistence.Entity";
    }

    @Override
    public List<AnnotationValue<?>> map(AnnotationValue<Annotation> annotation, VisitorContext visitorContext) { (1)
        final AnnotationValueBuilder<Introspected> builder = AnnotationValue.builder(Introspected.class)
                // don't bother with transients properties
                .member("excludedAnnotations", "javax.persistence.Transient"); (2)
        return Arrays.asList(
                builder.build(),
                AnnotationValue.builder(ReflectiveAccess.class).build()
        );
    }
}
  1. map 方法接收带有注释值的 AnnotationValue。

  2. 可以返回一个或多个注释,在本例中为@Transient。

上面的示例实现了 NamedAnnotationMapper 接口,该接口允许将注释与运行时代码混合。要针对具体注释类型进行操作,请改用 TypedAnnotationMapper,但请注意,它需要注释类本身位于注释处理器类路径中。