京东零售mockRpc实践

背景介绍
现今互联网应用为实现快速响应、探索、挖掘、引领用户的需求多数已使用微服务架构。京东零售也使用了微服务架构,微服务有很多优势不仅可以提升迭代速度,而且可以实现动态扩容缩容提升资源的利用率,特别是应对6.18、11.11等大促流量压力效果明显。但在实际开发中因为解耦不彻底、网络隔离等问题导致效率降低。mockRpc的目的是实现前中台彻底解耦、消除网络隔离提升开发效率。下图是京东零售的大体架构:


如上所示:每个服务都是相对独立的实体职责单一、可独立部署、可小团队维护,很好的支撑快速迭代并且扩容、缩容控制更灵活。但也存在如下问题:
1.前台服务的逻辑分支选择依赖中台返回的数据时,因中台数据不好控制而导致前台逻辑难以全覆盖。
2.前台服务本地调试时因网络隔离无法与中台服务建立连接,导致本地难以自测。
3.前中台同步开发时,若中台只提供了接口还未提供数据时,则前台难以提前自测、部署,间接影响客户端的开发进度。
为了解决上述问题,提升开发效率。有了mockRpc接口的想法,实现对中台数据的mock并对业务逻辑代码零侵入。
mockRpc平台
mockRpc平台在整个架构中的位置如下图所示:


如上图所示,在前台服务中引入了mockClient,由其hook调用的JSF接口并携带参数去请求mockServer;由mockServer决定是否请求中台接口以及返回mock数据;当mockServer告之mockClient开关关闭时,则会通过JSF接口去请求中台数据,当mockServer告之mockClient开关打开并返回了mock数据后,则前台服务拿mockServer返回的数据去做逻辑处理而不在调用JSF接口请求数据,从而实现了开发过程中的前、中台完全解耦。接下来将详细解释mockClient如何实现的hook JSF接口以及如何实现代码零侵入。
原理
Note:
1. JSF是一种RPC通信协议,实现原理和Dubbo类似,下文xml中的标签也可参考来理解。
2. 该文中前台服务、中台服务都是基于Spring的,接下来实现原理也是基于Spring来解释。
1.因前中台是基于Spring开发的,前台服务使用JSF时需要注入中台提供的接口,具体如下:

     <jsf:consumer id="qa*Service" interface="com.jd.*.*.*.QService"

          protocol="jsf" alias="TEST" timeout="2000" retries="1">

    <jsf:parameter key="token" value="f1*******c" hide="true"/>

  

如上所示,注入了com.jd.*.*.*.QService接口的代理类,当使用其提供的服务时根据id=”qa*Service”去Spring容器中找到该代理类调用其方法通过jsf协议获取中台数据。我们的目的就是Hook住发往中台的请求并返回我们自己的mock数据,但jsf协议是基于tcp的而且对数据进行了序列化、加密等操作,直接截获的方式很难实现。最后决定注入自己实现的com.jd.*.*.*.QService代理类到Spring容器中并且id设置为”qa*Service”并把jsf注入的代理类id修改为”qa*Service_jsf”,从而实现对服务接口的hook。
2.注入自己实现的代理类,xml配置如下:

<jsf:consumer id="qa*Service_jsf" interface="com.jd.*.*.*.QService"

  protocol="jsf" alias="TEST" timeout="2000" retries="1">





如上所示,通过mdc标签注入了com.jd.*.*.*.QService的代理类,并设置id=”qa*Service”,因为业务代码中获取代理类时是通过该id查找的,我们将自己定义的代理类以该id注入后,就相当于实现了对jsf注入代理类的hook,业务调用com.jd.*.*.*.QService 提供的服务时,找到的将是我们注入的代理类,从而实现对业务代码的零侵入。
参数解释:
mdc: 扩展的Spring schema,用于注入interface=”com.jd.*.*.*.QService”对应的代理类,该代理类中实现请求mock数据以及是否通过JSF接口调用真正中台数据的逻辑;

token:
在mockServer平台创建mock数据时生成的唯一标识,用于区分不同用户创建的同一接口的mock数据。
 

3.通过mdc标签注入代理类由mockClient实现,在调用服务接口时,将由该代理类携带token、类名(com.jd.*.*.*.QService)以及服务接口名通过http协议去请求mockServer。
4.mockServer先判断开关是否打开,若打开则返回对应的mock数据,若关闭也通知mockClient开关已关闭。
5.mockClient收到mockServer返回的数据后,先判断开关,若打开则将数据返回给服务调用者,若关闭则根据id=”qa*Service_jsf”去Spring容器中查找jsf注入的代理类,并调用其接口获取中台数据。
关于代码零侵入:
1. 因mdc标签注入的代理类时,使用的id和业务代码中一一对应,所以使用mdc标签注入的代理类无需更改业务代码。
2. 针对线上、预发、开发环境可配置对应的xml文件且相互无影响,我们只需要预发或者开发环境配置mdc标签,对线上零影响。
mockClient关键代码解析
mockClient主要实现两个重要功能:
1. 扩展Spring schema(mdc)实现注入自定义代理类的功能
2. 自定义代理类中实现请求mock数据以及在mock开关关闭后,去调用JSF接口获取中台数据的功能
1.扩展Spring schema 需要的几个文件,具体扩展方式可参照官方文档。


2.其中MdcNameSpaceHander类,实现将xml元素与实体类对应起来,如下所示将”consumer”与ConsumerBean.class关联。

public class MdcNamespaceHandler extends NamespaceHandlerSupport {

       public void init() {

          registerBeanDefinitionParser("consumer", new MdcBeanDefinitionParser(ConsumerBean.class));

       }

    }

3.ConsumerBean关键实现解析:

 public class ConsumerBean 

  implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {

/**

* 返回自定义的代理类

*/

public T getObject() {

Object bean = null;

try {

      //"_jsf"与xml文件中jsf标签注入代理类的id后缀一致

bean = applicationContext.getBean(id + "_jsf");

}catch (Exception e){

log.error("mock_rpc_client get jsf bean exception:{}",e);

}

    Object proxyInstance = Proxy.newProxyInstance(

          ClassLoaderUtils.getClassLoader(getInterfaceClass()), 

          new Class[]{getInterfaceClass()}, 

          new InvokerInvocationHandler(getInterface(), getToken(), bean, getAddress()));

return (T) proxyInstance;

}

/**

* 获取上下文,在getObject方法中将使用该上下文去容器中获取jsf注入的代理类

*/

public void setApplicationContext(ApplicationContext applicationContext) 

    throws BeansException {

this.applicationContext = applicationContext;

}

}

4.InvokerInvocationHandler关键实现解析:

 public class InvokerInvocationHandler implements InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 获得方法名

        String methodName = method.getName();

        // 获得参数类型

        Class[] parameterTypes = method.getParameterTypes();

        Param param = new Param();

        param.setClassName(className);

        param.setMethodName(methodName);

        param.setToken(token);

        //请求mockServer获取mock数据

        String mockResponse = HttpClientUtil.sendHttpRequest(address, param);

        log.info("mock_rpc_client mockResponse:{} ", mockResponse);

        if (StringUtils.isNotBlank(mockResponse)) {

            try {

                JSONObject jsonObject = JSON.parseObject(mockResponse);

                if (jsonObject != null) {

                    String code = jsonObject.getString("code");

                    String resultCode = jsonObject.getString("resultCode");

                    String mockData = jsonObject.getString("mockData");

                    if ("0".equals(code) && "0".equals(resultCode)) {

                        if (StringUtils.isNotBlank(mockData)) {

                            Type genericReturnType = method.getGenericReturnType();

                            // 反序列化为方法返回值类型的实例

                            return JSON.parseObject(mockData, genericReturnType);

                        }

                    }

                }

            } catch (Exception e) {

                log.error("mock_rpc_client exception:{} ", e);

            }

        }

        //当mockServer返回开关关闭或未返回数据时,使用jsf标签注入的代理类获取中台数据

        if (bean != null) {

            try {

                // 通过方法名找到jsf标签注入代理类的具体方法

                Method realMethod = bean.getClass().getMethod(methodName, 

                                                              parameterTypes);

                // 通过反射调用jsf接口获取中台服务

                return realMethod.invoke(bean, args);

            }catch (Exception e){

                log.error("mock_rpc_client mock_real_invoke_exception:{} ", e);

            }

            return null;

        } else {

            return null;

        }

    }

}


mockServer简要说明
mockServer模块提供mock数据配置、开关控制等功能,主要有两个部分:管理后台和getMockDate接口,分别实现mock数据录入、开关控制以及供mockClient获取开关策略或mock数据。
后台主要界面:
1.接口列表:


2.方法列表:


3.mock数据列表:


4.mock数据:


getMockData接口流程图如下:


总结
JSF平台本身也实现了mock功能,但因网络隔离、使用门槛等原因难以在实际开发中推广运用,而mockRpc不存在上述限制。经验证,部署mockRpc平台后开发调试效率得到显著提升,首先和中台完全解耦;其次验证自身代码逻辑再也不用通过硬编码模拟,实现零侵入。另外该思想并不只限于对JSF数据的mock,其他RPC框架如dubbo,也可以利用该思想去hook服务接口,且可以根据自己的业务场景在mockServer进行更多的策略控制。