1、背景:基于测试需求 测试项目时对于第三方rpc接口需要动态上下线和mock报文
2、方法:
1. 在消费者方使用SPI拓展,自定义一个拓展Cluster实现拦截rpc请求做到mock
2. 消费者无代码入侵,全在提供方实现mock
方法1:
参考地址:GitHub - dsc-cmt/dubbo-easy-mock: 针对Dubbo接口的Mock解决方案
方法2:
如果要实现动态上线一个rpc接口就要使用ServiceConfig类的export 方法
// 服务实现
HellowService hellowService = new HellowServiceImpl();
// 应用配置
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("test");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://192.168.41.77:2181");
// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(12345);
protocol.setThreads(200);
// 服务提供者暴露服务配置
ServiceConfig<HellowService> service = new ServiceConfig<HellowService>(); // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
service.setApplication(applicationConfig);
service.setRegistry(registry); // 多个注册中心可以用setRegistries()
service.setProtocol(protocol); // 多个协议可以用setProtocols()
service.setInterface(HellowService.class);
service.setRef(hellowService);
service.setVersion("1.0.0");
// 暴露及注册服务
service.export();
System.in.read()// press any key to exit
如图在service.export() 中
public synchronized void export() {
this.checkAndUpdateSubConfigs();
if (this.shouldExport()) {
if (this.shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, (long)this.getDelay(), TimeUnit.MILLISECONDS);
} else {
this.doExport();
}
}
}
public void checkAndUpdateSubConfigs() {
this.completeCompoundConfigs();
this.startConfigCenter();
this.checkDefault();
this.checkProtocol();
this.checkApplication();
if (!this.isOnlyInJvm()) {
this.checkRegistry();
}
this.refresh();
this.checkMetadataReport();
if (StringUtils.isEmpty(this.interfaceName)) {
throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
} else {
// 如果要实现热部署发布rpc 接口就需要修改此处代码 因为dubbo使用的加载器与自定义加载器不是一个
if (this.ref instanceof GenericService) {
this.interfaceClass = GenericService.class;
if (StringUtils.isEmpty(this.generic)) {
this.generic = Boolean.TRUE.toString();
}
} else {
try {
this.interfaceClass = Class.forName(this.interfaceName, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException var5) {
throw new IllegalStateException(var5.getMessage(), var5);
}
this.checkInterfaceAndMethods(this.interfaceClass, this.methods);
this.checkRef();
this.generic = Boolean.FALSE.toString();
}
Class stubClass;
if (this.local != null) {
if (Boolean.TRUE.toString().equals(this.local)) {
this.local = this.interfaceName + "Local";
}
try {
stubClass = ClassUtils.forNameWithThreadContextClassLoader(this.local);
} catch (ClassNotFoundException var4) {
throw new IllegalStateException(var4.getMessage(), var4);
}
if (!this.interfaceClass.isAssignableFrom(stubClass)) {
throw new IllegalStateException("The local implementation class " + stubClass.getName() + " not implement interface " + this.interfaceName);
}
}
if (this.stub != null) {
if (Boolean.TRUE.toString().equals(this.stub)) {
this.stub = this.interfaceName + "Stub";
}
try {
stubClass = ClassUtils.forNameWithThreadContextClassLoader(this.stub);
} catch (ClassNotFoundException var3) {
throw new IllegalStateException(var3.getMessage(), var3);
}
if (!this.interfaceClass.isAssignableFrom(stubClass)) {
throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + this.interfaceName);
}
}
this.checkStubAndLocal(this.interfaceClass);
this.checkMock(this.interfaceClass);
}
}
上图中 如果要实现动态发布必须修改类加载器 无法实现除非项目启动时另起线程用自定义加载器加载全部类。
那么如果ref 为 GenericService 泛型发布就不会校验接口与实现类实现关系就可以直接发布,
但是有一点泛型发布的服务如果消费者没有设置支持泛型是会找不到方法的。这儿科普下消费者注册接口会去获取提供者方的发布服务时保存的Invoker,源码在DubboProtocol 类getInvoker() 中
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
boolean isCallBackServiceInvoke = false;
boolean isStubServiceInvoke = false;
int port = channel.getLocalAddress().getPort();
String path = (String)inv.getAttachments().get("path");
isStubServiceInvoke = Boolean.TRUE.toString().equals(inv.getAttachments().get("dubbo.stub.event"));
if (isStubServiceInvoke) {
port = channel.getRemoteAddress().getPort();
}
isCallBackServiceInvoke = this.isClientSide(channel) && !isStubServiceInvoke;
if (isCallBackServiceInvoke) {
path = path + "." + (String)inv.getAttachments().get("callback.service.instid");
inv.getAttachments().put("_isCallBackServiceInvoke", Boolean.TRUE.toString());
}
String serviceKey = serviceKey(port, path, (String)inv.getAttachments().get("version"), (String)inv.getAttachments().get("group"));
DubboExporter<?> exporter = (DubboExporter)this.exporterMap.get(serviceKey);
if (exporter == null) {
throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + this.exporterMap.keySet() + ", may be version or group mismatch , channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);
} else {
return exporter.getInvoker();
}
}
ServiceConfig export 最终会走exportLocal 走到 DubboProtocol 的 export() 并且保存服务信息在缓存中 重写DubboProtocol 后需要缓存DubboExporter<T> exporter = new DubboExporter(invoker, key, this.exporterMap); 不知道为啥重写后会有两个DubboProtocol 对象 记得缓存 exporter 在其他类并且在getInvoker 判断找不到 去这个缓存对象找。
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
String key = serviceKey(url);
DubboExporter<T> exporter = new DubboExporter(invoker, key, this.exporterMap);
this.exporterMap.put(key, exporter);
Boolean isStubSupportEvent = url.getParameter("dubbo.stub.event", false);
Boolean isCallbackservice = url.getParameter("is_callback_service", false);
if (isStubSupportEvent && !isCallbackservice) {
String stubServiceMethods = url.getParameter("dubbo.stub.event.methods");
if (stubServiceMethods != null && stubServiceMethods.length() != 0) {
this.stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
} else if (this.logger.isWarnEnabled()) {
this.logger.warn(new IllegalStateException("consumer [" + url.getParameter("interface") + "], has set stubproxy support event ,but no stub methods founded."));
}
}
this.openServer(url);
this.optimizeSerialization(url);
return exporter;
}
消费者就会在 缓存中拿到暴露的服务的Invoker 那如果用泛型暴露的服务 拿到的Invoker 中的参数methods 会是 默认* 可看上面 serviceConfig 类代码 这样虽然服务暴露了但是消费者不修改接口支持泛型是会报错找不到方法的,那我们只有改serviceConfig 代码
if (ProtocolUtils.isGeneric(this.generic)) {
map.put("generic", this.generic);
map.put("methods", "*"); //泛化暴露服务时不让他设置* 改为设置自己的方法名
} else {
host = Version.getVersion(this.interfaceClass, this.version);
if (host != null && host.length() > 0) {
map.put("revision", host);
}
String[] methods = Wrapper.getWrapper(this.interfaceClass).getMethodNames();
if (methods.length == 0) {
logger.warn("No method found in service interface " + this.interfaceClass.getName());
map.put("methods", "*");
} else {
map.put("methods", StringUtils.join(new HashSet(Arrays.asList(methods)), ","));
}
}
如图设置方法名 可以在暴露服务时 serviceConfig.setMethods 插入 然后在源码中使用 this.methods 插入map 中。
这样消费者可以不改代码就能调用到rpc提供者。
如何mock rpc 请求呢?
如图
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
if (!(message instanceof Invocation)) {
throw new RemotingException(channel, "Unsupported request: " + (message == null ? null : message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
} else {
Invocation inv = (Invocation)message;
Invoker<?> invoker = DubboProtocol.this.getInvoker(channel, inv);
if (Boolean.TRUE.toString().equals(inv.getAttachments().get("_isCallBackServiceInvoke"))) {
String methodsStr = (String)invoker.getUrl().getParameters().get("methods");
boolean hasMethod = false;
if (methodsStr != null && methodsStr.contains(",")) {
String[] methods = methodsStr.split(",");
String[] var8 = methods;
int var9 = methods.length;
for(int var10 = 0; var10 < var9; ++var10) {
String method = var8[var10];
if (inv.getMethodName().equals(method)) {
hasMethod = true;
break;
}
}
} else {
hasMethod = inv.getMethodName().equals(methodsStr);
}
if (!hasMethod) {
DubboProtocol.this.logger.warn(new IllegalStateException("The methodName " + inv.getMethodName() + " not found in callback service interface ,invoke will be ignored. please update the api interface. url is:" + invoker.getUrl()) + " ,invocation is :" + inv);
return null;
}
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
Result result = invoker.invoke(inv);
return result.completionFuture().thenApply(Function.identity());
}
}
重写 DubboProtocol ExchangeHandler 对象中reply方法便是提供者接受到消费者请求的入口 在这里根据 Invocation 对象获取接口名,方法名 ,入参对象 转发到你的mock 服务器获取返回报文组装成Result对象 returrn 就可以了。 注意:泛型调用和正常调用获取Invocation参数方式不一样