手记

Dubbo Stub与Mock

前言

大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo StubMock。在前一个章节中我们介绍了 Dubbo 事件通知,以及我们也例举了常见的使用场景并且进行了源码解析来分析其实现原理,同时知道 Dubbo 中的事件通知可以在某个服务调用之前、调用之后、异常发生时触发回调事件,我们可以通过回调事件做一些额外的工作。我们在 Dubbo 服务开发过程中可能遇到我们调用的服务方并没有编写完成,那我们是不是需要等待服务提供方开发完成我们才能开始测试呢?那么在本章节我们会通过介绍 Dubbo StubMock来解决这个问题。那么什么是 StubMock ?下面就让我们快速开始吧!

1. 本地 Stub/Mock 简介

在 Dubbo 中提供的 Stub 也可称为本地存根具有类似代理模式的功能,即把我们调用的真正对象重新包装然后把包装对象提供给调用方,那么在调用真正的对象之前和之后我们可以做相应处理逻辑。同理 Mock 也可称为本地伪装和本地存根具有类似原理只是 Mock 针对发生RpcException异常或者我们强制使用 Mock 方式才去调用 Mock 的实现。从下面我们可以看出来StubMock与代理对象关系:


从图中我们可以看出的 StubMock 都是实现同一个服务的接口,它们都是通过代理对象来调用远程暴露的服务,而 Mock 紧紧是在调用失败时会触发。

Tips:MockStub 的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现 RpcException (比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错,如果用 Stub,可能就需要捕获并依赖 RpcException 类,而用 Mock 就可以不依赖 RpcException,因为它的约定就是只有出现 RpcException 时才执行。

2. 使用方式

2.1 Stub 配置方式

<dubbo:service interface="com.foo.BarService" stub="true" />

或者

<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />

Stub 的实现类:

/**
 * @author <a href="http://youngitman.tech">青年IT男</a>
 * @version v1.0.0
 * @className BookFacadeStub
 * @description
 * @JunitTest: {@link  }
 * @date 2020-11-15 23:43
 **/
public class BookFacadeStub implements BookFacade {

   //真正远程服务对象
    private BookFacade bookFacade;

    //必须提供BookFacade签名的构造函数
    public BookFacadeStub(BookFacade bookFacade){

        this.bookFacade = bookFacade;
        
    }

    @Override
    public List<Book> queryAll() {

        try {
            
            //做一些前置处理
            
            return bookFacade.queryAll();
            
        } catch (Exception e) {
            // 发生异常做一些处理
            return Lists.newArrayList();
            
        }finally {
            
            //做一些后置处理
            
        }
        
    }

}

2.2 Mock 配置方式

<dubbo:reference interface="com.foo.BarService" mock="true" />

或者

<dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />

Mock 的实现类:

public class BookFacadeMock implements BookFacade {

    /**
     *
     * 这里我们可以把服务端的方法执行时间加大 使之超时就可以触发Mock的调用
     *
     * @author liyong 
     * @date 12:18 AM 2020/11/16 
     * @param  
     * @exception 
     * @return List<Book> 
     **/
    @Override
    public List<Book> queryAll() {

        // 你可以伪造容错数据,此方法只在出现RpcException时被执行
        Book book = new Book();
        book.setDesc("default");
        book.setName("default");
        return Lists.newArrayList(book);

    }

}

3. 使用场景

在前面的 Stub/Mock 简介中我们可以知道他们都是基于对调用代理对象的包装,也就是调用代理对象前面我们可以做一些自定的操作。下面我们简单的介绍一些工作中的使用场景:

  1. 前置校验器:在我们工作中经常在调用操作前都会做一些校验,如果满足校验条件才能执行后面的逻辑。我们可以利用 Stub 的方式在调用代理对象前做一些校验工作。

  2. 日志打印:我们可以利用 Stub 在调用对象前面做一些通用的日志打印出来。

  3. 性能监控:利用 Stub 调用代理对象前后操作的时间来计算服务调用的时间。

  4. 服务降级:我们可以利用 Mock 针对出现 RpcException 异常时触发 Mock 调用机制来做一些服务降级处理工作,例如:当我们调用远程服务时远程服务不可用,这时候我们希望返回一个兜底的数据就可以使用 Mock 的方式。

5. 示例演示

下面以获取图书列表服务为例进行示例演示。项目结构如下:


以下是消费者的配置文件dubbo-consumer-xml.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       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">

    <dubbo:application name="demo-consumer" logger="log4j"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!--开启Mock/Stub模式-->
    <dubbo:reference id="bookFacade"
                     interface="com.muke.dubbocourse.mockstub.api.BookFacade"  mock="true"></dubbo:reference>

</beans>

上面的 XML 配置中我们使用mock="true"表示开启 Mock 模式。下面是服务提供者的 XML 配置dubbo-provider-xml.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       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">

    <dubbo:application name="demo-provider" metadata-type="remote"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <bean id="bookFacade" class="com.muke.dubbocourse.mockstub.provider.BookFacadeImpl"/>

    <!--暴露本地服务为Dubbo服务 ,当cluster="failover"指定集群容错模式-->
    <dubbo:service interface="com.muke.dubbocourse.mockstub.api.BookFacade" ref="bookFacade" stub="true"/>

</beans>

上面的服务提供者配置stub="true"表示开启本地存根模式。下面我们分别配置 StubMock 的实现类:

public class BookFacadeMock implements BookFacade {

    /**
     *
     * 这里我们可以把服务端的方法执行时间加大 使之超时就可以触发Mock的调用
     *
     * @author liyong
     * @date 12:18 AM 2020/11/16
     * @param
     * @exception
     * @return List<Book>
     **/
    @Override
    public List<Book> queryAll() {

        // 你可以伪造容错数据,此方法只在出现RpcException时被执行
        Book book = new Book();
        book.setDesc("default");
        book.setName("default");
        return Lists.newArrayList(book);

    }

}

这里的 BookFacadeMock 同样实现 BookFacade 接口,当调用 BookFacade 远程服务发生RpcException异常时就会调用 BookFacadeMock

public class BookFacadeStub implements BookFacade {

    private BookFacade bookFacade;

    public BookFacadeStub(BookFacade bookFacade){

        this.bookFacade = bookFacade;

    }

    @Override
    public List<Book> queryAll() {

        try {

            //做一些前置处理
            System.out.println("调用方法queryAll前置处理");
            return bookFacade.queryAll();

        } catch (Exception e) {
            // 发生异常做一些处理
            return Lists.newArrayList();

        }finally {

            //做一些后置处理
            System.out.println("调用方法queryAll后置处理");
        }

    }

}

上面的BookFacadeStub实现了接口 BookFacade ,同时提供参数类型为BookFacade的构造函数。当我们对接口配置了stub="true"我们获取的对象就是BookFacadeStub的实例。

5. 实现原理

下面我们主要分析Stub的实现其Mock的实现也是类似。 前面的简介中我们介绍了关于StubMock都是消费端对代理对象的包装,所有我们可以大胆猜测对StubMock的包装过程就在ReferenceConfig类中。首先我们看到org.apache.dubbo.config.ReferenceConfig#init方法核心代码如下:

/**
     *
     * 远程代理对象引用
     *
     * @author liyong
     * @date 5:56 PM 2020/8/26
     * @param
     * @exception
     * @return void
     **/
    public synchronized void init() {
      
        //...
      
        //本地存根check
        checkStubAndLocal(interfaceClass);
        //mock检测
        ConfigValidationUtils.checkMock(interfaceClass, this);

        //...
      
        //创建远程代理对象
        ref = createProxy(map);

        //...
    }

我们这里主要讨论Stuborg.apache.dubbo.config. AbstractInterfaceConfig#checkStubAndLocal核心内容如下:

/**
     * 本地存根检测
     */
    public void checkStubAndLocal(Class<?> interfaceClass) {
        if (ConfigUtils.isNotEmpty(local)) {
           //加载以接口名称+Local的Class
            Class<?> localClass = ConfigUtils.isDefault(local) ?
                    ReflectUtils.forName(interfaceClass.getName() + "Local") : ReflectUtils.forName(local);
          //校验该Class是否能够加载  
          verify(interfaceClass, localClass);
        }
        if (ConfigUtils.isNotEmpty(stub)) {
             //加载以接口名称+Local的Class
            Class<?> localClass = ConfigUtils.isDefault(stub) ?
                    ReflectUtils.forName(interfaceClass.getName() + "Stub") : ReflectUtils.forName(stub);
          //校验该Class是否能够加载  
          verify(interfaceClass, localClass);
        }
    }

上面的代码加载XxxLocalXxxStubClass,校验方法org.apache.dubbo.config. AbstractInterfaceConfig#verify核心内容如下:

    private void verify(Class<?> interfaceClass, Class<?> localClass) {
        //判断该localClass是否为interfaceClass的子类型
        if (!interfaceClass.isAssignableFrom(localClass)) {
            throw new IllegalStateException("The local implementation class " + localClass.getName() +
                    " not implement interface " + interfaceClass.getName());
        }

        try {
            //判断localClass构造函数是否以interfaceClass类型的参数签名
            ReflectUtils.findConstructor(localClass, interfaceClass);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException("No such constructor \"public " + localClass.getSimpleName() +
                    "(" + interfaceClass.getName() + ")\" in local implementation class " + localClass.getName());
        }
    }

上面的方法主要是校验localClass (例如:XxxLocalXxxStub)是否实现interfaceClass接口并且以interfaceClass参数签名的构造函数。接下来创建代理对象调用方法org.apache.dubbo.config.ReferenceConfig #createProxy最终会调用到org.apache.dubbo.rpc.proxy.wrapper .StubProxyFactoryWrapper #getProxy(org.apache.dubbo.rpc.Invoker<T>)方法:

public <T> T getProxy(Invoker<T> invoker) throws RpcException {
        T proxy = proxyFactory.getProxy(invoker);
        if (GenericService.class != invoker.getInterface()) {
            URL url = invoker.getUrl();
            //获取本地存根配置
            String stub = url.getParameter(STUB_KEY, url.getParameter(LOCAL_KEY));
            if (ConfigUtils.isNotEmpty(stub)) {
                Class<?> serviceType = invoker.getInterface();
                if (ConfigUtils.isDefault(stub)) {
                    //尝试获取stub如果未查找到默认使用XxxLocal
                    if (url.hasParameter(STUB_KEY)) {
                        stub = serviceType.getName() + "Stub";
                    } else {
                        stub = serviceType.getName() + "Local";
                    }
                }
                try {
                    //加载XxxStub或XxxLocal
                    Class<?> stubClass = ReflectUtils.forName(stub);
                    if (!serviceType.isAssignableFrom(stubClass)) {
                        throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + serviceType.getName());
                    }
                    try {
                        //获取参数签名为接口类型的构造函数
                        Constructor<?> constructor = ReflectUtils.findConstructor(stubClass, serviceType);
                        //包装远程代理对象,在调用前先执行本地逻辑(*Stub、*Local)
                        proxy = (T) constructor.newInstance(new Object[]{proxy});
                        //暴露包装后的对象
                        URLBuilder urlBuilder = URLBuilder.from(url);
                        if (url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT)) {
                           //..
                            try {
                                export(proxy, (Class) invoker.getInterface(), urlBuilder.build());
                            } catch (Exception e) {
                                LOGGER.error("export a stub service error.", e);
                            }
                        }
                    } catch (NoSuchMethodException e) {
                        //..
                    }
                } catch (Throwable t) {
                    //..
                }
            }
        }
        return proxy;
    }

从上面的代码中可以看出远程调用代理对象传递给XxxStubXxxLocal形式的实例构造函数,并且把XxxStubXxxLocal返回给ReferenceConfig.ref ,这样我们使用 Dubbo 服务是获取的就是XxxStubXxxLocal的实例对象。

6. 小结

在本小节中我们主要学习了 Dubbo 中 StubMock以及两种模式的使用方式。同时也分析了 StubMock 实现的原理,其本质上是通过包装对引用代理对象的调用,其中 MockStub 实现的一个子集针对调用抛出RpcException异常时被调用。

本节课程的重点如下:

  1. 理解 Dubbo StubMock

  2. 了解了两种模式使用方式

  3. 了解 StubMock 实现原理

  4. 了解 StubMock 使用场景

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

0人推荐
随时随地看视频
慕课网APP