Dubbo學習筆記-泛化實現進行mock


Dubbo RPC介紹

1. 什么是Dubbo,我們正常是怎么使用的?

Apache Dubbo™ 是一款高性能Java RPC框架.其中與Alibaba Dubbo的區別主要在於阿里開發的2.6.X且不再維護,Apache開發的2.7.X新增了元數據中心 MetaData 和配置中心 Conf-center 這兩個功能。元數據信息包括服務接口,及接口的方法信息。這些信息將被用於服務mock,服務測試。

我們平常是在實例上增加注解@Service暴露服務和使用@Reference來完成自動引用實例,樣例查看http://dubbo.apache.org/zh-cn/docs/user/configuration/annotation.html

2. 注冊一個RPC服務需要什么參數

官方給出的快速啟動樣例是這樣的: http://dubbo.apache.org/zh-cn/docs/user/quick-start.html 這個樣例是使用XML,我們並不常用,但可以比較清楚地觀察到注冊一個RPC服務需要什么參數。注意下面這個 provider.xml,我增加了一些改動用以說明

<?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:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
 
    <!-- 提供方應用信息,用於計算依賴關系 application.name為應用名,一個接口下可以有多個應用來提供 -->
    <dubbo:application name="hello-world-app"  />
 
    <!-- 使用multicast廣播注冊中心暴露服務地址  ip:port地址可以為多個,用,分割即可。 zookeeper://101.201.232.80:2181?backup=10.20.153.11:2181,10.20.153.12:2181"-->
    <dubbo:registry address="zookeeper://101.201.232.80:2181" />
 
    <!-- 用dubbo協議在20880端口暴露服務 -->
    <dubbo:protocol name="dubbo" port="20880" />
 
    <!-- 聲明需要暴露的服務接口 
	     interface為接口全類名,
	     ref服務對象實現引用,與下面的bean.id對應 
	     version=服務版本,升級接口如果不兼容之前接口,就需要升級版本號 
	     group=服務分組,當一個接口有多個實現,可以用分組區分
	-->
    <dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService" version="1.0.0" group="dubbo"/>
 
    <!-- 聲明實現類,使遠程RPC像本地bean一樣實現服務 id=service.ref class=實現類的全類名 -->
    <bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl" />
</beans>

其實區別定位一個RPC服務需要的條件一共有四個:

  1. application.name
  2. service.interface。注意:如果這個接口不存在,就無法注冊並暴露服務,而我們mock的目標就是個假的服務出來。
  3. service.version
  4. service.group

此處注意:服務的元數據是根據service.interface反射得到的,我們為了和2.7.X兼容,就必須自己造個假的元數據上去

3. 我們的Mock實現思路

Dubbo與mock相關的提供了兩種api:本地偽裝Mock泛化實現

本地偽裝Mock,XML對應為<dubbo:reference interface="com.foo.BarService" mock="true" .../>用於服務降級,當服務提供方全部掛掉后,客戶端不拋出異常,而是通過 Mock 數據返回授權失敗。

大白話就是參考AOP的after-throw,原服務拋出異常后,觸發本地mock進行服務降級。try {//原服務} catch (Exception e) {//執行mock實例}。這個坑了我很多時間,因為mock="true"太具有迷惑性,我原本使用這個mock約定實現mock平台功能,使用force模式強制調用mock平台上的服務,但是一來會覆蓋原先的本地mock;二來不符合無侵入原則。最終放棄。貼出參考用法如下

<dubbo:reference id="demoService" check="false" interface="com.foo.BarService">
	<!-- force表示強制走mock; return fake是mock執行的語句 -->
    <dubbo:parameter key="sayHello.mock" value="force:return fake"/>
</dubbo:reference>

泛化實現,XML對應為<dubbo:reference interface="com.foo.BarService" generic="true" .../>,用於服務器端沒有API接口及模型類元的情況,參數及返回值中的所有POJO均用Map表示,通常用於框架集成,比如:實現一個通用的遠程服務Mock框架,可通過實現GenericService接口處理所有服務請求。

這個就是我們最終采用的實現思路。通過指定一個通用服務接口GenericService的實現,完成服務的暴露。

// 這個就是 提供者dubbo 和標簽一一對應,該類很重,因為包括了與注冊中心的鏈接等等,需要自行緩存處理
ServiceConfig<GenericService> service = new ServiceConfig<GenericService>;
// 配置service里注冊中心的屬性,需要指定address,例"zookeeper://101.201.232.80:2181"。
service.setRegistry(new RegistryConfig("zookeeper://101.201.232.80:2181"));
// 配置應用信息,這里我們將應用名統一表示為luckymock。
ApplicationConfig application = new ApplicationConfig();
application.setDefault(true);
application.setName("luckymock");
service.setApplication(application);
// 指定通信協議和本地通訊端口
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(20880);
service.setProtocol(protocol);
// 說明是否是通用服務 並綁定通用服務接口的實現:GenericServiceImpl
service.setGeneric("true");
GenericService genericService = new GenericServiceImpl();
service.setRef(genericService);
// 配置這個服務特有的的東西
service.setGroup("dubbo");
service.setVersion("1.0.0");
service.setInterface("test")
// 對外暴露這個服務
service.export();
// 由於上面提到的元數據問題,我們手動去提交了一遍元數據。
// 這方面資料極少,我是通過源碼ZookeeperMetadataReport的storeMetadata方法追溯到MetadataReportService的publishProviderf方法,所有用法都參考這個方法
// 使用下面這個函數提交服務提供者的元數據,不一定是zookeeper作注冊中心
metadataReport.storeProviderMetadata(metadataIdentifier, fullServiceDefinition);

關於元數據,MetadataIdentifier 該類主要是用於配置zookeeper上元數據節點路徑和節點名稱

例:/dubbo/metadata/服務名(接口全類名)/1.0.0(版本)/dubbo(分組)/provide(side)/demo-provide(應用名)

FullServiceDefinition 該對象進行gson轉換后的json串,即為元數據節點內容

  • parameters:服務的屬性,包括version,group等等
  • canonicalName:接口全類名
  • codeSource:源代碼
  • methods:方法
  • types: 所有方法入參出參的類型

樣例

GenericServiceImpl 這個impl就是xml中service.ref所需要的類

import org.apache.dubbo.rpc.service.GenericException;
import org.apache.dubbo.rpc.service.GenericService;

/**
 * @Description: 服務端實現 GenericService
 * @Author: quanhuan.huang@luckincoffee.com
 * @Date: 2019/12/19 15:01
 */
public class GenericServiceImpl implements GenericService {
    @Override
    public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
       // 這里只有method方法名可以進行區別,所以該類可能需要動態編譯
       if (method.equals("hi")) {
            return "hi, " + args[0];
        } else if (method.equals("hello")) {
            return "hello, " + args[0];
        } else if (method.equals("sayHello")) {
            return "say:hello, " + args[0];
        }
        return "未找到該方法,無法mock";
    }
}

數據結構:DubboRpcDTO

public class DubboRpcDTO {
    /**
     * 服務接口的全類名
     */
    private String interfaceName;
    /**
     * 分組
     */
    private String group;
    /**
     * 版本
     */
    private String version;
    /**
     * 方法列表
     */
    private List<DubboMethodDTO> methodList;
}

數據結構:DubboMethodDTO

public class DubboMethodDTO {
    /**
     * 方法名
     */
    private String methodName;
    /**
     * 方法參數
     */
    private String[] arguments;
    /**
     * 返回類型
     */
    private String returnType;
}

服務實現

@Service
public class DubboProvideService {
    private static Logger logger = LoggerFactory.getLogger(DubboProvideService.class);
    private MetadataReportFactory metadataReportFactory = ExtensionLoader.getExtensionLoader(MetadataReportFactory.class).getAdaptiveExtension();

    /**
     * 該類很重 自行緩存
     */
    private static ServiceConfig<GenericService> service;

    /**
     * 提供rpc服務
     *
     * @return 是否完成
     */
    Boolean rpcMockProvide(DubboRpcDTO rpcDTO) throws ClassNotFoundException {

        // 注冊並對外暴露服務
        ServiceConfig<GenericService> service = getService();
        service.setGroup(rpcDTO.getGroup());
        service.setVersion(rpcDTO.getVersion());
        service.setInterface(rpcDTO.getInterfaceName());
        // 指向自己實現的通用類實例,需要動態化
        GenericService genericService = new GenericServiceImpl();
        service.setGeneric("true");
        service.setRef(genericService);
        service.export();

        // 注冊數據源
        FullServiceDefinition fullServiceDefinition = new FullServiceDefinition();
        TypeDefinitionBuilder builder = new TypeDefinitionBuilder();
        List<TypeDefinition> typeDefinitions = new LinkedList<>();
        List<MethodDefinition> methodDefinitions = new LinkedList<>();
        for (DubboMethodDTO method : rpcDTO.getMethodList()) {
            // 記錄方法
            MethodDefinition methodDefinition = new MethodDefinition();
            methodDefinition.setName(method.getMethodName());
            methodDefinition.setParameterTypes(method.getArguments());
            methodDefinition.setReturnType(method.getReturnType());
            methodDefinitions.add(methodDefinition);
            // 記錄所有入參的type
            for (String argument : method.getArguments()) {
                TypeDefinition td = builder.build(Class.forName(argument), Class.forName(argument));
                typeDefinitions.add(td);
            }
            // 返回值的type也需要記錄
            typeDefinitions.add(builder.build(Class.forName(method.getReturnType()), Class.forName(method.getReturnType())));
        }
        // 拼接服務內容
        Map<String, String> parameters = new HashMap<>(16);
        parameters.put("side", "provider");
        parameters.put("release", "2.7.3");
        parameters.put("methods", "*");
        parameters.put("deprecated", "false");
        parameters.put("dubbo", "2.0.2");
        parameters.put("interface", rpcDTO.getInterfaceName());
        parameters.put("version", rpcDTO.getVersion());
        parameters.put("generic", "true");
        parameters.put("application", "luckymock");
        parameters.put("dynamic", "true");
        parameters.put("register", "true");
        parameters.put("group", rpcDTO.getGroup());
        parameters.put("anyhost", "true");
        fullServiceDefinition.setParameters(parameters);
        fullServiceDefinition.setCodeSource(ClassUtils.getCodeSource(this.getClass()));
        fullServiceDefinition.setCanonicalName(rpcDTO.getInterfaceName());
        fullServiceDefinition.setTypes(typeDefinitions);
        fullServiceDefinition.setMethods(methodDefinitions);
        // 拼接服務描述
        MetadataIdentifier metadataIdentifier = new MetadataIdentifier();
        metadataIdentifier.setServiceInterface(rpcDTO.getInterfaceName());
        metadataIdentifier.setVersion(rpcDTO.getVersion());
        metadataIdentifier.setGroup(rpcDTO.getGroup());
        metadataIdentifier.setSide("provider");
        // 應用名統一為luckymock
        metadataIdentifier.setApplication("luckyMock");
        // 寫元數據中心
        MetadataReport metadataReport = metadataReportFactory.getMetadataReport(URL.valueOf("zookeeper://101.201.232.80:2181"));
        metadataReport.storeProviderMetadata(metadataIdentifier, fullServiceDefinition);
        logger.info("注冊RPC服務成功:{}", rpcDTO.getInterfaceName());
        return true;

    }

    /**
     * 該類很重,緩存
     *
     * @return @Service的信息
     */
    private static ServiceConfig<GenericService> getService() {
        if (null == service) {
            service = new ServiceConfig<>();
            // 注冊中心配置
            RegistryConfig registryConfig = new RegistryConfig();
            registryConfig.setAddress("zookeeper://101.201.232.80:2181");
            service.setRegistry(registryConfig);
            // 應用配置
            ApplicationConfig application = new ApplicationConfig();
            application.setName("luckymock");
            service.setApplication(application);
            // 協議配置
            ProtocolConfig protocol = new ProtocolConfig();
            protocol.setName("dubbo");
            protocol.setPort(20880);
            service.setProtocol(protocol);
        }
        return service;
    }
}

經測試,下面這個test可以完成provider.xml一樣的功能。

@Test
    public void demoService() {
        // 現在用泛化實現,實現com.lucky.demo.api.DemoService
        // 如果想要保證消費者正常調用,消費者處 InterfaceName這個類必須存在。當然,我們作為服務提供者不需要真的有這個類
        // 什么情況消費者也沒有這個接口呢?rpc測試平台,dubbo-admin已經做了。
        DubboRpcDTO dubboRpcDTO = new DubboRpcDTO();
        dubboRpcDTO.setGroup("dubbo");
        dubboRpcDTO.setVersion("1.0.0");
        dubboRpcDTO.setInterfaceName("com.lucky.demo.api.DemoService");
        List<DubboMethodDTO> methodList = new LinkedList<>();
        DubboMethodDTO dubboMethodDTO = new DubboMethodDTO();
        dubboMethodDTO.setMethodName("sayHello");
        dubboMethodDTO.setReturnType("java.lang.String");
        dubboMethodDTO.setArguments(new String[]{"java.lang.String"});
        methodList.add(dubboMethodDTO);
        dubboRpcDTO.setMethodList(methodList);

        try {
            dubboProvideService.rpcMockProvide(dubboRpcDTO);
            System.out.println("dubbo service started,enter any keys stop");
            Scanner scanner = new Scanner(System.in);
            scanner.next();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM