Micronaut 架构

2023-03-15 11:08 更新

以下文档描述了 Micronaut 的架构,专为那些正在寻找有关 Micronaut 内部工作及其架构信息的人而设计。这不是作为最终用户开发人员文档,而是针对那些对 Micronaut 的内部工作感兴趣的人。

本文档分为几个部分,描述编译器、内省、应用程序容器、依赖注入等。

由于本文档涵盖了 Micronaut 的内部工作原理,因此引用和描述的许多 API 都被视为内部、非公共 API,并使用 @Internal 进行注释。内部 API 可以在 Micronaut 的补丁版本之间发生变化,并且不在 Micronaut 的语义版本控制发布策略的范围内。

编译

Micronaut 编译器是对现有语言编译器的一组扩展:

  • Java - Java Annotation Processing (APT) API 用于 Java & Kotlin 代码(Micronaut 的未来版本将支持 Kotlin 的 KSP)
  • Groovy - Groovy AST 转换用于参与 Groovy 代码的编译。

为了使本文档保持简单,其余部分将描述与 Java 编译器的交互。

Micronaut 编译器访问最终用户代码并生成额外的字节码,这些字节码位于同一包结构中的用户代码旁边。

使用通过标准 Java 服务加载器机制加载的 TypeElementVisitor 实现来访问用户源的 AST。

每个 TypeElementVisitor 实现都可以覆盖一个或多个接收 Element 实例的 visit* 方法。

Element API 为给定元素(类、方法、字段等)提供了对 AST 和 AnnotationMetadata 计算的语言中立抽象。

注释元数据

Micronaut 是一种基于注解的编程模型的实现。也就是说,注解构成了框架 API 设计的基础部分。

鉴于此设计决策,制定了一个编译时模型来解决在运行时评估注释的挑战。

AnnotationMetadata API 是一种在编译时和运行时由框架组件使用的结构。 AnnotationMetadata 表示特定类型、字段、构造函数、方法或 bean 属性的注释信息的计算融合,可能包括在源代码中声明的注释,也可能包括可在运行时用于实现框架逻辑的合成元注释。

当使用 Element API 访问 Micronaut 编译器中的源代码时,每个 ClassElement、FieldElement、MethodElement、ConstructorElement 和 PropertyElement 都会计算 AnnotationMetadata 的一个实例。

AnnotationMetadata API 试图解决以下挑战:

  • 注释可以从类型和接口继承到实现中。为了避免在运行时遍历类/接口层次结构的需要,Micronaut 将在构建时计算继承的注释并处理成员覆盖规则

  • 注释可以用其他注释进行注释。这些注释通常称为元注释或原型。 AnnotationMetadata API 提供了一些方法来了解特定注解是否被声明为元注解,并找出哪些注解与其他注解进行了元注解

  • 通常需要将来自不同来源的注释元数据融合在一起。例如,对于 JavaBean 属性,您希望将来自私有字段、公共 getter 和公共 setter 的元数据组合到一个视图中,否则您必须在运行时运行逻辑以以某种方式组合来自 3 个不同来源的元数据。

  • 可重复的注解被合并和归一化。如果继承,则注释从父接口或类组合,提供单个 API 来评估可重复的注释,而不需要运行时逻辑来执行规范化。

当访问类型的源时,将通过 ElementFactory API 构造 ClassElement 的实例。

ElementFactory 使用 AbstractAnnotationMetadataBuilder 的一个实例,它包含语言特定的实现来为 AST 中的底层本机类型构造 AnnotationMedata。对于 Java,这将是 javax.model.element.TypeElement。

基本流程如下图所示:

annotationmetadata

此外,AbstractAnnotationMetadataBuilder 将通过标准 Java 服务加载器机制加载以下类型的一个或多个实例,这些实例允许操纵注释在 AnnotationMetadata 中的表示方式:

  • AnnotationMapper - 一种可以将一个注解的值映射到另一个注解的类型,在 AnnotationMetadata 中保留原始注解
  • AnnotationTransformer - 一种可以将一个注释的值转换为另一个注释的类型,从 AnnotationMetadata 中消除原始注释
  • AnnotationRemapper - 一种类型,可以转换给定包中所有注释的值,从 AnnotationMetadata 中消除原始注释

请注意,在编译时,AnnotationMetadata 是可变的,并且可以通过调用 Element API 的 annotate(..) 方法通过 TypeElementVisitor 的实现进一步更改。但是,在运行时,AnnotationMetadata 是不可变且固定的。这种设计的目的是允许扩展编译器,并使 Micronaut 能够解释不同的基于注释的源代码级编程模型。

在实践中,这有效地允许将源代码级注释模型与运行时使用的注释模型分离,以便可以使用不同的注释来表示相同的注释。

例如,jakarata.inject.Inject 或 Spring 的 @Autowired 支持作为 javax.inject.Inject 的同义词,方法是将源代码级注释转换为 javax.inject.Inject,后者是运行时唯一表示的注释。

最后,Java 中的注释还允许定义默认值。这些默认值不会保留在 AnnotationMetadata 的单个实例中,而是存储在共享的静态应用程序范围映射中,供以后检索应用程序已知使用的注释。

Bean 自省

Bean Introspections 的目标是提供一种替代反射和 JDK 的 Introspector API 的方法,该 API 与最新版本的 Java 中的 java.desktop 模块耦合。

Java 中的许多库需要以编程方式发现哪些方法以某种方式表示类的属性,虽然 JavaBeans 规范试图建立标准约定,但该语言本身已经发展到包括其他结构,如将属性表示为组件的 Records。

此外,其他语言如 Kotlin 和 Groovy 对需要在框架级别支持的类属性具有原生支持。

IntrospectedTypeElementVisitor 访问类型上 @Introspected 注释的声明,并在编译时生成与每个注释类型关联的 BeanIntrospection 实现:

introspections

这一代通过 io.micronaut.inject.beans.visitor.BeanIntrospectionWriter 发生,这是一个使用 ASM 字节码生成库生成两个额外类的内部类。

例如,给定一个名为 example.Person 的类,生成的类是:

  • example.$Person$IntrospectionRef - BeanIntrospectionReference 的一个实现,它允许应用程序软加载内省而不加载所有元数据或类本身(在内省类本身不在类路径上的情况下)。由于引用是通过 ServiceLoader 加载的,因此在生成的 META-INF/services/io.micronaut.core.beans.BeanIntrospectionReference 中引用此类型的条目也会在编译时生成。

  • example.$Person$Introspection - BeanIntrospection 的一个实现,它包含实际的运行时自省信息。

以下示例演示了 BeanIntrospection API 的用法:

 Java Groovy  Kotlin 
final BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class); // (1)
Person person = introspection.instantiate("John"); // (2)
System.out.println("Hello " + person.getName());

final BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String.class); // (3)
property.set(person, "Fred"); // (4)
String name = property.get(person); // (5)
System.out.println("Hello " + person.getName());
def introspection = BeanIntrospection.getIntrospection(Person) // (1)
Person person = introspection.instantiate("John") // (2)
println("Hello $person.name")

BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String) // (3)
property.set(person, "Fred") // (4)
String name = property.get(person) // (5)
println("Hello $person.name")
val introspection = BeanIntrospection.getIntrospection(Person::class.java) // (1)
val person : Person = introspection.instantiate("John") // (2)
print("Hello ${person.name}")

val property : BeanProperty<Person, String> = introspection.getRequiredProperty("name", String::class.java) // (3)
property.set(person, "Fred") // (4)
val name = property.get(person) // (5)
print("Hello ${person.name}")
  1. BeanIntrospection 按类型查找。当发生这种情况时,将在通过 ServiceLoader 加载的 BeanIntrospectionReference 实例中搜索内省。

  2. 实例化方法允许创建实例

  3. bean 的属性可以通过可用方法之一加载,在本例中为 getRequiredProperty

  4. 引用的 BeanProperty 可用于编写可变属性

  5. 并读取可读属性

Person 类仅在调用 getBeanType() 方法时初始化。如果该类不在类路径中,则会发生 NoClassDefFoundError,为防止这种情况,开发人员可以在尝试获取类型之前调用 BeanIntrospectionReference 上的 isPresent() 方法。

BeanIntrospection 的实现执行两个关键功能:

  1. 内省包含关于特定类型的属性和构造函数参数的 Bean 元数据,这些参数是从实际实现中抽象出来的(JavaBean 属性、Java 17+ Record、Kotlin 数据类、Groovy 属性等),并且还提供对 AnnotationMetadata 的访问,而无需使用反射来加载注释本身。

  2. 内省使得能够在不使用 Java 反射的情况下实例化和读/写 bean 属性,完全基于构建时生成的信息的子集。

通过覆盖 AbstractInitializableBeanIntrospection 的 dispatchOne 方法生成优化的无反射方法调度,例如:

protected final Object dispatchOne(int propertyIndex, Object bean, Object value
) {
    switch(propertyIndex) { (1)
    case 0:
        return ((Person) bean).getName(); (2)
    case 1:
        ((Person) bean).setName((String) value); (3)
        return null;
    default:
        throw this.unknownDispatchAtIndexException(propertyIndex); (4)
    }
}
  1. 每个读取或写入方法都分配有一个索引

  2. 索引在read方法中使用,不依赖反射,直接获取值

  3. 索引用于写入方法以在不使用反射的情况下设置属性

  4. 如果索引处不存在任何属性,则会抛出异常,尽管这是实现细节,代码路径永远不会到达这一点。

使用带有索引的分派方法的方法用于避免需要为每个方法生成一个类(这会消耗更多内存)或引入 lambda 的开销。

为了启用类型实例化,io.micronaut.inject.beans.visitor.BeanIntrospectionWriter 还将生成 instantiateInternal 方法的实现,该方法包含无反射代码以根据已知的有效参数类型实例化给定类型:

public Object instantiateInternal(Object[] args) {
    return new Person(
        (String)args[0],
        (Integer)args[1]
    );
}

Bean 定义

Micronaut 是 JSR-330 依赖注入规范的实现。

依赖注入(或控制反转)是 Java 中广泛采用和常见的模式,它允许松散地解耦组件,以便轻松扩展和测试应用程序。

通过单独的编程模型,将对象连接在一起的方式与该模型中的对象本身分离。对于 Micronaut,该模型基于 JSR-330 规范中定义的注释以及位于 io.micronaut.context.annotation 包中的一组扩展注释。

这些注释由 Micronaut 编译器访问,该编译器遍历源代码语言 AST 并构建用于在运行时将对象连接在一起的模型。

重要的是要注意实际的对象连接被推迟到运行时。

对于 Java 代码,BeanDefinitionInjectProcessor(它是一个 Java 注释处理器)是从 Java 编译器为每个用 bean 定义注释注释的类调用的。

bean 定义注解的构成很复杂,因为它考虑了元注解,但通常它是用 JSR-330 bean @Scope 注解的任何注解

BeanDefinitionInjectProcessor 将访问用户代码源中的每个 bean,并使用 ASM 字节代码生成库生成额外的字节代码,该库位于同一包中的注释类旁边。

由于历史原因,依赖注入处理器不使用 TypeElementVisitor API,但将来可能会这样做

字节代码生成在 BeanDefinitionWriter 中实现,它包含“访问”定义 bean 的不同方面的方法 (BeanDefinition)。

下图说明了流程:

beanwriter

例如给定以下类型:

 Java Groovy  Kotlin 
@Singleton
public class Vehicle {
    private final Engine engine;

    public Vehicle(Engine engine) {// (3)
        this.engine = engine;
    }

    public String start() {
        return engine.start();
    }
}
@Singleton
class Vehicle {
    final Engine engine

    Vehicle(Engine engine) { // (3)
        this.engine = engine
    }

    String start() {
        engine.start()
    }
}
@Singleton
class Vehicle(private val engine: Engine) { // (3)
    fun start(): String {
        return engine.start()
    }
}

生成以下内容:

  • 一个 example.$Vehicle$Definition$Reference 类,它实现了 BeanDefinitionReference 接口,允许应用程序软加载 bean 定义而不加载所有元数据或类本身(在自省类本身不在类路径上的情况下) .由于引用是通过 ServiceLoader 加载的,因此在生成的 META-INF/services/io.micronaut.inject.BeanDefinitionReference 中引用此类型的条目也会在编译时生成。

  • 包含实际 BeanDefinition 信息的 example.$Vehicle$Definition。

BeanDefinition 是一种保存有关特定类型的元数据的类型,包括:

  • 类级别的注释元数据

  • 计算的 JSR-330 @Scope 和 @Qualifier

  • 了解可用的 InjectionPoint 实例

  • 对定义的任何 ExecutableMethod 的引用

此外,BeanDefinition 包含知道如何将 bean 连接在一起的逻辑,包括如何构造类型以及如何注入字段和/或方法。

在编译期间,ASM 字节代码库用于填充 BeanDefinition 的详细信息,包括一个构建方法,对于前面的示例,该方法如下所示:

public Vehicle build(
    BeanResolutionContext resolution, (1)
    BeanContext context,
    BeanDefinition definition) {
    Vehicle bean = new Vehicle(
        (Engine) super.getBeanForConstructorArgument( (2)
            resolution,
            context,
            0, (3)
            (Qualifier)null)
    );
    return bean;
}
  1. BeanResolutionContext 被传递来跟踪循环 bean 引用并改进错误报告。

  2. 实例化类型,并通过调用 AbstractInitializableBeanDefinition 的方法查找每个构造函数参数

  3. 在这种情况下,跟踪构造函数参数的索引

当 Java 字段或方法具有私有访问权限时,需要进行特殊处理。在这种情况下,Micronaut 别无选择,只能回退到使用 Java 反射来执行依赖注入。

配置属性处理

Micronaut 编译器处理使用元注释 @ConfigurationReader 声明的 bean,例如 @ConfigurationProperties 和 @EachProperty 与其他 bean 截然不同。

为了支持将应用程序配置绑定到使用上述注释之一进行注释的类型,每个发现的可变 bean 属性都使用带有计算和规范化属性名称的 @Property 注释进行动态注释。

例如给定以下类型:

@ConfigurationProperties Example

 Java Groovy  Kotlin 
import io.micronaut.context.annotation.ConfigurationProperties;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import java.util.Optional;

@ConfigurationProperties("my.engine") // (1)
public class EngineConfig {

    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public int getCylinders() {
        return cylinders;
    }

    public void setCylinders(int cylinders) {
        this.cylinders = cylinders;
    }

    public CrankShaft getCrankShaft() {
        return crankShaft;
    }

    public void setCrankShaft(CrankShaft crankShaft) {
        this.crankShaft = crankShaft;
    }

    @NotBlank // (2)
    private String manufacturer = "Ford"; // (3)

    @Min(1L)
    private int cylinders;

    private CrankShaft crankShaft = new CrankShaft();

    @ConfigurationProperties("crank-shaft")
    public static class CrankShaft { // (4)

        private Optional<Double> rodLength = Optional.empty(); // (5)

        public Optional<Double> getRodLength() {
            return rodLength;
        }

        public void setRodLength(Optional<Double> rodLength) {
            this.rodLength = rodLength;
        }
    }
}
import io.micronaut.context.annotation.ConfigurationProperties

import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank

@ConfigurationProperties('my.engine') // (1)
class EngineConfig {

    @NotBlank // (2)
    String manufacturer = "Ford" // (3)

    @Min(1L)
    int cylinders

    CrankShaft crankShaft = new CrankShaft()

    @ConfigurationProperties('crank-shaft')
    static class CrankShaft { // (4)
        Optional<Double> rodLength = Optional.empty() // (5)
    }
}
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.Optional
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank

@ConfigurationProperties("my.engine") // (1)
class EngineConfig {

    @NotBlank // (2)
    var manufacturer = "Ford" // (3)

    @Min(1L)
    var cylinders: Int = 0

    var crankShaft = CrankShaft()

    @ConfigurationProperties("crank-shaft")
    class CrankShaft { // (4)
        var rodLength: Optional<Double> = Optional.empty() // (5)
    }
}

setManufacturer(String) 方法将使用 @Property(name="my.engine.manufacturer") 注释,其值将从配置的环境中解析。

AbstractInitializableBeanDefinition 的 injectBean 方法随后被逻辑覆盖,以处理从当前 BeanContext 中查找规范化属性名称 my.engine.manufacturer 并注入值(如果它以无反射方式存在)。

属性名称被规范化为 kebab 大小写(小写连字符分隔),这是用于存储其值的格式。

配置属性注入

@Generated
protected Object injectBean(
    BeanResolutionContext resolution,
    BeanContext context,
    Object bean) {
    if (this.containsProperties(resolution, context)) { (1)
        EngineConfig engineConfig = (EngineConfig) bean;
        if (this.containsPropertyValue(resolution, context, "my.engine.manufacturer")) { (2)
            String value = (String) super.getPropertyValueForSetter( (3)
                resolution,
                context,
                "setManufacturer",
                Argument.of(String.class, "manufacturer"), (4)
                "my.engine.manufacturer", (5)
                (String)null (6)
            )
            engineConfig.setManufacturer(value);
        }
    }
}
  1. 添加了顶级检查,以查看是否存在任何具有在 @ConfigurationProperties 注释中定义的前缀的属性。

  2. 执行检查以查看该属性是否确实存在

  3. 如果是,则通过调用 AbstractInitializableBeanDefinition 的 getPropertyValueForSetter 方法查找该值

  4. 创建一个 Argument 实例,用于转换为目标类型(在本例中为 String)。 Argument 还可能包含泛型信息。

  5. 属性的计算和规范化路径

  6. 如果 Bindable 注释用于指定默认值,则为默认值。

AOP 代理

Micronaut 支持基于注释的面向方面编程 (AOP),它允许通过使用在用户代码中定义的拦截器来装饰或引入类型行为。

AOP 术语的使用起源于 AspectJ 和 Spring 中的历史使用。

框架定义的任何注释都可以使用 @InterceptorBinding 注释进行元注释,支持不同类型的拦截,包括:

  • AROUND - 注释可用于装饰现有的方法调用
  • AROUND_CONSTRUCT - 一个注解可以用来拦截任何类型的构造
  • INTRODUCTION - 注释可用于向抽象或接口类型“引入”新行为
  • POST_CONSTRUCT - 注释可用于拦截在实例化对象后调用的@PostConstruct 调用。
  • PRE_DESTROY - 注释可用于拦截@PreDestroy 调用,这些调用是在对象即将被销毁后调用的。

Interceptor 的一个或多个实例可以与 @InterceptorBinding 相关联,允许用户实现应用横切关注点的行为。

在实现级别,Micronaut 编译器将访问使用@InterceptorBinding 进行元注释的类型,并构造一个新的 AopProxyWriter 实例,该实例使用 ASM 字节码生成库生成注释的子类(或接口情况下的实现)类型。

Micronaut 绝不会修改现有的用户字节代码,使用构建时生成的代理允许 Micronaut 生成附加代码,这些代码与用户代码并存并增强行为。然而,这种方法确实有局限性,例如,它要求带注释的类型是非最终类型,并且 AOP 建议不能应用于最终类型或有效的最终类型,例如 Java 17 Records。

例如给出以下注释:

Around Advice Annotation Example

 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 注释,它本身用 @InterceptorBinding(kind=AROUND) 注释,可以被认为是为 AROUND 建议定义 @InterceptorBinding 的简单快捷方式。

当此注释用于类型或方法时,例如:

Around Advice Usage Example

 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")
    }
}

编译器将访问该类型,AopProxyWriter 将使用 ASM 字节码生成库生成额外的字节码。

在编译过程中,AopProxyWriter 实例实质上代理了 BeanDefinitionWriter(参见 Bean 定义),用额外的行为装饰现有的字节码生成。下图说明了这一点:


BeanDefinitionWriter 将生成为每个 bean 生成的常规类,包括:

  • $NotNullExample$Definition.class - 原始未修饰的 bean 定义
  • $NotNullExample$Definition$Exec.class - ExecutableMethodsDefinition 的实现包含允许在不使用反射的情况下分派到每个拦截方法的逻辑。

AopProxyWriter 将修饰此行为并生成 3 个额外的类:

  • $NotNullExample$Definition$Intercepted.class - 装饰类的子类,它持有对应用的 MethodInterceptor 实例的引用并覆盖所有拦截的方法,构造 MethodInterceptorChain 实例并调用应用的拦截器
  • $NotNullExample$Definition$Intercepted$Definition.class - BeanDefinition 是原始未修饰的 bean 定义的子类。
  • $NotNullExample$Definition$Intercepted$Definition$Reference.class - 能够软加载拦截的 BeanDefinition 的 BeanDefinitionReference。

生成的大部分类都是用于加载和解析 BeanDefinition 的元数据。实际构建时代理是以 $Intercepted 结尾的类。该类实现了 Intercepted 接口并将代理类型子类化,覆盖任何非最终和非私有方法以调用 MethodInterceptorChain。

一个实现将创建一个构造函数,用于连接截获类型的依赖项,如下所示:

An intercepted type constructor

@Generated
class $NotNullExample$Definition$Intercepted
extends NotNullExample implements Intercepted { (1)
    private final Interceptor[][] $interceptors = new Interceptor[1][];
    private final ExecutableMethod[] $proxyMethods = new ExecutableMethod[1];

    public $NotNullExample$Definition$Intercepted(
        BeanResolutionContext resolution,
        BeanContext context,
        Qualifier qualifier,
        List<Interceptor> interceptors) {
        Exec executableMethods = new Exec(true); (2)
        this.$proxyMethods[0] = executableMethods.getExecutableMethodByIndex(0); (3)
        this.$interceptors[0] = InterceptorChain
            .resolveAroundInterceptors(
                context,
                this.$proxyMethods[0],
                interceptors
        );  (4)
    }
}
  1. @Generated 子类扩展自装饰类型并实现 Intercepted 接口

  2. 构造 ExecutableMethodsDefinition 的实例以将无反射调度程序解析为原始方法。

  3. 名为 $proxyMethods 的内部数组包含对用于代理调用的每个 ExecutableMethod 实例的引用。

  4. 一个名为 $interceptors 的内部数组包含对每个方法应用拦截器实例的引用,因为 @InterceptorBinding 可以是类型或方法级别,这些可能因每个方法而异。

具有与之关联的 @InterceptorBinding 的代理类型的每个非最终和非私有方法(类型级别或方法级别)都被代理原始方法的逻辑覆盖,例如:

@Overrides
public void doWork(String taskName) {
    ExecutableMethod method = this.$proxyMethods[0];
    Interceptor[] interceptors = this.$interceptors[0]; (1)
    MethodInterceptorChain chain = new MethodInterceptorChain( (2)
        interceptors,
        this,
        method,
        new Object[]{taskName}
    );
    chain.proceed(); (3)
}
  1. 该方法的 ExecutableMethod 和 Interceptor 实例数组位于。

  2. 一个新的 MethodInterceptorChain 是用拦截器、对被拦截实例的引用、方法和参数构造的。

  3. 在 MethodInterceptorChain 上调用 proceed() 方法。

请注意,@Around 注释的默认行为是通过生成的允许访问超级实现的合成桥方法调用超级实现来调用目标类型的原始重写方法(在上述情况下为 NotNullExample)。

在这种安排中,代理和代理目标是同一个对象,拦截器被调用,并且在上述情况下调用 proceed() 通过调用 super.doWork() 调用原始实现。

但是,可以使用 @Around 注释自定义此行为。

通过设置 @Around(proxyTarget=true) ,生成的代码还将实现 InterceptedProxy 接口,该接口定义了一个名为 interceptedTarget() 的方法,该方法解析代理应将方法调用委托给的目标对象。

默认行为 (proxyTarget=false) 在内存方面更有效,因为只需要一个 BeanDefinition 和一个代理类型的实例。

代理目标的评估是急切的并且在首次创建代理时完成,但是可以通过设置 @Around(lazy=true, proxyTarget=true) 使其变得惰性,在这种情况下代理只会在代理方法被检索时被检索调用。

下图说明了使用 proxyTarget=true 代理目标之间的行为差​​异:

aop proxies

图左侧的序列 (proxyTarget=false) 通过调用 super 调用代理方法,而右侧的序列从 BeanContext 中查找代理目标并调用目标上的方法。

最后一个自定义选项是 @Around(hotswap=true) ,它触发编译器生成一个编译时代理,该代理实现 HotSwappableInterceptedProxy,它定义了一个名为 swap(..) 的方法,允许用新实例换出代理的目标(为了使其成为线程安全的,生成的代码使用了 ReentrantReadWriteLock)。

安全注意事项

通过 AROUND 建议进行的方法拦截通常用于定义解决横切关注点的逻辑,其中之一就是安全性。

当多个拦截器实例应用于单个方法时,从安全角度来看,这些拦截器以特定顺序执行可能很重要。

Interceptor 接口扩展了 Ordered 接口,使开发人员能够通过覆盖 getOrder() 方法来控制拦截器排序。

当构造 MethodInterceptorChain 并且存在多个拦截器时,它们将按照最先执行的最高优先级拦截器的顺序排列。

为了帮助定义自己的 Around Advice 的开发人员,InterceptPhase 枚举定义了各种可用于正确声明 getOrder() 值的常量(例如,安全性通常属于 VALIDATE 阶段)。

可以为 io.micronaut.aop.chain 包启用跟踪级别的日志记录,以调试已​​解析的拦截器顺序。

应用上下文

一旦 Micronaut 编译器的工作完成并生成了所需的类,BeanContext 就会加载这些类以供运行时执行。

虽然标准的 Java 服务加载器机制用于定义 BeanDefinitionReference 的实例,但实例本身是用 SoftServiceLoader 加载的,这是一个更宽松的实现,允许在加载之前检查服务是否实际存在,并且还允许并行加载服务。

BeanContext 执行以下步骤:

  1. 并行软加载所有 BeanDefinitionReference 实例

  2. 实例化所有用@Context 注释的bean(bean 作用域为整个上下文)

  3. 为每个发现的已处理 ExecutableMethod 运行每个 ExecutableMethodProcessor。如果一个方法使用@Executable(processOnStartup = true) 进行元注释,则该方法被视为“已处理”

  4. 当上下文启动时,在 StartupEvent 类型上发布一个事件。

基本流程如下图所示:


ApplicationContext 是 BeanContext 的一个特殊版本,它添加了一个或多个活动环境(由 Environment 封装)和基于此环境的条件 bean 加载的概念。

环境是从一个或多个已定义的 PropertySource 实例加载的,这些实例是通过标准 Java 服务加载器机制通过加载 PropertySourceLoader 的实例发现的。

开发人员可以通过添加额外的实现和引用此类的相关 META-INF/services/io.micronaut.context.env.PropertySourceLoader 文件来扩展 Micronaut 以通过完全自定义的机制加载 PropertySource。

BeanContext 和 ApplicationContext 之间的高级差异如下所示:

applicationcontext

如上所示,ApplicationContext 加载了用于多种用途的环境,包括:

  • 通过 Bean Requirements 启用和禁用 beans

  • 允许通过@Value 或@Property 依赖注入配置

  • 允许绑定配置属性

HTTP服务器

Micronaut HTTP 服务器可以被认为是一个 Micronaut 模块——它是 Micronaut 的一个组件,它建立在包括依赖注入和 ApplicationContext 的生命周期在内的基本构建块之上。

HTTP 服务器包括一组抽象接口和公共代码,分别包含在 micronaut-http 和 micronaut-http-server 模块中(前者包括在客户端和服务器之间共享的 HTTP 原语)。

这些接口的默认实现是基于 Netty I/O 工具包提供的,其架构如下图所示:

components

Netty API 通常是一个非常低级的 I/O 网络 API,专为集成商设计,用于构建呈现更高抽象层的客户端和服务器。 Micronaut HTTP 服务器就是这样一个抽象层。

Micronaut HTTP 服务器的架构图及其实现中使用的组件描述如下:

httpserver

运行服务器的主要入口点是实现 ApplicationContextBuilder 的 Micronaut 类。通常,开发人员将以下调用置于其应用程序的主入口点:

定义主入口点

public static void main(String[] args) {
    Micronaut.run(Application.class, args);
}

传递的参数转换为 CommandLinePropertySource 并可通过 @Value 进行依赖注入。

执行运行将使用默认设置启动 Micronaut ApplicationContext,然后搜索类型为 EmbeddedServer 的 bean,它是一个接口,用于公开有关可运行服务器的信息,包括主机和端口信息。这种设计将 Micronaut 与实际的服务器实现分离,虽然默认服务器是 Netty(如上所述),但第三方只需提供 EmbeddedServer 的实现即可实现其他服务器。

服务器启动的顺序图如下所示:

embeddedserver

在 Netty 实现的情况下,EmbeddedServer 接口由 NettyHttpServer 实现。

服务器配置

NettyHttpServer 读取服务器配置,包括:

  • NettyHttpServerConfiguration - HttpServerConfiguration 的扩展版本,它定义了主机、端口等之外的特定于 Netty 的配置选项。
  • EventLoopGroupConfiguration - 配置一个或多个 Netty EventLoopGroup,可以将其配置为对服务器唯一或与一个或多个 HTTP 客户端共享。
  • ServerSslConfiguration - 为 ServerSslBuilder 提供配置,以将 Netty SslContext 配置为用于 HTTPS。

服务器配置安全注意事项

Netty 的 SslContext 提供了一个抽象,允许使用 JDK 提供的 javax.net.ssl.SSLContext 或 OpenSslEngine,这需要开发人员额外添加 netty-tcnative 作为依赖项(netty-tcnative 是 Tomcat 的 OpenSSL 绑定的一个分支)。

ServerSslConfiguration 允许将应用程序配置到磁盘上存在有效证书的安全、可读位置,以通过从磁盘加载配置来正确配置 javax.net.ssl.TrustManagerFactory 和 javax.net.ssl.KeyManagerFactory。

Netty 服务器初始化

当 NettyHttpServer 执行 start() 序列时,它将执行以下步骤:

  1. 读取 EventLoopGroupConfiguration 并创建启动 Netty 服务器所需的父 EventLoopGroup 实例和工作实例。

  2. 计算要使用的特定于平台的 ServerSocketChannel(取决于操作系统,这可能是 Epoll 或 KQueue,如果没有本机绑定是可能的,则回退到 Java NIO)

  3. 创建用于初始化 SocketChannel(客户端和服务器之间的连接)的 ServerBootstrap 实例。

  4. SocketChannel 由 Netty ChannelInitializer 初始化,它创建自定义的 Netty ChannelPipeline,用于 Micronaut 到服务器 HTTP/1.1 或 HTTP/2 请求,具体取决于配置。

  5. Netty ServerBootstrap 绑定到一个或多个配置的端口,有效地使服务器可用于接收请求。

  6. 触发了两个 Bean 事件,第一个是 ServerStartupEvent 以指示服务器已启动,然后最后在处理完所有这些事件后,仅当属性 micronaut.application.name 已设置时才会触发 ServiceReadyEvent。

此启动顺序如下所示:

nettybootstrap

NettyHttpServerInitializer 类用于初始化处理传入 HTTP/1.1 或 HTTP/2 请求的 ChannelPipeline。

ChannelPipeline 安全注意事项

用户可以通过实现实现 ChannelPipelineCustomizer 接口的 bean 并向管道添加新的 Netty ChannelHandler 来自定义 ChannelPipeline。

添加 ChannelHandler 允许执行诸如传入和传出数据包的线级日志记录之类的任务,并且可以在需要线级安全要求时使用,例如验证传入请求正文或传出响应正文的字节。

Netty 服务器路由

Micronaut 定义了一组 HTTP 注释,允许将用户代码绑定到传入的 HttpRequest 实例并自定义生成的 HttpResponse。

一个或多个已配置的 RouteBuilder 实现构造 UriRoute 的实例,Router 组件使用该实例来路由带注释类的传入请求方法,例如:

 Java Groovy  Kotlin 
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello") // (1)
public class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    public String index() {
        return "Hello World"; // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller('/hello') // (1)
class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    String index() {
        'Hello World' // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello") // (1)
class HelloController {

    @Get(produces = [MediaType.TEXT_PLAIN]) // (2)
    fun index(): String {
        return "Hello World" // (3)
    }
}

请求绑定注释可用于将方法参数绑定到 HTTP 主体、标头、参数等,框架将在数据传递给接收方法之前自动处理正确转义数据。

传入请求由 Netty 接收,ChannelPipeline 由 NettyHttpServerInitializer 初始化。传入的原始数据包被转换为 Netty HttpRequest,随后将其包装在 Micronaut NettyHttpRequest 中,后者对底层 Netty 请求进行了抽象。

NettyHttpRequest 通过 Netty ChannelHandler 实例链传递,直到它到达 RoutingInBoundHandler,后者使用上述 Router 来匹配带有注释的 @Controller 类型的方法的请求。

RoutingInBoundHandler 委托 RouteExecutor 来实际执行路由,它处理所有逻辑以分派给带注释的 @Controller 类型的方法。

执行后,如果返回值不为空,则从 MediaTypeCodecRegistry 中查找适当的 MediaTypeCodec 以获取响应 Content-Type(默认为 application/json)。 MediaTypeCodec 用于将返回值编码为 byte[] 并将其作为结果 HttpResponse 的主体包含在内。

下图说明了传入请求的此流程:

http server requestflow

RouteExecutor 将构造一个 FilterChain 以在执行带注释的 @Controller 类型的目标方法之前执行一个或多个 HttpServerFilter。

一旦所有 HttpServerFilter 实例都已执行,RouteExecutor 将尝试满足目标方法参数的要求,包括任何 Request 绑定注释。如果不能满足参数,则将 HTTP 400 - Bad Request HttpStatus 响应返回给调用客户端。

Netty 服务器路由安全注意事项

开发人员可以使用 HttpServerFilter 实例来控制对服务器资源的访问。通过不继续执行 FilterChain,可以将替代响应(例如 403 - Forbidden)返回给客户端,禁止访问敏感资源。

请注意,HttpServerFilter 接口从 Ordered 接口扩展而来,因为 FilterChain 中经常存在多个过滤器。通过实施 getOrder() 方法,开发人员可以返回适当的优先级来控制排序。此外,ServerFilterPhase 枚举提供了一组常量,开发人员可以使用这些常量来正确定位过滤器,包括通常放置安全规则的 SECURITY 阶段。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号