以下文檔描述了 Micronaut 的架構(gòu),專為那些正在尋找有關(guān) Micronaut 內(nèi)部工作及其架構(gòu)信息的人而設(shè)計。這不是作為最終用戶開發(fā)人員文檔,而是針對那些對 Micronaut 的內(nèi)部工作感興趣的人。
本文檔分為幾個部分,描述編譯器、內(nèi)省、應(yīng)用程序容器、依賴注入等。
由于本文檔涵蓋了 Micronaut 的內(nèi)部工作原理,因此引用和描述的許多 API 都被視為內(nèi)部、非公共 API,并使用 @Internal 進(jìn)行注釋。內(nèi)部 API 可以在 Micronaut 的補(bǔ)丁版本之間發(fā)生變化,并且不在 Micronaut 的語義版本控制發(fā)布策略的范圍內(nèi)。
Micronaut 編譯器是對現(xiàn)有語言編譯器的一組擴(kuò)展:
為了使本文檔保持簡單,其余部分將描述與 Java 編譯器的交互。
Micronaut 編譯器訪問最終用戶代碼并生成額外的字節(jié)碼,這些字節(jié)碼位于同一包結(jié)構(gòu)中的用戶代碼旁邊。
使用通過標(biāo)準(zhǔn) Java 服務(wù)加載器機(jī)制加載的 TypeElementVisitor 實現(xiàn)來訪問用戶源的 AST。
每個 TypeElementVisitor 實現(xiàn)都可以覆蓋一個或多個接收 Element 實例的 visit* 方法。
Element API 為給定元素(類、方法、字段等)提供了對 AST 和 AnnotationMetadata 計算的語言中立抽象。
注釋元數(shù)據(jù)
Micronaut 是一種基于注解的編程模型的實現(xiàn)。也就是說,注解構(gòu)成了框架 API 設(shè)計的基礎(chǔ)部分。
鑒于此設(shè)計決策,制定了一個編譯時模型來解決在運(yùn)行時評估注釋的挑戰(zhàn)。
AnnotationMetadata API 是一種在編譯時和運(yùn)行時由框架組件使用的結(jié)構(gòu)。 AnnotationMetadata 表示特定類型、字段、構(gòu)造函數(shù)、方法或 bean 屬性的注釋信息的計算融合,可能包括在源代碼中聲明的注釋,也可能包括可在運(yùn)行時用于實現(xiàn)框架邏輯的合成元注釋。
當(dāng)使用 Element API 訪問 Micronaut 編譯器中的源代碼時,每個 ClassElement、FieldElement、MethodElement、ConstructorElement 和 PropertyElement 都會計算 AnnotationMetadata 的一個實例。
AnnotationMetadata API 試圖解決以下挑戰(zhàn):
注釋可以從類型和接口繼承到實現(xiàn)中。為了避免在運(yùn)行時遍歷類/接口層次結(jié)構(gòu)的需要,Micronaut 將在構(gòu)建時計算繼承的注釋并處理成員覆蓋規(guī)則
注釋可以用其他注釋進(jìn)行注釋。這些注釋通常稱為元注釋或原型。 AnnotationMetadata API 提供了一些方法來了解特定注解是否被聲明為元注解,并找出哪些注解與其他注解進(jìn)行了元注解
通常需要將來自不同來源的注釋元數(shù)據(jù)融合在一起。例如,對于 JavaBean 屬性,您希望將來自私有字段、公共 getter 和公共 setter 的元數(shù)據(jù)組合到一個視圖中,否則您必須在運(yùn)行時運(yùn)行邏輯以以某種方式組合來自 3 個不同來源的元數(shù)據(jù)。
可重復(fù)的注解被合并和歸一化。如果繼承,則注釋從父接口或類組合,提供單個 API 來評估可重復(fù)的注釋,而不需要運(yùn)行時邏輯來執(zhí)行規(guī)范化。
當(dāng)訪問類型的源時,將通過 ElementFactory API 構(gòu)造 ClassElement 的實例。
ElementFactory 使用 AbstractAnnotationMetadataBuilder 的一個實例,它包含語言特定的實現(xiàn)來為 AST 中的底層本機(jī)類型構(gòu)造 AnnotationMedata。對于 Java,這將是 javax.model.element.TypeElement。
基本流程如下圖所示:
此外,AbstractAnnotationMetadataBuilder 將通過標(biāo)準(zhǔn) Java 服務(wù)加載器機(jī)制加載以下類型的一個或多個實例,這些實例允許操縱注釋在 AnnotationMetadata 中的表示方式:
請注意,在編譯時,AnnotationMetadata 是可變的,并且可以通過調(diào)用 Element API 的 annotate(..) 方法通過 TypeElementVisitor 的實現(xiàn)進(jìn)一步更改。但是,在運(yùn)行時,AnnotationMetadata 是不可變且固定的。這種設(shè)計的目的是允許擴(kuò)展編譯器,并使 Micronaut 能夠解釋不同的基于注釋的源代碼級編程模型。
在實踐中,這有效地允許將源代碼級注釋模型與運(yùn)行時使用的注釋模型分離,以便可以使用不同的注釋來表示相同的注釋。
例如,jakarata.inject.Inject 或 Spring 的 @Autowired 支持作為 javax.inject.Inject 的同義詞,方法是將源代碼級注釋轉(zhuǎn)換為 javax.inject.Inject,后者是運(yùn)行時唯一表示的注釋。
最后,Java 中的注釋還允許定義默認(rèn)值。這些默認(rèn)值不會保留在 AnnotationMetadata 的單個實例中,而是存儲在共享的靜態(tài)應(yīng)用程序范圍映射中,供以后檢索應(yīng)用程序已知使用的注釋。
Bean 自省
Bean Introspections 的目標(biāo)是提供一種替代反射和 JDK 的 Introspector API 的方法,該 API 與最新版本的 Java 中的 java.desktop 模塊耦合。
Java 中的許多庫需要以編程方式發(fā)現(xiàn)哪些方法以某種方式表示類的屬性,雖然 JavaBeans 規(guī)范試圖建立標(biāo)準(zhǔn)約定,但該語言本身已經(jīng)發(fā)展到包括其他結(jié)構(gòu),如將屬性表示為組件的 Records。
此外,其他語言如 Kotlin 和 Groovy 對需要在框架級別支持的類屬性具有原生支持。
IntrospectedTypeElementVisitor 訪問類型上 @Introspected 注釋的聲明,并在編譯時生成與每個注釋類型關(guān)聯(lián)的 BeanIntrospection 實現(xiàn):
這一代通過 io.micronaut.inject.beans.visitor.BeanIntrospectionWriter 發(fā)生,這是一個使用 ASM 字節(jié)碼生成庫生成兩個額外類的內(nèi)部類。
例如,給定一個名為 example.Person 的類,生成的類是:
example.$Person$IntrospectionRef - BeanIntrospectionReference 的一個實現(xiàn),它允許應(yīng)用程序軟加載內(nèi)省而不加載所有元數(shù)據(jù)或類本身(在內(nèi)省類本身不在類路徑上的情況下)。由于引用是通過 ServiceLoader 加載的,因此在生成的 META-INF/services/io.micronaut.core.beans.BeanIntrospectionReference 中引用此類型的條目也會在編譯時生成。
example.$Person$Introspection - BeanIntrospection 的一個實現(xiàn),它包含實際的運(yùn)行時自省信息。
以下示例演示了 BeanIntrospection API 的用法:
Java | Groovy | Kotlin |
|
|
|
BeanIntrospection 按類型查找。當(dāng)發(fā)生這種情況時,將在通過 ServiceLoader 加載的 BeanIntrospectionReference 實例中搜索內(nèi)省。
實例化方法允許創(chuàng)建實例
bean 的屬性可以通過可用方法之一加載,在本例中為 getRequiredProperty
引用的 BeanProperty 可用于編寫可變屬性
并讀取可讀屬性
Person 類僅在調(diào)用 getBeanType() 方法時初始化。如果該類不在類路徑中,則會發(fā)生 NoClassDefFoundError,為防止這種情況,開發(fā)人員可以在嘗試獲取類型之前調(diào)用 BeanIntrospectionReference 上的 isPresent() 方法。
BeanIntrospection 的實現(xiàn)執(zhí)行兩個關(guān)鍵功能:
內(nèi)省包含關(guān)于特定類型的屬性和構(gòu)造函數(shù)參數(shù)的 Bean 元數(shù)據(jù),這些參數(shù)是從實際實現(xiàn)中抽象出來的(JavaBean 屬性、Java 17+ Record、Kotlin 數(shù)據(jù)類、Groovy 屬性等),并且還提供對 AnnotationMetadata 的訪問,而無需使用反射來加載注釋本身。
內(nèi)省使得能夠在不使用 Java 反射的情況下實例化和讀/寫 bean 屬性,完全基于構(gòu)建時生成的信息的子集。
通過覆蓋 AbstractInitializableBeanIntrospection 的 dispatchOne 方法生成優(yōu)化的無反射方法調(diào)度,例如:
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)
}
}
每個讀取或?qū)懭敕椒ǘ挤峙溆幸粋€索引
索引在read方法中使用,不依賴反射,直接獲取值
索引用于寫入方法以在不使用反射的情況下設(shè)置屬性
如果索引處不存在任何屬性,則會拋出異常,盡管這是實現(xiàn)細(xì)節(jié),代碼路徑永遠(yuǎn)不會到達(dá)這一點。
使用帶有索引的分派方法的方法用于避免需要為每個方法生成一個類(這會消耗更多內(nèi)存)或引入 lambda 的開銷。
為了啟用類型實例化,io.micronaut.inject.beans.visitor.BeanIntrospectionWriter 還將生成 instantiateInternal 方法的實現(xiàn),該方法包含無反射代碼以根據(jù)已知的有效參數(shù)類型實例化給定類型:
public Object instantiateInternal(Object[] args) {
return new Person(
(String)args[0],
(Integer)args[1]
);
}
Bean 定義
Micronaut 是 JSR-330 依賴注入規(guī)范的實現(xiàn)。
依賴注入(或控制反轉(zhuǎn))是 Java 中廣泛采用和常見的模式,它允許松散地解耦組件,以便輕松擴(kuò)展和測試應(yīng)用程序。
通過單獨的編程模型,將對象連接在一起的方式與該模型中的對象本身分離。對于 Micronaut,該模型基于 JSR-330 規(guī)范中定義的注釋以及位于 io.micronaut.context.annotation 包中的一組擴(kuò)展注釋。
這些注釋由 Micronaut 編譯器訪問,該編譯器遍歷源代碼語言 AST 并構(gòu)建用于在運(yùn)行時將對象連接在一起的模型。
重要的是要注意實際的對象連接被推遲到運(yùn)行時。
對于 Java 代碼,BeanDefinitionInjectProcessor(它是一個 Java 注釋處理器)是從 Java 編譯器為每個用 bean 定義注釋注釋的類調(diào)用的。
bean 定義注解的構(gòu)成很復(fù)雜,因為它考慮了元注解,但通常它是用 JSR-330 bean @Scope 注解的任何注解
BeanDefinitionInjectProcessor 將訪問用戶代碼源中的每個 bean,并使用 ASM 字節(jié)代碼生成庫生成額外的字節(jié)代碼,該庫位于同一包中的注釋類旁邊。
由于歷史原因,依賴注入處理器不使用 TypeElementVisitor API,但將來可能會這樣做
字節(jié)代碼生成在 BeanDefinitionWriter 中實現(xiàn),它包含“訪問”定義 bean 的不同方面的方法 (BeanDefinition)。
下圖說明了流程:
例如給定以下類型:
Java | Groovy | Kotlin |
|
|
|
生成以下內(nèi)容:
一個 example.$Vehicle$Definition$Reference 類,它實現(xiàn)了 BeanDefinitionReference 接口,允許應(yīng)用程序軟加載 bean 定義而不加載所有元數(shù)據(jù)或類本身(在自省類本身不在類路徑上的情況下) .由于引用是通過 ServiceLoader 加載的,因此在生成的 META-INF/services/io.micronaut.inject.BeanDefinitionReference 中引用此類型的條目也會在編譯時生成。
包含實際 BeanDefinition 信息的 example.$Vehicle$Definition。
BeanDefinition 是一種保存有關(guān)特定類型的元數(shù)據(jù)的類型,包括:
類級別的注釋元數(shù)據(jù)
計算的 JSR-330 @Scope 和 @Qualifier
了解可用的 InjectionPoint 實例
對定義的任何 ExecutableMethod 的引用
此外,BeanDefinition 包含知道如何將 bean 連接在一起的邏輯,包括如何構(gòu)造類型以及如何注入字段和/或方法。
在編譯期間,ASM 字節(jié)代碼庫用于填充 BeanDefinition 的詳細(xì)信息,包括一個構(gòu)建方法,對于前面的示例,該方法如下所示:
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;
}
BeanResolutionContext 被傳遞來跟蹤循環(huán) bean 引用并改進(jìn)錯誤報告。
實例化類型,并通過調(diào)用 AbstractInitializableBeanDefinition 的方法查找每個構(gòu)造函數(shù)參數(shù)
在這種情況下,跟蹤構(gòu)造函數(shù)參數(shù)的索引
當(dāng) Java 字段或方法具有私有訪問權(quán)限時,需要進(jìn)行特殊處理。在這種情況下,Micronaut 別無選擇,只能回退到使用 Java 反射來執(zhí)行依賴注入。
配置屬性處理
Micronaut 編譯器處理使用元注釋 @ConfigurationReader 聲明的 bean,例如 @ConfigurationProperties 和 @EachProperty 與其他 bean 截然不同。
為了支持將應(yīng)用程序配置綁定到使用上述注釋之一進(jìn)行注釋的類型,每個發(fā)現(xiàn)的可變 bean 屬性都使用帶有計算和規(guī)范化屬性名稱的 @Property 注釋進(jìn)行動態(tài)注釋。
例如給定以下類型:
@ConfigurationProperties Example
Java | Groovy | Kotlin |
|
|
|
setManufacturer(String) 方法將使用 @Property(name="my.engine.manufacturer") 注釋,其值將從配置的環(huán)境中解析。
AbstractInitializableBeanDefinition 的 injectBean 方法隨后被邏輯覆蓋,以處理從當(dāng)前 BeanContext 中查找規(guī)范化屬性名稱 my.engine.manufacturer 并注入值(如果它以無反射方式存在)。
屬性名稱被規(guī)范化為 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);
}
}
}
添加了頂級檢查,以查看是否存在任何具有在 @ConfigurationProperties 注釋中定義的前綴的屬性。
執(zhí)行檢查以查看該屬性是否確實存在
如果是,則通過調(diào)用 AbstractInitializableBeanDefinition 的 getPropertyValueForSetter 方法查找該值
創(chuàng)建一個 Argument 實例,用于轉(zhuǎn)換為目標(biāo)類型(在本例中為 String)。 Argument 還可能包含泛型信息。
屬性的計算和規(guī)范化路徑
如果 Bindable 注釋用于指定默認(rèn)值,則為默認(rèn)值。
AOP 代理
Micronaut 支持基于注釋的面向方面編程 (AOP),它允許通過使用在用戶代碼中定義的攔截器來裝飾或引入類型行為。
AOP 術(shù)語的使用起源于 AspectJ 和 Spring 中的歷史使用。
框架定義的任何注釋都可以使用 @InterceptorBinding 注釋進(jìn)行元注釋,支持不同類型的攔截,包括:
Interceptor 的一個或多個實例可以與 @InterceptorBinding 相關(guān)聯(lián),允許用戶實現(xiàn)應(yīng)用橫切關(guān)注點的行為。
在實現(xiàn)級別,Micronaut 編譯器將訪問使用@InterceptorBinding 進(jìn)行元注釋的類型,并構(gòu)造一個新的 AopProxyWriter 實例,該實例使用 ASM 字節(jié)碼生成庫生成注釋的子類(或接口情況下的實現(xiàn))類型。
Micronaut 絕不會修改現(xiàn)有的用戶字節(jié)代碼,使用構(gòu)建時生成的代理允許 Micronaut 生成附加代碼,這些代碼與用戶代碼并存并增強(qiáng)行為。然而,這種方法確實有局限性,例如,它要求帶注釋的類型是非最終類型,并且 AOP 建議不能應(yīng)用于最終類型或有效的最終類型,例如 Java 17 Records。
例如給出以下注釋:
Around Advice Annotation Example
Java | Groovy | Kotlin |
|
|
|
注解的保留策略必須是 RUNTIME
通常,您希望能夠在類或方法級別應(yīng)用建議,因此目標(biāo)類型是 TYPE 和 METHOD
這里使用了 @Around 注釋,它本身用 @InterceptorBinding(kind=AROUND) 注釋,可以被認(rèn)為是為 AROUND 建議定義 @InterceptorBinding 的簡單快捷方式。
當(dāng)此注釋用于類型或方法時,例如:
Around Advice Usage Example
Java | Groovy | Kotlin |
|
|
|
編譯器將訪問該類型,AopProxyWriter 將使用 ASM 字節(jié)碼生成庫生成額外的字節(jié)碼。
在編譯過程中,AopProxyWriter 實例實質(zhì)上代理了 BeanDefinitionWriter(參見 Bean 定義),用額外的行為裝飾現(xiàn)有的字節(jié)碼生成。下圖說明了這一點:
BeanDefinitionWriter 將生成為每個 bean 生成的常規(guī)類,包括:
AopProxyWriter 將修飾此行為并生成 3 個額外的類:
生成的大部分類都是用于加載和解析 BeanDefinition 的元數(shù)據(jù)。實際構(gòu)建時代理是以 $Intercepted 結(jié)尾的類。該類實現(xiàn)了 Intercepted 接口并將代理類型子類化,覆蓋任何非最終和非私有方法以調(diào)用 MethodInterceptorChain。
一個實現(xiàn)將創(chuàng)建一個構(gòu)造函數(shù),用于連接截獲類型的依賴項,如下所示:
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)
}
}
@Generated 子類擴(kuò)展自裝飾類型并實現(xiàn) Intercepted 接口
構(gòu)造 ExecutableMethodsDefinition 的實例以將無反射調(diào)度程序解析為原始方法。
名為 $proxyMethods 的內(nèi)部數(shù)組包含對用于代理調(diào)用的每個 ExecutableMethod 實例的引用。
一個名為 $interceptors 的內(nèi)部數(shù)組包含對每個方法應(yīng)用攔截器實例的引用,因為 @InterceptorBinding 可以是類型或方法級別,這些可能因每個方法而異。
具有與之關(guān)聯(lián)的 @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)
}
該方法的 ExecutableMethod 和 Interceptor 實例數(shù)組位于。
一個新的 MethodInterceptorChain 是用攔截器、對被攔截實例的引用、方法和參數(shù)構(gòu)造的。
在 MethodInterceptorChain 上調(diào)用 proceed() 方法。
請注意,@Around 注釋的默認(rèn)行為是通過生成的允許訪問超級實現(xiàn)的合成橋方法調(diào)用超級實現(xiàn)來調(diào)用目標(biāo)類型的原始重寫方法(在上述情況下為 NotNullExample)。
在這種安排中,代理和代理目標(biāo)是同一個對象,攔截器被調(diào)用,并且在上述情況下調(diào)用 proceed() 通過調(diào)用 super.doWork() 調(diào)用原始實現(xiàn)。
但是,可以使用 @Around 注釋自定義此行為。
通過設(shè)置 @Around(proxyTarget=true) ,生成的代碼還將實現(xiàn) InterceptedProxy 接口,該接口定義了一個名為 interceptedTarget() 的方法,該方法解析代理應(yīng)將方法調(diào)用委托給的目標(biāo)對象。
默認(rèn)行為 (proxyTarget=false) 在內(nèi)存方面更有效,因為只需要一個 BeanDefinition 和一個代理類型的實例。
代理目標(biāo)的評估是急切的并且在首次創(chuàng)建代理時完成,但是可以通過設(shè)置 @Around(lazy=true, proxyTarget=true) 使其變得惰性,在這種情況下代理只會在代理方法被檢索時被檢索調(diào)用。
下圖說明了使用 proxyTarget=true 代理目標(biāo)之間的行為差??異:
圖左側(cè)的序列 (proxyTarget=false) 通過調(diào)用 super 調(diào)用代理方法,而右側(cè)的序列從 BeanContext 中查找代理目標(biāo)并調(diào)用目標(biāo)上的方法。
最后一個自定義選項是 @Around(hotswap=true) ,它觸發(fā)編譯器生成一個編譯時代理,該代理實現(xiàn) HotSwappableInterceptedProxy,它定義了一個名為 swap(..) 的方法,允許用新實例換出代理的目標(biāo)(為了使其成為線程安全的,生成的代碼使用了 ReentrantReadWriteLock)。
安全注意事項
通過 AROUND 建議進(jìn)行的方法攔截通常用于定義解決橫切關(guān)注點的邏輯,其中之一就是安全性。
當(dāng)多個攔截器實例應(yīng)用于單個方法時,從安全角度來看,這些攔截器以特定順序執(zhí)行可能很重要。
Interceptor 接口擴(kuò)展了 Ordered 接口,使開發(fā)人員能夠通過覆蓋 getOrder() 方法來控制攔截器排序。
當(dāng)構(gòu)造 MethodInterceptorChain 并且存在多個攔截器時,它們將按照最先執(zhí)行的最高優(yōu)先級攔截器的順序排列。
為了幫助定義自己的 Around Advice 的開發(fā)人員,InterceptPhase 枚舉定義了各種可用于正確聲明 getOrder() 值的常量(例如,安全性通常屬于 VALIDATE 階段)。
可以為 io.micronaut.aop.chain 包啟用跟蹤級別的日志記錄,以調(diào)試已??解析的攔截器順序。
應(yīng)用上下文
一旦 Micronaut 編譯器的工作完成并生成了所需的類,BeanContext 就會加載這些類以供運(yùn)行時執(zhí)行。
雖然標(biāo)準(zhǔn)的 Java 服務(wù)加載器機(jī)制用于定義 BeanDefinitionReference 的實例,但實例本身是用 SoftServiceLoader 加載的,這是一個更寬松的實現(xiàn),允許在加載之前檢查服務(wù)是否實際存在,并且還允許并行加載服務(wù)。
BeanContext 執(zhí)行以下步驟:
并行軟加載所有 BeanDefinitionReference 實例
實例化所有用@Context 注釋的bean(bean 作用域為整個上下文)
為每個發(fā)現(xiàn)的已處理 ExecutableMethod 運(yùn)行每個 ExecutableMethodProcessor。如果一個方法使用@Executable(processOnStartup = true) 進(jìn)行元注釋,則該方法被視為“已處理”
當(dāng)上下文啟動時,在 StartupEvent 類型上發(fā)布一個事件。
基本流程如下圖所示:
ApplicationContext 是 BeanContext 的一個特殊版本,它添加了一個或多個活動環(huán)境(由 Environment 封裝)和基于此環(huán)境的條件 bean 加載的概念。
環(huán)境是從一個或多個已定義的 PropertySource 實例加載的,這些實例是通過標(biāo)準(zhǔn) Java 服務(wù)加載器機(jī)制通過加載 PropertySourceLoader 的實例發(fā)現(xiàn)的。
開發(fā)人員可以通過添加額外的實現(xiàn)和引用此類的相關(guān) META-INF/services/io.micronaut.context.env.PropertySourceLoader 文件來擴(kuò)展 Micronaut 以通過完全自定義的機(jī)制加載 PropertySource。
BeanContext 和 ApplicationContext 之間的高級差異如下所示:
如上所示,ApplicationContext 加載了用于多種用途的環(huán)境,包括:
通過 Bean Requirements 啟用和禁用 beans
允許通過@Value 或@Property 依賴注入配置
允許綁定配置屬性
Micronaut HTTP 服務(wù)器可以被認(rèn)為是一個 Micronaut 模塊——它是 Micronaut 的一個組件,它建立在包括依賴注入和 ApplicationContext 的生命周期在內(nèi)的基本構(gòu)建塊之上。
HTTP 服務(wù)器包括一組抽象接口和公共代碼,分別包含在 micronaut-http 和 micronaut-http-server 模塊中(前者包括在客戶端和服務(wù)器之間共享的 HTTP 原語)。
這些接口的默認(rèn)實現(xiàn)是基于 Netty I/O 工具包提供的,其架構(gòu)如下圖所示:
Netty API 通常是一個非常低級的 I/O 網(wǎng)絡(luò) API,專為集成商設(shè)計,用于構(gòu)建呈現(xiàn)更高抽象層的客戶端和服務(wù)器。 Micronaut HTTP 服務(wù)器就是這樣一個抽象層。
Micronaut HTTP 服務(wù)器的架構(gòu)圖及其實現(xiàn)中使用的組件描述如下:
運(yùn)行服務(wù)器的主要入口點是實現(xiàn) ApplicationContextBuilder 的 Micronaut 類。通常,開發(fā)人員將以下調(diào)用置于其應(yīng)用程序的主入口點:
定義主入口點
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
傳遞的參數(shù)轉(zhuǎn)換為 CommandLinePropertySource 并可通過 @Value 進(jìn)行依賴注入。
執(zhí)行運(yùn)行將使用默認(rèn)設(shè)置啟動 Micronaut ApplicationContext,然后搜索類型為 EmbeddedServer 的 bean,它是一個接口,用于公開有關(guān)可運(yùn)行服務(wù)器的信息,包括主機(jī)和端口信息。這種設(shè)計將 Micronaut 與實際的服務(wù)器實現(xiàn)分離,雖然默認(rèn)服務(wù)器是 Netty(如上所述),但第三方只需提供 EmbeddedServer 的實現(xiàn)即可實現(xiàn)其他服務(wù)器。
服務(wù)器啟動的順序圖如下所示:
在 Netty 實現(xiàn)的情況下,EmbeddedServer 接口由 NettyHttpServer 實現(xiàn)。
服務(wù)器配置
NettyHttpServer 讀取服務(wù)器配置,包括:
服務(wù)器配置安全注意事項
Netty 的 SslContext 提供了一個抽象,允許使用 JDK 提供的 javax.net.ssl.SSLContext 或 OpenSslEngine,這需要開發(fā)人員額外添加 netty-tcnative 作為依賴項(netty-tcnative 是 Tomcat 的 OpenSSL 綁定的一個分支)。
ServerSslConfiguration 允許將應(yīng)用程序配置到磁盤上存在有效證書的安全、可讀位置,以通過從磁盤加載配置來正確配置 javax.net.ssl.TrustManagerFactory 和 javax.net.ssl.KeyManagerFactory。
Netty 服務(wù)器初始化
當(dāng) NettyHttpServer 執(zhí)行 start() 序列時,它將執(zhí)行以下步驟:
讀取 EventLoopGroupConfiguration 并創(chuàng)建啟動 Netty 服務(wù)器所需的父 EventLoopGroup 實例和工作實例。
計算要使用的特定于平臺的 ServerSocketChannel(取決于操作系統(tǒng),這可能是 Epoll 或 KQueue,如果沒有本機(jī)綁定是可能的,則回退到 Java NIO)
創(chuàng)建用于初始化 SocketChannel(客戶端和服務(wù)器之間的連接)的 ServerBootstrap 實例。
SocketChannel 由 Netty ChannelInitializer 初始化,它創(chuàng)建自定義的 Netty ChannelPipeline,用于 Micronaut 到服務(wù)器 HTTP/1.1 或 HTTP/2 請求,具體取決于配置。
Netty ServerBootstrap 綁定到一個或多個配置的端口,有效地使服務(wù)器可用于接收請求。
觸發(fā)了兩個 Bean 事件,第一個是 ServerStartupEvent 以指示服務(wù)器已啟動,然后最后在處理完所有這些事件后,僅當(dāng)屬性 micronaut.application.name 已設(shè)置時才會觸發(fā) ServiceReadyEvent。
此啟動順序如下所示:
NettyHttpServerInitializer 類用于初始化處理傳入 HTTP/1.1 或 HTTP/2 請求的 ChannelPipeline。
ChannelPipeline 安全注意事項
用戶可以通過實現(xiàn)實現(xiàn) ChannelPipelineCustomizer 接口的 bean 并向管道添加新的 Netty ChannelHandler 來自定義 ChannelPipeline。
添加 ChannelHandler 允許執(zhí)行諸如傳入和傳出數(shù)據(jù)包的線級日志記錄之類的任務(wù),并且可以在需要線級安全要求時使用,例如驗證傳入請求正文或傳出響應(yīng)正文的字節(jié)。
Netty 服務(wù)器路由
Micronaut 定義了一組 HTTP 注釋,允許將用戶代碼綁定到傳入的 HttpRequest 實例并自定義生成的 HttpResponse。
一個或多個已配置的 RouteBuilder 實現(xiàn)構(gòu)造 UriRoute 的實例,Router 組件使用該實例來路由帶注釋類的傳入請求方法,例如:
Java | Groovy | Kotlin |
|
|
|
請求綁定注釋可用于將方法參數(shù)綁定到 HTTP 主體、標(biāo)頭、參數(shù)等,框架將在數(shù)據(jù)傳遞給接收方法之前自動處理正確轉(zhuǎn)義數(shù)據(jù)。
傳入請求由 Netty 接收,ChannelPipeline 由 NettyHttpServerInitializer 初始化。傳入的原始數(shù)據(jù)包被轉(zhuǎn)換為 Netty HttpRequest,隨后將其包裝在 Micronaut NettyHttpRequest 中,后者對底層 Netty 請求進(jìn)行了抽象。
NettyHttpRequest 通過 Netty ChannelHandler 實例鏈傳遞,直到它到達(dá) RoutingInBoundHandler,后者使用上述 Router 來匹配帶有注釋的 @Controller 類型的方法的請求。
RoutingInBoundHandler 委托 RouteExecutor 來實際執(zhí)行路由,它處理所有邏輯以分派給帶注釋的 @Controller 類型的方法。
執(zhí)行后,如果返回值不為空,則從 MediaTypeCodecRegistry 中查找適當(dāng)?shù)?nbsp;MediaTypeCodec 以獲取響應(yīng) Content-Type(默認(rèn)為 application/json)。 MediaTypeCodec 用于將返回值編碼為 byte[] 并將其作為結(jié)果 HttpResponse 的主體包含在內(nèi)。
下圖說明了傳入請求的此流程:
RouteExecutor 將構(gòu)造一個 FilterChain 以在執(zhí)行帶注釋的 @Controller 類型的目標(biāo)方法之前執(zhí)行一個或多個 HttpServerFilter。
一旦所有 HttpServerFilter 實例都已執(zhí)行,RouteExecutor 將嘗試滿足目標(biāo)方法參數(shù)的要求,包括任何 Request 綁定注釋。如果不能滿足參數(shù),則將 HTTP 400 - Bad Request HttpStatus 響應(yīng)返回給調(diào)用客戶端。
Netty 服務(wù)器路由安全注意事項
開發(fā)人員可以使用 HttpServerFilter 實例來控制對服務(wù)器資源的訪問。通過不繼續(xù)執(zhí)行 FilterChain,可以將替代響應(yīng)(例如 403 - Forbidden)返回給客戶端,禁止訪問敏感資源。
請注意,HttpServerFilter 接口從 Ordered 接口擴(kuò)展而來,因為 FilterChain 中經(jīng)常存在多個過濾器。通過實施 getOrder() 方法,開發(fā)人員可以返回適當(dāng)?shù)膬?yōu)先級來控制排序。此外,ServerFilterPhase 枚舉提供了一組常量,開發(fā)人員可以使用這些常量來正確定位過濾器,包括通常放置安全規(guī)則的 SECURITY 階段。
更多建議: