DUBBO配置規則詳解
歡迎加入DUBBO交流羣:259566260
研究DUBBO也已經大半年了,對它的大部分源碼進行了分析,以及對它的內部機制有了比較深入的瞭解,以及各個模塊的實現。DUBBO包含很多內容,如果想了解DUBBO第一步就是啓動它,從而可以很好的使用它,那麼如何更好的使用呢?就需要知道DUBBO的各個配置項,以及它可以通過哪些途徑進行配置。個人對配置的理解,就好比時對動物的馴服,如何很好的馴服一頭猛獸,那就需要知道它各種習性,從而調整,已達到自己期望的結果。這篇不對DUBBO有哪些配置項可以配置,但是通過這篇文章,你應該能夠知道DUBBO可以進行哪些配置。本文會通過分析DUBBO加載配置源碼的分析,來使得大家對DUBBO的配置一塊有更加深入的瞭解。從而達到“馴服”DUBBO,以使得它成爲你們自己的DUBBO。
DUBBO在配置這一塊做的確實很完美,提供很很多參數,以及提供了多種渠道。下面進入正題,看看DUBBO怎麼加載配置的。在講這些之前,先給大家介紹一下在DUBBO源碼層面定義了哪些類來存儲各個模塊的配置項,從而瞭解DUBBO可以對哪些模塊進行配置。
哪些東西可以配置
由於大部分項目都會使用Spring,而且DUBBO也提供了通過Spring來進行配置,那麼先從這裏進行着手。DUBBO加載Spring的集成時在dubbo-config下面的dubbo-config-spring模塊下面,其中有一個類DubboNamespaceHandler
,它實現了Spring提供的接口NamespaceHandlerSupport
。那麼Spring怎麼發現整個實現類的呢?在該模塊的META-INF文件夾下有兩個文件: spring.handlers和spring.schemas,這兩個文件裏面制定了dubbo的namespace的XSD文件的位置以及dubbo的namespace由DubboNamespaceHandler
來處理解析。說了這麼多廢話,只是想說明Spring是怎麼解析<dubbo:.../>
配置的。
知道了DUBBO和Spring關於配置一塊時怎麼整合的之後,那麼你應該就不會詫異Spring怎麼那麼聰明,能夠解析dubbo的namespace。接下來看看DubboNamespaceHandler
類裏面有什麼東西。
<!--lang:java-->
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}
public void init() {
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}
}
可以看到再init
方法裏面都是調用一個方法registerBeanDefinitionParser
,但是參數略微有些不同。registerBeanDefinitionParser
方法的第一個參數是dubbo的namespace下面節點名稱,第二個參數時該節點由誰來進行解析。例如:registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
是解析<dubbo:application../>
配置信息的,依此類推通過Spring可以配置<dubbo:module../>
,<dubbo:registry../>
等等,就不一一列舉了。至於每個標籤配置的作用,由於不是本篇的內容,所以這裏就不做過多的介紹。
通過上面應該清楚知道DUBBO可以配置哪些?這個問題應該不會再困擾你了。下面看看DUBBO是怎麼加載這些配置項的。
如何讀取我們的配置
通過Spring的Bean配置讀取
在項目中,會配置Spring的XML(雖然DUBBO也支持註解形式,但是個人不是很推崇,因爲這樣會將DUBBO整合到你的項目源碼裏面,而我的建議是不要講DUBBO和你的項目綁的太緊,我的建議是DUBBO的配置和項目隔離,從而方便管理,加入以後項目不使用DUBBO,而使用其他的分佈式RPC框架,對項目的調整會比較小),比如經常會通過下面的配置項引用一個遠程的服務:
<!--lang:xml-->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" timeout="2000" check="false"/>
通過上面的內容,應該知道dubbo:reference
將會由new DubboBeanDefinitionParser(ReferenceBean.class, false)
來解析(雖然看上去貌似所有配置都是用DubboBeanDefinitionParser
來解析,但是有很大的不同)。那看看類DubboBeanDefinitionParser
裏面做了什麼事情。
<!--lang:java-->
public class DubboBeanDefinitionParser implements BeanDefinitionParser {
private static final Logger logger = LoggerFactory.getLogger(DubboBeanDefinitionParser.class);
private final Class<?> beanClass;
private final boolean required;
public DubboBeanDefinitionParser(Class<?> beanClass, boolean required) {
this.beanClass = beanClass;
this.required = required;
}
public BeanDefinition parse(Element element, ParserContext parserContext) {
return parse(element, parserContext, beanClass, required);
}
@SuppressWarnings("unchecked")
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {....}
}
首先看類的定義,DubboBeanDefinitionParser
實現了Spring的BeanDefinitionParser
接口,該接口時專門用來解析Bean的定義的(一看類名就應該知道),並且實現了public BeanDefinition parse(Element element, ParserContext parserContext)
方法(別看整個方法返回了一個BeanDefinition
對象,其實Spring並沒有利用整個返回的對象,具體你可以看看Spring的源碼,所以要把Bean的定義注入到Spring容器中,就需要手動的往Spring中注入,因爲Spring沒有給我們來做這件事請。),然後調用了靜態方法private static BeanDefinition parse(...)
,那麼該類主要就是在靜態的方法parse
上了。由於該方法內容實在是太長了,不便粘貼出全部內容,我只分析主要的部分。在DubboBeanDefinitionParser
構造方法參數上有一個Class<?> beanClass
參數,它就是指定講當前標籤配置內容轉換成對應類的BeanDefinition
並且注入到Spring容器中。
<!--lang:java-->
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClass(beanClass);
beanDefinition.setLazyInit(false);
String id = element.getAttribute("id");
if ((id == null || id.length() == 0) && required) {
//構造一個Bean的ID
}
....
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
....
for (Method setter : beanClass.getMethods()) {
String name = setter.getName();
if (name.length() > 3 && name.startsWith("set")
&& Modifier.isPublic(setter.getModifiers())
&& setter.getParameterTypes().length == 1) {
Class<?> type = setter.getParameterTypes()[0];
String property = StringUtils.camelToSplitName(name.substring(3, 4).toLowerCase() + name.substring(4), "-");
props.add(property);
Method getter = null;
try {
getter = beanClass.getMethod("get" + name.substring(3), new Class<?>[0]);
} catch (NoSuchMethodException e) {
try {
getter = beanClass.getMethod("is" + name.substring(3), new Class<?>[0]);
} catch (NoSuchMethodException e2) {
}
}
if (getter == null
|| ! Modifier.isPublic(getter.getModifiers())
|| ! type.equals(getter.getReturnType())) {
continue;
}
if ("parameters".equals(property)) {
parameters = parseParameters(element.getChildNodes(), beanDefinition);
} else if ("methods".equals(property)) {
parseMethods(id, element.getChildNodes(), beanDefinition, parserContext);
} else if ("arguments".equals(property)) {
parseArguments(id, element.getChildNodes(), beanDefinition, parserContext);
}
.....
beanDefinition.getPropertyValues().addPropertyValue(property, reference);
......
上面代碼很顯然看到是通過反射的形式獲取類的get/set
方法然,從而判斷該參數是否可以通過Spring注入進去,最後添加到beanDefinition
中,並且注入到Spring容器中。上面則是從Spring中加載配置的機制。
通過java運行命令-D以及properties中獲取配置
上面內容看到DUBBO可以對哪些模塊進行配置,並且通過哪些類來存儲這些配置信息,例如:ReferenceBean
,RegistryConfig
,ServiceBean
等,如果進去看看這些類的定義會發現他們都繼承了AbstractConfig
抽象類,該抽象類中定義了protected static void appendProperties(AbstractConfig config)
方法,參數時一個實現它自己的子類,接下來看看它做了什麼:
<!--lang:java-->
protected static void appendProperties(AbstractConfig config) {
if (config == null) {
return;
}
String prefix = "dubbo." + getTagName(config.getClass()) + ".";
Method[] methods = config.getClass().getMethods();
for (Method method : methods) {
try {
String name = method.getName();
if (name.length() > 3 && name.startsWith("set") && Modifier.isPublic(method.getModifiers())
&& method.getParameterTypes().length == 1 && isPrimitive(method.getParameterTypes()[0])) {
String property = StringUtils.camelToSplitName(name.substring(3, 4).toLowerCase() + name.substring(4), "-");
String value = null;
if (config.getId() != null && config.getId().length() > 0) {
String pn = prefix + config.getId() + "." + property;
value = System.getProperty(pn);
if(! StringUtils.isBlank(value)) {
logger.info("Use System Property " + pn + " to config dubbo");
}
}
if (value == null || value.length() == 0) {
String pn = prefix + property;
value = System.getProperty(pn);
if(! StringUtils.isBlank(value)) {
logger.info("Use System Property " + pn + " to config dubbo");
}
}
if (value == null || value.length() == 0) {
Method getter;
try {
getter = config.getClass().getMethod("get" + name.substring(3), new Class<?>[0]);
} catch (NoSuchMethodException e) {
try {
getter = config.getClass().getMethod("is" + name.substring(3), new Class<?>[0]);
} catch (NoSuchMethodException e2) {
getter = null;
}
}
if (getter != null) {
if (getter.invoke(config, new Object[0]) == null) {
if (config.getId() != null && config.getId().length() > 0) {
value = ConfigUtils.getProperty(prefix + config.getId() + "." + property);
}
if (value == null || value.length() == 0) {
value = ConfigUtils.getProperty(prefix + property);
}
if (value == null || value.length() == 0) {
String legacyKey = legacyProperties.get(prefix + property);
if (legacyKey != null && legacyKey.length() > 0) {
value = convertLegacyValue(legacyKey, ConfigUtils.getProperty(legacyKey));
}
}
}
}
}
if (value != null && value.length() > 0) {
method.invoke(config, new Object[] {convertPrimitive(method.getParameterTypes()[0], value)});
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
上面方法一開始是生產當前配置的前綴String prefix = "dubbo." + getTagName(config.getClass()) + "."
,此處可以看到dubbo的所有配置項都是以dubbo.
開始的,接下來是通過getTagName
方法生成當前配置類的配置名稱,進去看看getTagName
是怎麼爲各個配置類生成各自的配置名的。
<!--lang:java-->
private static final String[] SUFFIXS = new String[] {"Config", "Bean"};
private static String getTagName(Class<?> cls) {
String tag = cls.getSimpleName();
for (String suffix : SUFFIXS) {
if (tag.endsWith(suffix)) {
tag = tag.substring(0, tag.length() - suffix.length());
break;
}
}
tag = tag.toLowerCase();
return tag;
}
分析上面的代碼很清楚的知道是怎麼生成各自配置類的配置名的,比如:ReferenceBean
通過該方法會返回reference
。所以關於引用遠程Bean的配置都是以dubbo.reference.
開頭的。生成當前配置類的前綴之後,那麼還是按照管理反射出當前類的所有set
方法,接下來就是從System.getProperty
中取,如果其中沒有則會從ConfigUtils.getProperty
中取。其中需要注意的是:從System.getProperty
中取值並沒有判斷當前屬性是否有值(通過spring注入的),而從ConfigUtils.getProperty
中取會判斷是否有值,如果沒有才會從其中取,這裏可以說明DUBBO配置項的優先級java -D優先於Spring配置,Spring配置優先於properties
文件的配置,這也符合一般項目的規則。不知道大家注意到沒,不管是System.getProperty
還是ConfigUtils.getProperty
都會取兩次,一次是String pn = prefix + config.getId() + "." + property
,另一次是String pn = prefix + property
,這兩種的區別就是多了一個config.getId()
,可能會好奇config.getId()
是什麼內容呢?在介紹Spring加載配置的時候,應該知道config
對象其實是Spring容器裏面的,那麼這裏的getId
,其實就是我們在配置Spring的Bean時候配置的ID。例如:
<!--lang:java-->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" timeout="2000" check="false" />
該配置會產生一個ReferenceBean
對象,那麼此時的config.getId()
就是demoService
。所以String pn = prefix + config.getId() + "." + property
配置的目的是有沒有針對當前某個Bean的配置項。比如:配置dubbo消費端的超時時間,一般通過dubbo.reference.timeout
,這其實是指定消費端所有Bean的超時時間。有時候我們需要指定某個Bean超時時間可以通過dubbo.reference.{beanId}.timeout
來指定,例如上面的可以通過dubbo.reference.demoService.timeout
來配置該Bean的超時時間。
到此對DUBBO所有配置途徑以及其原理進行了分析和介紹,貌似還沒說怎麼取發現dubbo有哪些配置。這裏教大家怎麼去發現DUBBO有哪些配置。上面介紹過,DUBBO將配置項設置到各個配置類的實體中都是通過判斷是否有get/set
方法,到這裏大家應該清楚怎麼去發現了吧?就是如果想了解dubbo某個模塊的配置,直接到對應的配置類中看它有哪些字段,知道它的字段名,以及確定你要配置的範圍,是全局還是某個bean,還有你想通過什麼途徑來配置,按照dubbo提供的規則很好確定對應的字段怎麼來進行配置。那麼你可能會說,字段時一個單詞知道是怎麼配置,那麼如果字段時多個單詞組合的呢?由於java的編碼風格一般時駝峯格式第:第一個單詞首字母小寫後面單詞首字母大寫,那麼這種情況對應dubbo來說怎麼獲取該配置項呢?看下面:
<!--lang:java-->
//name是get/set方法名
String property = StringUtils.camelToSplitName(name.substring(3, 4).toLowerCase() + name.substring(4), "-");
不難發現對於駝峯,dubbo時通過-
字符來分割各個單詞。其實到這裏基本上可以很好的配置DUBBO了,但是有一個上面的途徑如果需要調整參數都需要重啓應用,達不到動態調整,於是dubbo爲了能夠滿足在項目不重啓的情況下可以動態的調整配置參數。
通過DUBBO管理應用添加動態配置
這種方式主要原理就是通過管理器將動態參數發佈到註冊中心(zookeeper)中,然後各個節點可以獲得最新的配置變更,然後進行動態調整。想知道這一塊內容,需要取看看類RegistryDirectory
的實現,該類它實現了NotifyListener
。那麼它就可以監聽到註冊中心的任何變更。再瞭解RegistryDirectory
之前,先看看DUBBO對每個服務在註冊中心存儲了哪些信息?如果用的時zookeeper,那麼你會在每個服務節點目錄下面看到一下幾個目錄consumers
,providers
,configurators
,routers
。其中動態配置時放在configurators
節點目錄下。服務消費端會監聽configurators
目錄變更,如果變更則會調用RegistryDirectory
的void notify(List<URL> urls)
方法。監聽configurators
目錄變更觸發的void notify(List<URL> urls)
方法時,urls
的是類似override://...
,表示將覆蓋調用該服務的某些配置(dubbo中對所有的調用配置都是通過URL的形式來展示的),講這些URL上面的參數信息替換到調用服務端的URL上面取,並且重新構造該服務的Invoke
對象,從而達到更新參數的目的。
可以給接口的方法配置參數
整個場景在實際項目中是存在的,比如一個接口存在多個方法,有時候講參數配置現在再接口層面還是不夠的,需要精確到方法級別,例如對接口調用的超時時間設置。dubbo對與方法級別的配置提供了兩種途徑。
Spring Bean的方式配置
如下進行配置:
<!--lang:xml-->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" timeout="2000" check="false" >
<dubbo:method name="sayHello" timeout="1000"/>
</dubbo:reference>
通過多嵌套一個dubbo:method
標籤來實現。
動態配置
在DUBBO的管理器提供了動態配置的功能,通過添加動態配置,以及指定通知的消費端來指定某個服務的某個方法調整參數。它那裏配置的規則時方法名加配置項的名稱,例如:key配置成:sayHello.timeout
,value=”10000”。那麼管理器會在註冊中心的對應服務的configurators
添加一條override://...?sayHello.timeout=10000
節點,指定消費端會監聽到這個變更,執行上面內容的流程。
到此關於DUBBO配置已經介紹完畢,可能有一些地方還時沒說明清楚,存在疑慮。如果有疑問歡迎提問或者自己取看看相關的DUBBO源碼,那一定會了解更加清楚。