前言
? ? ? ?Apache Dubbo? (incubating)是一款高性能Java RPC框架渡八。在平常業(yè)務(wù)開發(fā)過程中使用的越來越頻繁,同時(shí)也會(huì)遇到更多的問題券盅。這就需要我們更多的了解一下dubbo源碼其障,以便更好的處理問題霉撵。
? ? ??看源碼的話就會(huì)直面一個(gè)棘手的問題:不知道從哪下手浙于,找不到切入點(diǎn)芹助。所以堂湖,本文準(zhǔn)備就dubbo的啟動(dòng)過程做一下宏觀的流程分析,希望對(duì)大家有所幫助周瞎。
問題引入
? ? ??用過dubbo的同學(xué)都知道苗缩,我們只需要在xml文件中配置zk、協(xié)議声诸、要暴露的服務(wù)等信息酱讶,發(fā)布jar包、然后啟動(dòng)spring彼乌。我們的服務(wù)就可以被調(diào)用了泻肯。如下,我們暴露了HelloService慰照。啟動(dòng)spring就可以被遠(yuǎn)程調(diào)用了:
<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" ></dubbo:service>
直入主題
? ? ??那么在spring容器啟動(dòng)的過程中灶挟,都做了什么操作才使我們的服務(wù)可以暴露出去呢?為什么dubbo是透明化接入應(yīng)用毒租,對(duì)應(yīng)用沒有任何 API 侵入的呢稚铣?
1.Spring可擴(kuò)展Schema的支持
? ? ??Spring框架從2.0版本開始,提供了基于Schema風(fēng)格的XML擴(kuò)展機(jī)制墅垮,允許開發(fā)者擴(kuò)展最基本的spring配置文件惕医,這樣我們就可以編寫自定義的xml bean解析器然后集成到Spring IoC容器中。
也就是說利用這個(gè)機(jī)制就可以把我們?cè)趚ml文件中配置的東西實(shí)例化成對(duì)象算色。
使用這種機(jī)制需要通過以下幾步:
- 設(shè)計(jì)配置屬性和JavaBean
- 編寫XSD文件
- 編寫NamespaceHandler和BeanDefinitionParser完成解析工作
- 編寫spring.handlers和spring.schemas串聯(lián)起所有部件
接著我們以dubbo的provider為例開始分析
<dubbo:provider registry="test_zk" version="1.0.0" iothreads="300" retries="0"/>
spring啟動(dòng)過程中會(huì)去掃描META-INF/spring.schemas抬伺,拿到dubbo的擴(kuò)展配置,然后根據(jù)配置找到META-INF/dubbo.xsd文件
http\://dubbo.apache.org/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd
至于spring為什么會(huì)掃面META-INF/spring.schemas目錄呢灾梦?答案在PluggableSchemaResolver.java中
public class PluggableSchemaResolver implements EntityResolver {
public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);
private final ClassLoader classLoader;
private final String schemaMappingsLocation;
private volatile Map<String, String> schemaMappings;
public PluggableSchemaResolver(ClassLoader classLoader) {
this.classLoader = classLoader;
this.schemaMappingsLocation = "META-INF/spring.schemas";
}
}
dubbo.xsd文件中定義了我們Bean的標(biāo)簽峡钓,和Bean中定義的字段一一對(duì)應(yīng)的妓笙;
這一步spring會(huì)把dubbo.xsd文件解析成 Dom 樹,在解析的自定義標(biāo)簽的時(shí)候能岩, spring 會(huì)根據(jù)標(biāo)簽的命名空間和標(biāo)簽名找到一個(gè)解析器寞宫。
這個(gè)命名空間就是targetNamespace。拿到這個(gè)參數(shù)去掃面META-INF/spring.handlers捧灰。拿到dubbo配置的handler路徑
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns="http://dubbo.apache.org/schema/dubbo"
targetNamespace="http://dubbo.apache.org/schema/dubbo">
http\://dubbo.apache.org/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
這樣就找到了DubboNamespaceHandler,由該解析器來完成對(duì)該標(biāo)簽內(nèi)容的解析淆九,并返回一個(gè) BeanDefinition 。
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
public DubboNamespaceHandler() {
}
public void init() {
this.registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
this.registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
this.registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
this.registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
this.registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
this.registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
this.registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
this.registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
this.registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
this.registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
}
static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}
在這個(gè)過程中就會(huì)把dubbo自定義的schema配置初始化成Bean對(duì)象毛俏,并維護(hù)在spring容器中炭庙。
(深入了解schema機(jī)制 可參考:https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/xsd-configuration.html)
2.spring 事件機(jī)制
dubbo使用到的配置信息都已經(jīng)托管在spring容器中了,服務(wù)又是怎么暴露的呢煌寇?萬事俱別焕蹄,只欠東風(fēng),此時(shí)就需要一個(gè)觸發(fā)dubbo服務(wù)啟動(dòng)的事件阀溶。
public void onApplicationEvent(ApplicationEvent event) {
if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {
if (isDelay() && ! isExported() && ! isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
}
3.服務(wù)暴露
從export()方法開始,才真正進(jìn)入了dubbo的服務(wù)暴露流程更振,在這個(gè)過程中就會(huì)涉及到多協(xié)議暴露服務(wù)炕桨、注冊(cè)zk、暴露本地和遠(yuǎn)程服務(wù)肯腕,獲取invoker献宫,將invoker轉(zhuǎn)化成exporter等一系列操作。如同官方提供的那樣:
接著會(huì)到ServiceConfig.export()方法实撒,這里面涉及到dubbo服務(wù)延遲暴露的一個(gè)點(diǎn)姊途,delay這個(gè)參數(shù)可以配置在<dubbo:provider/> 或者<dubbo:service/>中,目的是為了延遲注冊(cè)服務(wù)時(shí)間(毫秒) 知态,設(shè)為-1時(shí)捷兰,表示延遲到Spring容器初始化完成時(shí)暴露服務(wù)。一些特殊的場(chǎng)景肴甸,可以通過修改該參數(shù)來解決服務(wù)剛啟動(dòng)接口響應(yīng)較慢的案例寂殉。
ServiceConfig.doExport()主要是做一些合法性的校驗(yàn)工作:
- application®istry&protocol等有效性檢查囚巴;
- <dubbo:service>中配置的interface合法性檢查:接口不能為空,檢查接口類型必需為接口,檢查方法是否在接口中存在.(checkInterfaceAndMethods)
- 檢查xml配置中interface和ref是否匹配(interfaceClass.isInstance(ref))
- 有效性檢查通過后原在,調(diào)用doExportUrls()發(fā)布dubbo服務(wù)友扰;
在ServiceConfig.doExportUrls()方法,這里會(huì)進(jìn)行多協(xié)議暴露服務(wù)庶柿,由于dubbo不僅支持dubbo協(xié)議同時(shí)還支持http村怪、webservice、thrift等協(xié)議浮庐。如果我們配置的service需要同時(shí)提供多種服務(wù)甚负,那么會(huì)根據(jù)不同的協(xié)議進(jìn)行循環(huán)暴露。
<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" protocol="dubbo"></dubbo:service>
<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" protocol="http"></dubbo:service>
在doExportUrlsFor1Protocol中會(huì)把所有的相關(guān)屬性封裝到Map中审残,構(gòu)造dubbo定義的統(tǒng)一數(shù)據(jù)模型URL,這個(gè)url會(huì)貫穿服務(wù)暴露和調(diào)用的整個(gè)流程梭域。
URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);
接著是根據(jù)參數(shù)scope判斷服務(wù)的發(fā)布范圍。服務(wù)的發(fā)布范圍分為不暴露搅轿、本地暴露病涨、遠(yuǎn)程暴露。
scope的配置規(guī)則如下:
- 如果配置scope=none,不發(fā)布這個(gè)dubbo服務(wù)璧坟;
- 如果配置scope=local既穆,只本地暴露這個(gè)dubbo服務(wù);
- 如果配置remote雀鹃,只遠(yuǎn)程暴露這個(gè)dubbo服務(wù)
- 如果不配置或者不為以上三種幻工,既暴露本地服務(wù),又暴露遠(yuǎn)程服務(wù)黎茎;
//配置為none不暴露
if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
if (registryURLs != null && registryURLs.size() > 0
&& url.getParameter("register", true)) {
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));
URL monitorUrl = loadMonitor(registryURL);
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
if (logger.isInfoEnabled()) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
}
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
Exporter<?> exporter = protocol.export(invoker);
exporters.add(exporter);
}
} else {
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
Exporter<?> exporter = protocol.export(invoker);
exporters.add(exporter);
}
}
}
那么為什么會(huì)有本地暴露呢?因?yàn)樵赿ubbo中我們一個(gè)服務(wù)可能既是Provider,又是Consumer,因此就存在他自己調(diào)用自己服務(wù)的情況,如果再通過網(wǎng)絡(luò)去訪問囊颅,就會(huì)白白增加一層網(wǎng)絡(luò)開銷。所以本地暴露和遠(yuǎn)程暴露的區(qū)別如下:
- 本地暴露是暴露在JVM中,不需要網(wǎng)絡(luò)通信.
-
遠(yuǎn)程暴露是將ip,端口等信息暴露給遠(yuǎn)程客戶端,調(diào)用時(shí)需要網(wǎng)絡(luò)通信.
本地暴露服務(wù)的時(shí)候url是以injvm開頭的工三,而遠(yuǎn)程服務(wù)是以registry開頭的迁酸,如圖示:
injvm.png
registry.png上面代碼也可以看出來,本地暴露和遠(yuǎn)程暴露的本質(zhì)都是是通過把拼裝好的url轉(zhuǎn)換成invoker俭正,然后把invoker轉(zhuǎn)換為exporter奸鬓。
點(diǎn)開getInvoker方法
/**
* create invoker.
*
* @param <T>
* @param proxy
* @param type
* @param url
* @return invoker
*/
@Adaptive({Constants.PROXY_KEY})
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;
這里用到了Adaptive,就會(huì)生成動(dòng)態(tài)編譯的Adaptive類掸读。這個(gè)類就是getInvoker方法的具體實(shí)現(xiàn)串远。
拿到invoker之后,調(diào)用protocol.export(invoker)把invoker轉(zhuǎn)換成exporter儿惫。
/**
* 暴露遠(yuǎn)程服務(wù):<br>
* 1. 協(xié)議在接收請(qǐng)求時(shí)澡罚,應(yīng)記錄請(qǐng)求來源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
* 2. export()必須是冪等的,也就是暴露同一個(gè)URL的Invoker兩次肾请,和暴露一次沒有區(qū)別留搔。<br>
* 3. export()傳入的Invoker由框架實(shí)現(xiàn)并傳入,協(xié)議不需要關(guān)心铛铁。<br>
*
* @param <T> 服務(wù)的類型
* @param invoker 服務(wù)的執(zhí)行體
* @return exporter 暴露服務(wù)的引用隔显,用于取消暴露
* @throws RpcException 當(dāng)暴露服務(wù)出錯(cuò)時(shí)拋出却妨,比如端口已占用
*/
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
到這里就是服務(wù)暴露的總流程。
4.netty服務(wù)啟動(dòng)
在invoker->exporter轉(zhuǎn)換的過程中又涉及到了dubbo連接池的創(chuàng)建和netty的初始化括眠。
在createServer方法中利用dubbo SPI機(jī)制找到NettyTransporter,new NettyServer()->doOpen().最終我們就看到boss 線程掷豺,worker 線程捞烟,和 ServerBootstrap。
而 Client 在 Spring getBean 的時(shí)候德频,會(huì)創(chuàng)建 Client.當(dāng)調(diào)用遠(yuǎn)程方法的時(shí)候婴程,將數(shù)據(jù)通過 dubbo 協(xié)議編碼發(fā)送到 NettyServer,然后 NettServer 收到數(shù)據(jù)后解碼抱婉,并調(diào)用本地方法档叔,并返回?cái)?shù)據(jù),完成一次RPC 調(diào)用蒸绩。
final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
NettyHandler類它繼承了netty框架的SimpleChannelHandler類衙四,重寫了messageReceived方法。接收到客戶端請(qǐng)求的入口就是messageReceived方法
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
try {
handler.received(channel, e.getMessage());
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
}
}
執(zhí)行了handler的received方法患亿,這個(gè)handler其實(shí)就是DubboProtocol中的requestHandler传蹈,因?yàn)樵趩?dòng)netty服務(wù)的時(shí)候,就將requestHandler對(duì)象一直傳遞到了NettyServer步藕,再通過NettyServer類的構(gòu)造函數(shù)將它保存到了NettyServer類的終極父類AbstractPeer的handler屬性上惦界,AbstractPeer類又實(shí)現(xiàn)了ChannelHandler接口,重寫了received方法咙冗。
所以當(dāng)netty框架接收到請(qǐng)求時(shí)執(zhí)行messageReceived方法里面的handler.received(channel, e.getMessage()); 沾歪,其實(shí)執(zhí)行的是AbstractPeer類的received方法,received然后里面又執(zhí)行了handler.received(ch, msg); 然后received中又調(diào)用了reply方法雾消;
在reply方法中灾搏,完成了數(shù)據(jù)的解碼,和合法性校驗(yàn)立润。最終調(diào)用本地方法狂窑,返回?cái)?shù)據(jù),完成一次RPC 調(diào)用桑腮。
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
if (message instanceof Invocation) {
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);
//如果是callback 需要處理高版本調(diào)用低版本的問題
if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))){
String methodsStr = invoker.getUrl().getParameters().get("methods");
boolean hasMethod = false;
if (methodsStr == null || methodsStr.indexOf(",") == -1){
hasMethod = inv.getMethodName().equals(methodsStr);
} else {
String[] methods = methodsStr.split(",");
for (String method : methods){
if (inv.getMethodName().equals(method)){
hasMethod = true;
break;
}
}
}
if (!hasMethod){
logger.warn(new IllegalStateException("The methodName "+inv.getMethodName()+" not found in callback service interface ,invoke will be ignored. please update the api interface. url is:" + invoker.getUrl()) +" ,invocation is :"+inv );
return null;
}
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
return invoker.invoke(inv);
}
throw new RemotingException(channel, "Unsupported request: " + message == null ? null : (message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
}
@Override
public void received(Channel channel, Object message) throws RemotingException {
if (message instanceof Invocation) {
reply((ExchangeChannel) channel, message);
} else {
super.received(channel, message);
}
}
最后
? ? ? ?如圖泉哈,dubbo的的模型十分易懂,但涉及到的東西確實(shí)很多。以上只是對(duì)第一步:0.start 做了一個(gè)簡單的流水賬分析丛晦。
? ? ? 所以巨缘,本文只是想做個(gè)引子,更多的細(xì)節(jié)還需要靠大家去挖掘采呐。剩下的只有去debug the universe了。