寫在前面:2020年面試必備的Java後端進階面試題總結了一份複習指南在Github上,內容詳細,圖文並茂,有需要學習的朋友可以Star一下!
GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master
一、引言
服務引用有 直連 和 註冊中心 兩種方式,一般來說直連方式不推薦用於生產,僅提供測試或預發佈的調試使用。所以本篇重點分析通過註冊中心引用服務的過程。
二、服務引用的起點
Dubbo 服務引用的起點有兩個,一般來說我們都是以 ReferenceBean 對應的服務注入形式使用,例如常用的註解形式 @DubboReference ( @Reference 註解在新版 Dubbo 中已廢棄);另一種是 Spring 容器調用 ReferenceBean 的 #afterPropertiesSet 方法時引用服務,這種方式需要配置 <dubbo:reference> 的 init 屬性開啓,Dubbo 默認使用第一種方式。
我們從 ReferenceBean 入手:
ReferenceBean 實現了 FactoryBean 和 InitializingBean :
- InitializingBean
看一下 #afterPropertiesSet 方法:
@Override
@SuppressWarnings({"unchecked"})
public void afterPropertiesSet() throws Exception { // 初始化 Dubbo config bean
prepareDubboConfigBeans();
// lazy init by default.
if (init == null) {
init = false;
}
// eager init if necessary.
if (shouldInit()) {
getObject();
}
}
- FactoryBean
Spring 通過調用 #getBean 方法可以返回 bean 的實例,而實現了 FactoryBean 接口,會調用 #getObject 方法返回 bean 的實例。
@Override
public Object getObject() {
// 調用 ReferenceConfig 的 get 方法獲取 bean 實例
return get();
}
比較上面兩個方法,我們可以知道 Spring 在實例化 Dubbo 的 ReferenceBean 時會調用 ReferenceConfig 的 #get 方法獲取 bean 實例,執行 Dubbo 服務引用的過程。
三、配置檢查處理
繼續跟隨 ReferenceConfig 的 #get 方法:
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
} // 若服務引用代理爲空,執行 init 方法
if (ref == null) {
// 處理配置,調用 createProxy 生成代理類
init();
}
return ref;
}
- ReferenceConfig 的 init( ) 方法
public synchronized void init() {
// 標識是否已經初始化,避免重複初始化
if (initialized) {
return;
} // 獲取 DubboBootstrap 引導類實例 if (bootstrap == null) {
bootstrap = DubboBootstrap.getInstance(); bootstrap.init(); } // 檢查接口、consumer 等配置是否合法,並對相應的配置賦值 checkAndUpdateSubConfigs(); // 本地存根檢查
checkStubAndLocal(interfaceClass); ConfigValidationUtils.checkMock(interfaceClass, this); Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, CONSUMER_SIDE); // 加入運行時參數,Dubbo 版本號、時間戳、進程號等
ReferenceConfigBase.appendRuntimeParameters(map);
// 是否爲泛化接口
if (!ProtocolUtils.isGeneric(generic)) {
String revision = Version.getVersion(interfaceClass, version); if (revision != null && revision.length() > 0) {
map.put(REVISION_KEY, revision); } // 獲取接口的方法列表,加入 map
String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); if (methods.length == 0) {
logger.warn("No method found in service interface " + interfaceClass.getName());
map.put(METHODS_KEY, ANY_VALUE); } else {
map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), COMMA_SEPARATOR)); } } map.put(INTERFACE_KEY, interfaceName); // 將 ApplicationConfig、ConsumerConfig、ReferenceConfig 等對象的字段信息添加到 map 中
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
AbstractConfig.appendParameters(map, consumer);
AbstractConfig.appendParameters(map, this);
Map<String, AsyncMethodInfo> attributes = null; if (CollectionUtils.isNotEmpty(getMethods())) {
attributes = new HashMap<>(); for (MethodConfig methodConfig : getMethods()) {
AbstractConfig.appendParameters(map, methodConfig, methodConfig.getName());
String retryKey = methodConfig.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey); if ("false".equals(retryValue)) {
map.put(methodConfig.getName() + ".retries", "0");
} } AsyncMethodInfo asyncMethodInfo = AbstractConfig.convertMethodConfig2AsyncInfo(methodConfig); if (asyncMethodInfo != null) {
attributes.put(methodConfig.getName(), asyncMethodInfo); } } } // 從系統變量中獲取服務消費者 ip String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY); if (StringUtils.isEmpty(hostToRegistry)) {
hostToRegistry = NetUtils.getLocalHost(); } else if (isInvalidLocalHost(hostToRegistry)) {
throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
} map.put(REGISTER_IP_KEY, hostToRegistry); // 存儲配置數據
serviceMetadata.getAttachments().putAll(map);
// 創建代理
ref = createProxy(map);
serviceMetadata.setTarget(ref);
serviceMetadata.addAttribute(PROXY_CLASS_REF, ref);
ConsumerModel consumerModel = repository.lookupReferredService(serviceMetadata.getServiceKey()); consumerModel.setProxyObject(ref);
consumerModel.init(attributes); initialized = true; // 發佈 ReferenceConfigInitializedEvent 事件
dispatch(new ReferenceConfigInitializedEvent(this, invoker)); }
代碼較長,主要是各種配置的檢查和初始化,並收集這些信息加入 map 存儲,以及創建代理。
四、引用服務
接着上面我們繼續看 #createProxy 方法,其不僅執行創建代理的邏輯,同時還會調用其他方法創建、合併 Invoker 實例。
private T createProxy(Map<String, String> map) {
// 判斷是否本地暴露,包含指定服務 url 直連的情況判斷、或根據參數配置是否進行本地暴露,如協議、scope、injvm 等
if (shouldJvmRefer(map)) { // 本地引用
// 創建 URL,協議爲 njvm
URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
// 調用 refer 方法創建 InjvmInvoker 實例
invoker = REF_PROTOCOL.refer(interfaceClass, url); if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
} // 遠程引用
} else {
urls.clear(); // 若 url 不爲空
if (url != null && url.length() > 0) {
// 配置多個 url 時,用分號分隔
String[] us = SEMICOLON_SPLIT_PATTERN.split(url); if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u); if (StringUtils.isEmpty(url.getPath())) {
// 設置 url 路徑爲接口全限定名
url = url.setPath(interfaceName); } // 協議爲 registry 時,指定註冊中心
if (UrlUtils.isRegistry(url)) {
// 將 map 轉換爲查詢字符串,賦值給 refer
urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map))); } else {
// 合併 url,移除服務提供者的一些配置(這些配置來源於用戶配置的 url 屬性),
// 比如線程池相關配置。並保留服務提供者的部分配置,比如版本,group,時間戳等
// 最後將合併後的配置設置爲 url 查詢字符串中。
urls.add(ClusterUtils.mergeUrl(url, map)); } } } } else { // 從註冊中心的配置中組裝 URL
// 協議不是 injvm
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
checkRegistry(); List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
if (monitorUrl != null) {
map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString())); } // 添加 refer 參數到 url,並加入 urls 集合
urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map))); } } // 沒有配置註冊中心,拋出異常
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
} } } // 只有一個註冊中心或者服務提供者
if (urls.size() == 1) {
// 構建 invoker 實例
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
} else { // 多個註冊中心或多個服務提供者,或者兩者混合
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
invokers.add(REF_PROTOCOL.refer(interfaceClass, url)); if (UrlUtils.isRegistry(url)) {
// 最後一個註冊中心 URL
registryURL = url; } } if (registryURL != null) {
// 如果註冊中心鏈接不爲空,則將使用 ZoneAwareCluster
String cluster = registryURL.getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME); // 創建 StaticDirectory 實例,並由 Cluster 對多個 Invoker 進行合併
invoker = Cluster.getCluster(cluster, false).join(new StaticDirectory(registryURL, invokers));
} else { // not a registry url, must be direct invoke.
String cluster = CollectionUtils.isNotEmpty(invokers) ? (invokers.get(0).getUrl() != null ? invokers.get(0).getUrl().getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME) : Cluster.DEFAULT)
: Cluster.DEFAULT; invoker = Cluster.getCluster(cluster).join(new StaticDirectory(invokers));
} } } if (shouldCheck() && !invoker.isAvailable()) {
invoker.destroy(); throw new IllegalStateException("Failed to check the status of the service "
+ interfaceName + ". No provider available for the service "
+ (group == null ? "" : group + "/")
+ interfaceName + (version == null ? "" : ":" + version)
+ " from the url "
+ invoker.getUrl() + " to the consumer "
+ NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
} if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
} // create service proxy
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
在 #createProxy 方法中,首先檢查配置是否是本地暴露,如果是,則根據自適應擴展機制獲取 InjvmProtocol,並調用 #refer 方法生成 InjvmInvoker 實例,完成服務引用。相反,則讀取直連 url 配置,或讀取註冊中心 url ,並將其存儲到 urls 集合中,根據 urls 的大小進行不同的處理。如果 urls 大小爲 1,則直接根據自適應擴展調用調用 #refer 方法生成 invoker 。如果 urls 大於 1,則分別根據 url 生成 invoker,然後再通過 Cluster 合併多個 invoker ,最後調用 ProxyFactory 生成代理類。
五、創建 Invoker
講到服務暴露時,我們同樣分析了 Invoker 的創建過程。Invoker 作爲 Dubbo 的通用模型,代表着一個可執行體。在服務提供者來看,Invoker 用於調用真實的服務實現類;而在服務消費者來看,Invoker 用於執行遠程調用,在上面創建代理的方法中,我們注意到創建 Invoker 的一個關鍵方法 Protocol#refer(Class<T> type, URL url) 。
Protocol 的實現有很多,我們還是以常見的 DubboProtocol 和 RegistryProtocol 來分析 refer 方法如何構建 Invoker 。
1. DubboProtocol
DubboProtocol 繼承了 AbstractProtocol 抽象類,從其 refer 方法入手:
@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
return new AsyncToSyncInvoker<>(protocolBindingRefer(type, url));
}
其調用了模板方法 #protocolBindingRefer(type, url) 。
回到 DubboProtocol#protocolBindingRefer(type, url) 方法:
@Override
public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url); // create rpc invoker.
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
上面代碼非常簡單,關注其中調用了 #getClients(url) 方法用於獲取客戶端實例,實例類型爲 ExchangeClient。
ExchangeClient 實際上並不具備通信能力,它需要基於更底層的客戶端實例進行通信。比如 NettyClient、MinaClient 等,默認情況下,Dubbo 使用 NettyClient 進行通信。
對於 DubboProtocol 的引用邏輯我們先大概瞭解這麼多,關於集羣、通信後面會詳細說。下面再看 RegistryProtocol 的 refer 方法:
@Override
@SuppressWarnings("unchecked")
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { // 通過參數獲取 Registry 的協議,並將其設置爲協議頭
url = getRegistryUrl(url);
// 獲取註冊中心實例
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// url 查詢字符串轉換爲 map
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
// 獲取 group 配置
String group = qs.get(GROUP_KEY);
// 多個 group
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(Cluster.getCluster(MergeableCluster.NAME), registry, type, url);
}
}
Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
return doRefer(cluster, registry, type, url);
}
繼續看 #doRefer 方法:
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 創建 RegistryDirectory 實例 RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
// 設置註冊中心和協議 directory.setRegistry(registry); directory.setProtocol(protocol); // all attributes of REFER_KEY Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
// 創建服務消費者 URL URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters); if (directory.isShouldRegister()) { directory.setRegisteredConsumerUrl(subscribeUrl); registry.register(directory.getRegisteredConsumerUrl()); } directory.buildRouterChain(subscribeUrl); directory.subscribe(toSubscribeUrl(subscribeUrl)); // 將多個服務提供者合併 Invoker<T> invoker = cluster.join(directory);
// 註冊中心此時沒有其他服務提供者 List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
if (CollectionUtils.isEmpty(listeners)) { return invoker; } // 多個服務提供者時,通過 Wrapper 包裹,並通知 RegistryProtocol 的監聽器 RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker);
for (RegistryProtocolListener listener : listeners) { listener.onRefer(this, registryInvokerWrapper); } return registryInvokerWrapper; }
如此,生成 Invoker 創建完畢,再根據服務接口生成代理對象,便可執行遠程調用,生成代理的部分邏輯和上篇服務暴露的入口一致,即 ProxyFactory 的 getProxy 方法,感興趣的小夥伴可自行查看。