1. 程式人生 > 程式設計 >Motan系列-Motan的服務註冊

Motan系列-Motan的服務註冊

Motan系列文章


本文將以 註解暴露服務 的方式探究Motan服務的註冊過程。

0 @MotanService註解是個啥

@MotanService 註解標記的類,在應用啟動時,會被Motan掃描,並作為服務的具體實現註冊到註冊中心中。

就像下面這樣:

@MotanService(export = "demoMotan:8002")
public class MotanDemoServiceImpl implements MotanDemoService {

    @Override
public String hello(String name) { System.out.println(name); return "Hello " + name + "!"; } @Override public User rename(User user,String name) throws Exception { Objects.requireNonNull(user); System.out.println(user.getId() + " rename " + user.getName() + " to "
+ name); user.setName(name); return user; } } 複製程式碼

Motan如何完成與Spring的整合 一文中已經說過應用啟動時,是如何掃描到 @MotanService 註解標記的類的,這裡不再贅述。

1 @MotanService的解析

@MotanService 的解析過程在 com.weibo.api.motan.config.springsupport.AnnotationBean 類中的 postProcessAfterInitialization(Object bean,String beanName)

方法實現。

首先會解析配置資訊,例如這個服務實現的介面是誰、以及applicationmoodulegroupversionfilter等的配置資訊,最後會將這些資訊封裝到 com.weibo.api.motan.config.springsupport.ServiceConfigBean 的物件中。

先來看一下 ServiceConfigBean 的UML圖:

Motan_ServiceConfigBean_UML

圖中最左側的繼承關係是 ServiceConfigBean 自身的繼承關係,右側的是Spring的相關擴充套件。這個圖這裡先有個印象,我們繼續上面的思路走。

Motan將配置資訊封裝到 ServiceConfigBean 後,呼叫了 afterPropertiesSet() 方法,由上圖可知,這個方法是 InitializingBean 介面中抽象方法的實現。

這個方法其實就幹了下面三件事兒:

@Override
public void afterPropertiesSet() throws Exception {
    // 檢查並配置basicConfig
    checkAndConfigBasicConfig();
    // 檢查是否已經裝配export,如果沒有則到basicConfig查詢
    checkAndConfigExport();
    // 檢查並配置registry
    checkAndConfigRegistry();
}
複製程式碼
  • 檢查配置解析過程後,ServiceConfigBean的 basicService 屬性是否為空,如果是空,需要重新解析並設定他的值。

如何重新設定他的值呢?從UML圖中我們可以找到 basicService 的位置,在 ServiceConfig 類中的第7個Field。欄位型別是 BasicServiceInterfaceConfig。這個檢查其實就是找到當前Spring容器中所有 BasicServiceInterfaceConfig 型別的bean,如果只找到一個,就把這個賦值到 basicService 上,如果有多個,需要找到 BasicServiceInterfaceConfigisDefault 屬性為true的那個,並賦值。

  • 檢查 export 的值是否已經設定,如果沒有設定,到 basicService 中查詢。

這一步其實是檢查 protocol,也就是 motanmotan2這些協議是否已經設定好,export欄位的格式為:protocol1:port1,protocol2:port2。對應到UML中,export欄位在 AbstractServiceConfig 類中。 這裡同時會將 export 的值解析到 AbstractInterfaceConfigprotocols 欄位中。

  • 檢查註冊中心的配置

例如 zookeeperconsul 這些是否已經配置好。如果是空的話,還是從 basicService 中查詢,並將結果配置到 AbstractInterfaceConfigregistries 屬性中。

這些都檢查好以後,basicServiceexportprotocolsregistries這些欄位就初始化好了。然後會將新創建出來的這個 ServiceConfigBean 例項新增到 AnnotationBeanserviceConfigs 屬性中。

private final Set<ServiceConfigBean<?>> serviceConfigs = new ConcurrentHashSet<ServiceConfigBean<?>>();
複製程式碼

至此 @MotanService 解析完成,可以準備釋出並註冊服務了。

2 服務的啟動

完成上述的解析和初始化後,會呼叫 ServiceConfigBeanexport() 方法來發布並註冊服務。

serviceConfig.export();
複製程式碼

其實現如下:

public synchronized void export() { // 這裡加了個併發的控制,鎖使用的是 this
    // 如果已經發布過了,直接返回
    if (exported.get()) {
        LoggerUtil.warn(String.format("%s has already been expoted,so ignore the export request!",interfaceClass.getName()));
        return;
    }

    // 檢查暴露服務的類是否是某個介面的實現,如果不是則丟擲異常
    // 檢查暴露的方法是否在介面中存在,如果沒有則丟擲異常
    checkInterfaceAndMethods(interfaceClass,methods);

    // 解析註冊中心地址,並將host、port等引數封裝到URL類中
    List<URL> registryUrls = loadRegistryUrls();
    if (registryUrls == null || registryUrls.size() == 0) {
        throw new IllegalStateException("Should set registry config for service:" + interfaceClass.getName());
    }

    // 解析協議和服務暴露的埠,預設為 `motan` 協議。Map的結構為:<協議,埠>
    Map<String,Integer> protocolPorts = getProtocolAndPort();
    for (ProtocolConfig protocolConfig : protocols) {
        Integer port = protocolPorts.get(protocolConfig.getId());
        if (port == null) {
            throw new MotanServiceException(String.format("Unknow port in service:%s,protocol:%s",interfaceClass.getName(),protocolConfig.getId()));
        }
        // 註冊並暴露服務
        doExport(protocolConfig,port,registryUrls);
    }

    afterExport();
}
複製程式碼

PS:URL這個類是Motan自己定義的類,Motan中幾乎所有跟URL相關的東西都用它封裝。

上述程式碼先初始化了暴露服務之前需要的一些資料:註冊中心地址、服務協議、暴露埠等,真正執行服務註冊的是 doExport 方法。這個方法較長,這裡只貼出關鍵部分。

private void doExport(ProtocolConfig protocolConfig,int port,List<URL> registryURLs) {
    // ... 省略 ...
    // 省略部分程式碼主要作用是處理下面這行URL中的引數,例如:protocolName -> motan,hostAddress -> 本機IP,port -> 暴露埠 等
    // map是解析出來的配置,以及一些預設配置,例如:
    /*
    "haStrategy" -> "failover"
    "module" -> "ad-common"
    "check" -> "false"
    "nodeType" -> "service"
    "version" -> "1.1.0"
    "filter" -> "cafTracing,pepperProfiler,sentinelProfiler"
    "minWorkerThread" -> "20"
    "retries" -> "1"
    "protocol" -> "motan"
    "application" -> "ad-common"
    "maxWorkerThread" -> "200"
    "shareChannel" -> "true"
    "refreshTimestamp" -> "1571821305290"
    "id" -> "ad-commonBasicServiceConfigBean"
    "export" -> "ad-commonProtocolConfigBean:8022"
    "requestTimeout" -> "30000"
    "group" -> "ad-common"
     */
    URL serviceUrl = new URL(protocolName,hostAddress,map);
    // 校驗服務是否已經存在
    // 註冊完成的服務會新增到一個set中,serviceExists方法就是檢查這個set中是否已經包含了這個服務的描述符(描述符的格式大概是host、port、protocol、version、nodeType組合的字串)
    // serviceUrl就是這個東西:motan://192.168.100.14:8022/com.coohua.ad.common.remote.api.AdCommonRPC?group=ad-common
    if (serviceExists(serviceUrl)) {
        LoggerUtil.warn(String.format("%s configService is malformed,for same service (%s) already exists ",serviceUrl.getIdentity()));
        throw new MotanFrameworkException(String.format("%s configService is malformed,serviceUrl.getIdentity()),MotanErrorMsgConstant.FRAMEWORK_INIT_ERROR);
    }

    List<URL> urls = new ArrayList<URL>();

    // injvm 協議只支援註冊到本地,其他協議可以註冊到local、remote
    if (MotanConstants.PROTOCOL_INJVM.equals(protocolConfig.getId())) {
        // ... 省略,主要關注下面的註冊中心暴露服務
    } else {
        for (URL ru : registryURLs) {
            urls.add(ru.createCopy()); // 這裡是一個淺拷貝,只是new了一個URL,具體欄位用的還是之前的引用。
        }
    }

    // registereUrls 是註冊中心的URL
    for (URL u : urls) {
        u.addParameter(URLParamType.embed.getName(),StringTools.urlEncode(serviceUrl.toFullStr()));
        registereUrls.add(u.createCopy());
    }

    ConfigHandler configHandler = ExtensionLoader.getExtensionLoader(ConfigHandler.class).getExtension(MotanConstants.DEFAULT_VALUE);

    // 到註冊中心註冊服務,urls是註冊中心的地址
    exporters.add(configHandler.export(interfaceClass,ref,urls));
}
複製程式碼

最後呼叫 configHandler.export 註冊時,經過上面的解析過程,url的parameters引數中已經包含了註冊需要用到的資訊,例如:

"path" -> "com.weibo.api.motan.registry.RegistryService"
"address" -> "192.168.103.254:2181"
"application" -> null
"name" -> "direct"
"connectTimeout" -> "3000"
"id" -> "ad-commonRegistryConfigBean"
"refreshTimestamp" -> "1571821250310"
"embed" -> "motan%3A%2F%2F192.168.100.14%3A8022%2Fcom.coohua.ad.common.remote.api.AdCommonRPC%3FhaStrategy%3Dfailover%26module%3Dad-common%26check%3Dfalse%26nodeType%3Dservice%26version%3D1.1.0%26filter%3DcafTracing%2CpepperProfiler%2CsentinelProfiler%26minWorkerThread%3D20%26retries%3D1%26protocol%3Dmotan%26application%3Dad-common%26maxWorkerThread%3D200%26shareChannel%3Dtrue%26refreshTimestamp%3D1571821305290%26id%3Dad-commonBasicServiceConfigBean%26export%3Dad-commonProtocolConfigBean%3A8022%26requestTimeout%3D30000%26group%3Dad-common%26"
"requestTimeout" -> "1000"
複製程式碼

接下來看一下 configHandler.export 做了什麼事情。

public <T> Exporter<T> export(Class<T> interfaceClass,T ref,List<URL> registryUrls) {
    // 解碼url -> motan://192.168.100.14:8022/com.coohua.ad.common.remote.api.AdCommonRPC?group=ad-common
    String serviceStr = StringTools.urlDecode(registryUrls.get(0).getParameter(URLParamType.embed.getName()));
    URL serviceUrl = URL.valueOf(serviceStr);

    // export service
    String protocolName = serviceUrl.getParameter(URLParamType.protocol.getName(),URLParamType.protocol.getValue());
    // SPI的方式拿到具體的Protocol實現,預設情況下拿到 motan 的 Protocol
    Protocol orgProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(protocolName);
    Provider<T> provider = getProvider(orgProtocol,serviceUrl,interfaceClass);

    Protocol protocol = new ProtocolFilterDecorator(orgProtocol);
    // 在這裡走Motan的filter chain,並啟動服務,filter chain通過呼叫 ProtocolFilterDecorator 的 decorateWithFilter 方法實現
    // 走完filter chain後,會呼叫 orgProtocol 的 export 方法來暴露服務,這個方法的實現在 AbstractProtocol 類中
    Exporter<T> exporter = protocol.export(provider,serviceUrl);

    // 在註冊中心中註冊服務
    register(registryUrls,serviceUrl);

    return exporter;
}
複製程式碼

AbstractProtocolexport 方法中會呼叫 createExporter 方法建立一個 Exporter 類的例項(具體來說是 DefaultRpcExporter),在這個建立過程中會呼叫 NettyEndpointFactorycreateServer 方法建立一個Server出來,並存放在exporter的server變數中。

然後呼叫 exporterinit 方法,在 init 方法中又呼叫了 doInit 方法,這個方法呼叫了 server.open(),至此,服務啟動,並監聽在本機指定的埠上。

@Override
protected boolean doInit() {
    boolean result = server.open();

    return result;
}
複製程式碼

3 服務的註冊

此時服務已經成功啟動了,但還沒註冊到註冊中心,所以還不能被發現。接下來,繼續上面的程式碼,看一下 register(registryUrls,serviceUrl); 這行程式碼幹了啥。

此時兩個引數的值分別是:

  • registryUrls: zookeeper://192.168.103.254:2181/com.weibo.api.motan.registry.RegistryService?group=default_rpc
  • serviceUrl: motan://192.168.100.14:8022/com.coohua.ad.common.remote.api.AdCommonRPC?group=ad-common

這個方法的實現如下:

private void register(List<URL> registryUrls,URL serviceUrl) {

    for (URL url : registryUrls) {
        // 根據protocol的名稱獲取具體的 RegistryFactory ,這裡以 zookeeper 為例
        RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension(url.getProtocol());
        if (registryFactory == null) {
            throw new MotanFrameworkException(new MotanErrorMsg(500,MotanErrorMsgConstant.FRAMEWORK_REGISTER_ERROR_CODE,"register error! Could not find extension for registry protocol:" + url.getProtocol()
                            + ",make sure registry module for " + url.getProtocol() + " is in classpath!"));
        }
        // 嘗試獲取url對應registry已有的例項,如果沒有,就建立一個
        // 這裡zookeeper是用ZkClient管理的
        Registry registry = registryFactory.getRegistry(url);
        // 在zk中建立Node,完成服務的註冊
        registry.register(serviceUrl);
    }
}
複製程式碼

ZK中的註冊結果:

[zk: localhost:2181(CONNECTED) 0] ls /motan/ad-common/com.coohua.ad.common.remote.api.AdCommonRPC/server
[192.168.100.14:8022]
複製程式碼

至此,服務就可以被呼叫方發現了。

最後轉載一張圖總結一下上面的過程

Motan服務註冊

圖片來源:fdx321.github.io/2017/07/23/…