SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。
1、前言
在 SOFABoot 環境下,SOFARPC 提供三種方式給開發人員釋出和引用 RPC 服務:
XML 方式(配置)
Annotation 方式(註解)
程式設計 API 方式(動態)
程式設計 API 方式與Spring 的 ApplicationContextAware
類似。XML的方式依賴於在xml中引入 SOFA 名稱空間,利用 Bean 的生命週期管理,進行 Bean 的注入。相比這兩種方式,通過 Annotation 方式釋出 JVM 服務更加靈活方便,只需要在實現類上加 @SofaService
、@SofaRefernce
註解即可進行服務的釋出和引用。
本文針對 SOFARPC 在註解的支援和使用原理、原始碼兩部分進行一一介紹。
2、註解原理解析
2.1、註解是什麼
註解又稱為後設資料,可以對程式碼中新增資訊,這是一種形式化的方法,可以在稍後的某個時刻非常方便地使用這些資料。這個時刻可能是編譯時,也可能是執行時。
註解是 JDK1.5 版本開始引入的一個特性,用於對程式碼進行說明,可以對包、類、介面、欄位、方法引數、區域性變數等進行註解。註解的本質就是一個繼承了 Annotation 介面的介面。一個註解準確意義上來說,只不過是一種特殊的註釋而已,如果沒有解析它的程式碼,它可能連註釋都不如。
一般常用的註解可以分為三類:
Java自帶的標準註解,包括
@Override
(標明重寫某個方法)、@Deprecated
(標明某個類或方法過時)和@SuppressWarnings
(標明要忽略的警告);元註解,元註解是用於定義註解的註解;
自定義註解,可以根據自己的需求定義註解;
2.2、元註解
元註解是用於修飾註解的註解,通常用在註解的定義上。JAVA 中有以下幾個元註解:
@Target:註解的作用目標,也就是指明,你的註解到底是用來修飾方法的?修飾類的?還是用來修飾欄位屬性的,有以下幾種型別:
ElementType.TYPE:允許被修飾的註解作用在類、介面和列舉上
ElementType.FIELD:允許作用在屬性欄位上
ElementType.METHOD:允許作用在方法上
ElementType.PARAMETER:允許作用在方法引數上
ElementType.CONSTRUCTOR:允許作用在構造器上
ElementType.LOCAL_VARIABLE:允許作用在本地區域性變數上
ElementType.ANNOTATION_TYPE:允許作用在註解上
ElementType.PACKAGE:允許作用在包上
@Retention:指定了被修飾的註解的生命週期,分以下三種型別:
RetentionPolicy.SOURCE:該註解只保留在一個原始檔當中,當編譯器將原始檔編譯成class檔案時,它不會將原始檔中定義的註解保留在class檔案中。
RetentionPolicy.CLASS:該註解只保留在一個class檔案當中,當載入class檔案到記憶體時,虛擬機器會將註解去掉,從而在程式中不能訪問。
RetentionPolicy.RUNTIME:該註解在程式執行期間都會存在記憶體當中。此時,我們可以通過反射來獲得定義在某個類上的所有註解。
@Documented:當我們執行 JavaDoc 文件打包時會被儲存進 doc 文件,反之將在打包時丟棄。
@Inherited:解修飾的註解是具有可繼承性的,也就說我們的註解修飾了一個類,而該類的子類將自動繼承父類的該註解。
以 @Override
為例子:
當編譯器檢測到某個方法被修飾了 @Override
註解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。
@Override
僅被編譯器可知,編譯器在對 java 檔案進行編譯成位元組碼的過程中,一旦檢測到某個方法上被修飾了該註解,就會去匹對父類中是否具有一個同樣方法簽名的函式,否則不能通過編譯。
2.3、註解解析方式
解析一個類或者方法的註解通常有兩種形式,一種是編譯期直接的掃描,一種是執行期反射。
2.3.1、編譯器的掃描
指的是編譯器在對 java 程式碼編譯位元組碼的過程中會檢測到某個類或者方法被一些註解修飾,這時它就會對於這些註解進行某些處理。典型的就是註解 @Override
,一旦編譯器檢測到某個方法被修飾了 @Override
註解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。
這一種情況只適用於那些編譯器已經熟知的註解類,比如 JDK 內建的幾個註解,而你自定義的註解,編譯器是不知道你這個註解的作用的。
2.3.1、執行期反射
首先對虛擬機器的幾個註解相關的屬性表進行介紹,先大體瞭解註解在位元組碼檔案中是如何儲存的。虛擬機器規範定義了一系列和註解相關的屬性表,也就是說,無論是欄位、方法或是類本身,如果被註解修飾了,就可以被寫進位元組碼檔案。屬性表有以下幾種:
RuntimeVisibleAnnotations:執行時可見的註解
RuntimeInVisibleAnnotations:執行時不可見的註解
RuntimeVisibleParameterAnnotations:執行時可見的方法引數註解
RuntimeInVisibleParameterAnnotations:執行時不可見的方法引數註解
AnnotationDefault:註解類元素的預設值
java.lang.reflect.AnnotatedElement
介面是所有程式元素(Class、Method和Constructor)的父介面,程式通過反射獲取了某個類的 AnnotatedElemen t物件之後,利用 Java 的反射機獲取程式程式碼中的註解,然後根據預先設定的處理規則解析處理相關注解以達到主機本身設定的功能目標。
本質上來說,反射機制就是註解使用的核心,程式可以呼叫該物件的以下方法來訪問 Annotation資訊:
getAnnotation:返回指定的註解
isAnnotationPresent:判定當前元素是否被指定註解修飾
getAnnotations:返回所有的註解
getDeclaredAnnotation:返回本元素的指定註解
getDeclaredAnnotations:返回本元素的所有註解,不包含父類繼承而來的
3、SOFARPC 原始碼解析
3.1、註解說明
以com.alipay.sofa.runtime.api.annotation.SofaReference
為例子(SofaService 類似),原始碼如下:
基於元註解的含義,可以瞭解到:
@SofaReference
生命週期為 RetentionPolicy.RUNTIME,代表永久儲存,可以反射獲取;註解的作用目標 ElementType.FIELD,ElementType.METHOD,說明允許作用在方法和屬性欄位上;
RPC 的繫結方式有 JVM、BOLT、REST 三種;
預設服務繫結關係為 JVM 方式;
3.2、服務釋出與引用解析
通過 ServiceAnnotationBeanPostProcesso
類中postProcessAfterInitialization
、postProcessBeforeInitialization
方法分別進行服務的釋出和引用,其中通過反射對於註解的解析步驟大體相似,主要包含:
獲取 SofaService.class、SofaReference.class 指定註解
獲取的 SOFA 引用的型別,預設為 void
獲取的 SOFA 引用的 uniqueId
3.2.1、總體流程
首先看下服務釋出和引用整體流程圖,主要包含註解解析、元件生成、元件註冊幾個步驟,後面對每個步驟進行更加詳細的解釋。
3.2.2、服務釋出
@SofaService
的目標是將一個類註冊到 SOFA Context 中。釋出到 SofaRuntimeContext 的過程其實就是把服務元件物件塞到 ConcurrentMap<ComponentName, ComponentInfo> registry
物件中,當有其他地方需要查詢服務元件的時候,可以通過 registry 進行查詢。主要包含以下幾個步驟:
會遍歷 SOFA 繫結關係,通過 handleSofaServiceBinding 方法進行不同型別的 RPC Binding。
生成 ServiceComponent 服務元件物件。
呼叫 ServiceComponent 服務元件的 register、resolve、activate方法,逐一呼叫對應 BindingAdapter 對外暴露服務。
不同的 BindingAdapter,對應的 outBinding 服務處理策略不一樣。對於 JvmBindingAdapter 直接返回空,因為服務不需要暴露給外部,當其他模組呼叫該服務,直接通過 registry 物件進行查詢。其他 RPC BindingAdapter 則將服務資訊推送到註冊中心 Confreg。
將 ServiceComponent 註冊到 sofa 的上下文sofaRuntimeContext 中。
3.2.3、服務引用
@SofaReference
的目標則是將 SOFA Context 中的一個服務註冊成為 Spring 中的一個bean。基於以上註解解析基礎上,主要通過 ReferenceRegisterHelper.registerReference()
方法從SOFA上下文中,拿到服務對應的代理物件。在 registerReference()
方法內部,主要包含以下操作:
當註解的
jvmFirst()
為 true 時,會為服務自動再新增一個本地 JVM 的 binding,這樣能夠做到優先本地呼叫,避免跨機呼叫。生成 ReferenceComponent 服務元件物件。
與 ServiceComponent 處理方式類似,ReferenceComponent 也會新增到 ConcurrentMap<ComponentName, ComponentInfo> registry物件中,分別執行元件的register、resolve、activate 三個方法。其中 register、resolve 方法主要是改變元件的生命週期,代理物件的生成就是在 activate 方法中完成的。
ReferenceComponent 元件通過不同型別的 binding 生成不同型別的代理物件。如果只有一個binding,使用當前 binding 生成代理物件。如果有多個 binding,優先使用 jvm binding 來生成本地呼叫的代理物件,若本地代理物件不存在,使用遠端代理物件。
對於JvmBindingAdapter 的 inBinding 方法,直接藉助於動態代理技術進行生成代理物件,對於 RpcBindingAdapter 的 inBinding,在構造的過程存在向註冊中心訂閱的邏輯。
4、總結
通過 XML 的方式去配置 SOFA 的 JVM 服務和引用非常簡潔,但是多了一定的編碼工作量。
因此,除了通過 XML 方式釋出 JVM 服務和引用之外,SOFA 還提供了 Annotation 的方式來發布和引用 JVM 服務。@SofaService
註解省去了<sofa:service>
宣告,但 bean 的定義還是必須要有的。
SOFA 實際上是註冊了一個BeanPostProcessor 來處理@SofaService
和 @SofaReference
註解。需要釋出引用的物件屬於當前 bean 的例項變數,使用 xml 的方式進行服務釋出和引用,可以直接通過 Bean 生命週期的 InitializingBean#afterPropertiesSet
方法進行擴充套件。在工程中註解掃描是一個對所有 bean 的操作,只能通過實現 spring 的 beanpostprocessor 這個介面,另外有些屬性可能在釋出時需要用到。
因此使用註解的方式進行服務釋出和引用,分別基於 Bean 生命週期的 BeanPostProcessor#postProcessAfterInitialization
、#postProcessBeforeInitialization
方法進行擴充套件。
對比服務的釋出和引用的兩種常用方式,XML 是一種集中式的後設資料,與原始碼無繫結,註解是一種分散式的後設資料,與原始碼緊繫結。SOFARPC 初始的版本,並不支援通過註解進行 RPC 服務的釋出和引用,需要使用 XML 的方式進行配置。後來在開源 SOFARPC 版本中增加這個功能的註解支援,對服務釋出和引用做了一個使用方式的補充,而對於 XML 與註解的優劣取捨,大家可以根據團隊的規範和個人的評估進行相應的使用。
5、參考文件
Java annotation:
https://en.wikipedia.org/wiki/Java_annotation
SOFASTACK 服務釋出/服務引用:
http://www.sofastack.tech/sofa-rpc/docs/Publish-And-Reference
相關連結
SOFA 文件: http://www.sofastack.tech/
SOFA: https://github.com/alipay
SOFARPC: https://github.com/alipay/sofa-rpc
SOFABolt: https://github.com/alipay/sofa-bolt
《剖析 | SOFARPC 框架》系列歷史文章
長按關注,獲取分散式架構乾貨
歡迎大家共同打造 SOFAStack https://github.com/alipay