京东零售mockRpc实践
Posted 京东零售技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了京东零售mockRpc实践相关的知识,希望对你有一定的参考价值。
现今互联网应用为实现快速响应、探索、挖掘、引领用户的需求多数已使用微服务架构。京东零售也使用了微服务架构,微服务有很多优势不仅可以提升迭代速度,而且可以实现动态扩容缩容提升资源的利用率,特别是应对6.18、11.11等大促流量压力效果明显。但在实际开发中因为解耦不彻底、网络隔离等问题导致效率降低。mockRpc的目的是实现前中台彻底解耦、消除网络隔离提升开发效率。下图是京东零售的大体架构:
如上所示:每个服务都是相对独立的实体职责单一、可独立部署、可小团队维护,很好的支撑快速迭代并且扩容、缩容控制更灵活。但也存在如下问题:
1.前台服务的逻辑分支选择依赖中台返回的数据时,因中台数据不好控制而导致前台逻辑难以全覆盖。
2.前台服务本地调试时因网络隔离无法与中台服务建立连接,导致本地难以自测。
3.前中台同步开发时,若中台只提供了接口还未提供数据时,则前台难以提前自测、部署,间接影响客户端的开发进度。
为了解决上述问题,提升开发效率。有了mockRpc接口的想法,实现对中台数据的mock并对业务逻辑代码零侵入。
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主要实现两个重要功能:
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<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模块提供mock数据配置、开关控制等功能,主要有两个部分:管理后台和getMockDate接口,分别实现mock数据录入、开关控制以及供mockClient获取开关策略或mock数据。
后台主要界面:
1.接口列表:
2.方法列表:
3.mock数据列表:
4.mock数据:
getMockData接口流程图如下:
JSF平台本身也实现了mock功能,但因网络隔离、使用门槛等原因难以在实际开发中推广运用,而mockRpc不存在上述限制。经验证,部署mockRpc平台后开发调试效率得到显著提升,首先和中台完全解耦;其次验证自身代码逻辑再也不用通过硬编码模拟,实现零侵入。另外该思想并不只限于对JSF数据的mock,其他RPC框架如dubbo,也可以利用该思想去hook服务接口,且可以根据自己的业务场景在mockServer进行更多的策略控制。
以上是关于京东零售mockRpc实践的主要内容,如果未能解决你的问题,请参考以下文章