京东零售mockRpc实践

Posted 京东零售技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了京东零售mockRpc实践相关的知识,希望对你有一定的参考价值。


背景介绍


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



如上所示:每个服务都是相对独立的实体职责单一、可独立部署、可小团队维护,很好的支撑快速迭代并且扩容、缩容控制更灵活。但也存在如下问题:

1.前台服务的逻辑分支选择依赖中台返回的数据时,因中台数据不好控制而导致前台逻辑难以全覆盖。

2.前台服务本地调试时因网络隔离无法与中台服务建立连接,导致本地难以自测。

3.前中台同步开发时,若中台只提供了接口还未提供数据时,则前台难以提前自测、部署,间接影响客户端的开发进度。

为了解决上述问题,提升开发效率。有了mockRpc接口的想法,实现对中台数据的mock并对业务逻辑代码零侵入。


mockRpc平台


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中的<jsf:consumer />标签也可参考<dubbo:consumer />来理解。

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"/>  </jsf:consumer>


如上所示,注入了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"><jsf:parameter key="token" value="f1*******c" hide="true"/></jsf:consumer><mdc:consumer id="qa*Service" interface="com.jd.*.*.*.QService" token="f1*******c" />


如上所示,通过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 需要的几个文件,具体扩展方式可参照官方文档。


京东零售mockRpc实践

  

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<T>  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.接口列表:


京东零售mockRpc实践


2.方法列表:


京东零售mockRpc实践


3.mock数据列表:


京东零售mockRpc实践


4.mock数据:



getMockData接口流程图如下:



总结


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

以上是关于京东零售mockRpc实践的主要内容,如果未能解决你的问题,请参考以下文章

京东零售数据仓库演进之路

京东实时数据产品应用实践

京东千万并发 API 网关实践之路!

京东短网址高可用提升最佳实践

京东平台研发:领域驱动设计(DDD)实践总结

零售业小程序行业解决方案