從 <sofa:XXX> 標簽開始看 SOFA-Boot 如何融入 Spring
前言
SOFA-Boot 現階段支持 XML 的方式在 Spring 中定義 Bean,通過這些標簽,我們就能從 Spring 容器中取出 RPC 中的引用,並進行調用,那麽他是如何處理這些自定義標簽的呢?一起來看看。
如何使用?
官方例子:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sofa="http://sofastack.io/schema/sofaboot"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://sofastack.io/schema/sofaboot http://sofastack.io/schema/sofaboot.xsd"
default-autowire="byName">
<bean id="personServiceImpl" class="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonServiceImpl"/>
<sofa:service ref="personServiceImpl" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService" >
<sofa:binding.bolt/>
<sofa:binding.rest/>
</sofa:service>
<sofa:reference id="personReferenceBolt" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService">
<sofa:binding.bolt/>
</sofa:reference>
<sofa:reference id="personReferenceRest" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService">
<sofa:binding.rest/>
</sofa:reference>
<bean id="personFilter" class="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonServiceFilter"/>
</beans>
顯眼的 sofa 標簽。那麽如何知道他是怎麽處理這些標簽的內容的呢?
答:從上面的 xmlns 命名空間可以找到。如果寫過自定義標簽的話,一定很熟悉了。
如果沒有寫過的話,就簡單介紹一下。Spring 支持用戶自定標簽,需要遵守以下規範。
編寫 xsd 文件,就是定義標簽的屬性。
繼承抽象類
NamespaceHandlerSupport
並實現 init 方法,此類就是處理命名空間的類,不僅需要實現 init 方法,你還需要繼承AbstractSingleBeanDefinitionParser
類,自行解析標簽。通常寫法是這樣:
registerBeanDefinitionParser("tagName",new UserBeanDefinitionParser());
在做完上面的步驟後,你需要在 META-INF 目錄下編寫
spring.handlers
和spring.schemas
文件,前者 key 為命名空間名稱,value 為NamespaceHandlerSupport
的具體實現;後者 key 為 xsd 命名空間,value 為 xsd 具體文件(classpath 下)。
完成上面的步驟後,Spring 在加載 xml 配置文件的時候,會檢查命名空間,如果是自定義的,則會根據命名空間的 key 找到對應的解析器,也就是 NamespaceHandlerSupport
對自定義標簽進行解析。
So,我們的目的是看 sofa 標簽是如何解析的,則找到解析 sofa 的NamespaceHandlerSupport
。
尋找解析 sofa 標簽的源頭
通過 IDEA 或者別的工具全局搜索,我們找到了源碼中位於 start 模塊下,resources 下的 META-INF 目錄,目錄下有以下幾個文件:
- rpc.xsd
- sofaboot.xsd
- spring.handlers
- spring.schemas
這幾個文件我們是比較關註的,重點看 handlers 文件。內容如下:
http\://sofastack.io/schema/sofaboot=com.alipay.sofa.infra.config.spring.namespace.handler.SofaBootNamespaceHandler
SofaBootNamespaceHandler
明顯就是明明空間處理類,繼承了 Spring 的 NamespaceHandlerSupport
。
該類的 init 方法通過 Java 的 SPI 進行擴展,找到 SofaBootTagNameSupport 的標簽支持類,目前有 2 個實現: ServiceDefinitionParser 和 ReferenceDefinitionParser。
兩個類支持不同的 element。一個是引用服務,一個是發布服務。
這樣就比較清晰了。在得到兩個類之後,註冊到 Spring 的 parsers map 中。key 是 element 名字,value 是解析器。
具體的解析上面說了,service 和 reference。
Sofa 在這個設計上使用了模板模式,使用一個抽象類 AbstractContractDefinitionParser
,並定義一個 doParseInternal 抽象方法讓子類去實現。
抽象父類將一些公用的屬性進行解析,下圖中都是公用的屬性:
你咋確定是公用的屬性呢?首先,xml 解析後,肯定是要註入到 Bean 裏面去的,那麽這個 Bean 是什麽呢?實際上,在 AbstractSingleBeanDefinitionParser
抽象類中,會有一個返回 Bean 類型的方法,對應著一個 tag,而他們的父類就是AbstractContractFactoryBean
。
該類公用屬性就對應上面圖片中的定義。
有了這些基礎的信息,還是不能夠直接使用的,畢竟都是字符串。我們要看看 SOFA 是如何將自己和 Spring 容器融合在一起的。
如何融入 Spring?
來看看他們的子類, ReferenceDefinitionParser 類(ServiceFactoryBean 類似)。對應的 Bean 是 ReferenceFactoryBean,該類單獨定義了負載均衡(loadBalance)的屬性。
啊,從這個類的名字看,很熟悉,之前在分析 Spring 源碼的時候,就看見過 FactoryBean。可以通過 getObject 方法修改返回的 Bean。
來看看這個類的 類圖
如我們所料,繼承了 Spring 中關鍵的擴展點。而他的 getObject 方法將返回一個代理:
@Override
public Object getObject() throws Exception {
return proxy;
}
具體創建代理的方法是 com.alipay.sofa.runtime.service.component.ReferenceComponent
類的 createProxy 方法,然後調用適配器。適配器的作用就是膠水的作用,將 Spring 容器和第三方框架粘合起來,Spring 提供的接口則是 FactoryBean 等接口,而第三方框架可以在 getObject 方法中大有作為。
SOFA-Boot 的適配器在 om.alipay.sofa.rpc.boot.runtime.adapter
包中,具體實現則是 RpcBindingAdapter 類,其中 outBinding 方法用於發布服務,inBinding 方法用於引用服務,這裏直接使用的就是 SOFA-RPC 的 consumerConfig 和 providerConfig。這就應該很熟悉了吧。哈哈。
來點代碼看看(已去除異常處理):
@Override
public Object outBinding(Object contract, RpcBinding binding, Object target, SofaRuntimeContext sofaRuntimeContext) {
String uniqueName = ProviderConfigContainer.createUniqueName((Contract) contract, binding);
ProviderConfig providerConfig = ProviderConfigContainer.getProviderConfig(uniqueName);
providerConfig.export();
if (ProviderConfigContainer.isAllowPublish()) {
Registry registry = RegistryConfigContainer.getRegistry();
providerConfig.setRegister(true);
registry.register(providerConfig);
}
return Boolean.TRUE;
}
@Override
public Object inBinding(Object contract, RpcBinding binding, SofaRuntimeContext sofaRuntimeContext) {
ConsumerConfig consumerConfig = ConsumerConfigHelper.getConsumerConfig((Contract) contract, binding);
ConsumerConfigContainer.addConsumerConfig(binding, consumerConfig);
Object result = consumerConfig.refer();
binding.setConsumerConfig(consumerConfig);
return result;
}
So,當 Spring 容器使用 getObject 方法的時候,獲取的就是這個代理對象啦。是不是很簡單?
有一點需要註意一下,ServiceFactoryBean 的設計和 ReferenceFactoryBean 確實是類似,但是!但是!他的 getObject 方法就是實現類本身,這是 RPC 框架本身的設計決定的,因為它不需要代理,只需要發布服務就行,當收到了客戶端傳來的信息,就直接調用實現類的指定方法就好了,沒有客戶端這麽復雜。
當然,SOFA 中還有一個組件的概念,我們有時間會好好看看這塊的設計。
總結
這次我們從 SOFA-Boot 配置文件的 xml 標簽開始,對他如何融入 Spring 進行了分析,實際上,整體和我們聯想的類似,使用 Spring 的 FactoryBean 的 getObject 方法返回代理,如果是客戶端的話,在 getObject 方法中,會創建一個動態代理,這就要使用 SOFA-RPC 了,所以,融合 RPC 和 Spring 的任務肯定是個適配器。SOFA 的實現就是 RpcBindingAdapter 類。在該類中,將 RPC 和 Spring 適配。
而發布服務相比較引用服務就簡單一點了,整體上就是通過註解將服務發布,當然也是使用的 RpcBindingAdapter 進行適配。但是沒有使用代理(不需要)。
好了,這次研究 SOFA 和 Spring 融合的過程就到這裏啦。
bye !
從 <sofa:XXX> 標簽開始看 SOFA-Boot 如何融入 Spring