实现一个RPC框架
文章目录
1.RPC需要做什么
首先看一下RPC调用的流程:
由调用过程可得出:
- provide需要向中心注册服务、向cunsumer提供服务
- 注册中心需要接收provider的注册并保存注册信息、向cunsumer提供目标服务的所有provider的信息
- cunsumer需要到中心获取服务provider列表、向provider发起请求
2.框架需要提供什么
a.provider
provider需要向中心注册服务,所以框架需要向provider提供注册机制,使provider能够向中心注册服务,同时还要把注册信息保存到本地,因为当consumer调用时,provider在获取到consumer的调用信息后,需要知道调用的是哪个类的哪个方法,所以服务信息不只要向中心注册,还要在本地保存。
provider还要启动一个网络服务,来接受consumer的请求并处理。
因此,规划框架为provider提供:
- ProviderServiceManage:provider的服务管理器,提供register方法,和get方法,register负责将服务信息注册到中心和本地,get负责根据调用信息获取到实现类
- ProviderHttpServer:负责启动provider网络服务,并注册服务信息
b.consumer
consumer发生调用时,需要先到中心获取服务列表,并通过负载均衡决定调用哪一个provider,然后向provider发送请求。
因此,规划框架为consumer提供:
- ServiceDiscover:服务发现执行器,负责根据调用信息到注册中心获取服务列表
- LoadBalance:负载均衡,负责根据传入的provider信息列表,根据算法返回一个本次要调用的provider的信息
- HttpClient:网络客户端,负责发起远程调用
c.center
注册中心需要启动一个网络服务,接收provider的注册请求并记录,在consumer执行服务发现操作的时候把对应的provider列表返回。
因此,规划框架为center提供:
- HttpServer:负责启动网络服务
- CenterServiceManage:注册中心服务管理器,提供register和get方法,register负责将provider的注册信息保存,get负责根据调用信息获取对应的provider列表
3.实现
3.1目录规划
框架项目规划如下:
basic包下放置一些基础内容
common包下放置一些基本类
http包下放置网络服务相关
loadbalance包下放置负载均衡相关
proxy包放置代理
register包放置服务注册相关
3.2基础类
首先定义一些存放信息的DTO:
- Invaoction:存放接口调用信息,当发生调用时,需要知道调用的是哪个服务的哪个方法,定位服务可以用服务名+版本号实现,定位方法需要知道方法名、参数列表,实现调用还需要参数,所以DTO的字段为:服务名、方法名、版本、参数列表、参数
- URL:存放地址信息:主机名称和端口
- DemoCenterInfo:负责记录注册中心地址,使provider和consumer某些方法获取注册中心信息时更方便
首先是Invocation:
package com.demo.common;
import java.io.Serializable;
import java.util.Arrays;
public class Invocation implements Serializable {
private String serviceName;
private String methodName;
private Class[] parameterTypes;
private Object[] parameters;
private String version;
public Invocation(String serviceName, String methodName, Class[] parameterTypes, Object[] parameters, String version) {
this.serviceName = serviceName;
this.methodName = methodName;
this.parameterTypes = parameterTypes;
this.parameters = parameters;
this.version = version;
}
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class[] getParameterTypes() {
return parameterTypes;
}
public void setParameterTypes(Class[] parameterTypes) {
this.parameterTypes = parameterTypes;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
public String getVersion() {
if (version == null || "".equals(version)) {
return "1.0";
}
return version;
}
public void setVersion(String version) {
this.version = version;
}
@Override
public String toString() {
return "serviceName" + "=" + serviceName + "," +
"methodName" + "=" + methodName + "," +
"version" + "=" + version + "," +
"parameterTypes" + "=" + Arrays.toString(parameterTypes) + "," +
"parameters" + "=" + Arrays.toString(parameters);
}
}
接下来实现URL:
package com.demo.common;
import java.io.Serializable;
public class URL implements Serializable {
private String hostname;
private Integer port;
public String getHostname() {
return hostname;
}
public Integer getPort() {
return port;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public void setPort(Integer port) {
this.port = port;
}
}
最后是DemoCenterInfo:
package com.demo.common;
public class DemoCenterInfo {
private static String hostname;
private static Integer port;
public static Integer getPort() {
return port;
}
public static String getHostname() {
return hostname;
}
public static void setPort(Integer port) {
DemoCenterInfo.port = port;
}
public static void setHostName(String hostName) {
DemoCenterInfo.hostname = hostName;
}
}
3.3实现网络服务
基础类已经实现完成了,接下来就是网络服务的实现。通过对调用过程的分析可得:provider需要向中心发送请求进行注册、consumer需要向中心发送请求获取服务列表以及向provider发送请求进行远程调用,所以需要一个客户端,来进行网络请求的发送;provider需要启动一个服务,接受并处理来自consumer的调用请求,而center需要启动一个服务接收并处理provider的注册、接收并处理consumer的服务列表获取(服务发现)。
首先定义一个网络客户端,主要是发送请求使用,定义一个接口类,用于规范实现类,存放在com.demo.basic.http下:
package com.demo.basic.http;
import com.demo.common.Invocation;
import java.lang.reflect.Method;
public interface IHttpClient {
public Object send(String hostname, Integer port, Invocation invocation);
public <T> T send(String hostname, Integer port, Invocation invocation, Method method);
}
定义了两个方法,第一个是未指定返回值类型的发送请求,第二个是将方法信息也传入,实现类可以根据方法的信息去编码返回值,用以返回期待类型的数据。
实现类如下:
package com.demo.http;
import com.demo.basic.datacode.DeCode;
import com.demo.basic.http.IHttpClient;
import com.demo.common.Invocation;
import org.apache.commons.io.IOUtils;
import com.alibaba.fastjson.JSONObject;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpClient implements IHttpClient {
@Override
public Object send(String hostname, Integer port, Invocation invocation) {
try {
URL url = new URL("http", hostname, port, "/");
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
// 配置
OutputStream outputStream = httpURLConnection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(invocation);
oos.flush();
oos.close();
InputStream inputStream = httpURLConnection.getInputStream();
// 读取结果并返回
String resultBeforeDecode = IOUtils.toString(inputStream);
// 解码
return DeCode.decode(resultBeforeDecode);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public <T> T send(String hostname, Integer port, Invocation invocation, Method method) {
try {
URL url = new URL("http", hostname, port, "/");
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
// 配置
OutputStream outputStream = httpURLConnection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(invocation);
oos.flush();
oos.close();
InputStream inputStream = httpURLConnection.getInputStream();
// 读取结果并返回
String resultBeforeDecode = IOUtils.toString(inputStream);
// 解码
return DeCode.decode(resultBeforeDecode, method);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
两个方法都使用到了一个DeCode.decode方法,是因为在使用网络传输数据时,需要将实例转码成字节流。因此实现了EnCode.encode方法将实例通过JSONObject的toString方法转为字符串,然后使用IOUtils.write方法写到流里,客户端接收到请求后取出字符串,交给DeCode.decode方法进行解码,解码成字符串后通过JSONObject还原成实例。
客户端还使用到了一个工具类:IOUtils,用于将流读取成字符串。
DeCode代码如下:
package com.demo.basic.datacode;
import com.alibaba.fastjson.JSONObject;
import java.lang.reflect.Method;
public class DeCode {
public static String decode(String data) {
// 这里可替换为自己的算法(encode的逆运算)
return data;
}
public static <T> T decode(String str, Method method) {
// 实现解码
return JSONObject.parseObject(str, method.getGenericReturnType());
}
}
EnCode的代码如下:
package com.demo.basic.datacode;
import com.alibaba.fastjson.JSONObject;
public class EnCode {
public static String encode(Object obj) {
return JSONObject.toJSONString(obj);
}
}
至此,发送网络请求并获取返回值的部分已经实现了,接下来是启动tomcat的部分。
首先定义一个接口类,规范实现类的方法:
package com.demo.basic.http;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.http.HttpServlet;
public interface IHttpServer {
public void startServer(String hostname, Integer port, HttpServlet httpServlet);
public Tomcat createServer(String hostname, Integer port, HttpServlet httpServlet);
public void runServer(Tomcat tomcat) throws LifecycleException;
public void wait(Tomcat tomcat);
}
其中startServer用于启动一个网络服务并进入请求等待,适用于中心端调用,启动服务后就进入等待,避免程序结束运行;createServer用于创建并返回一个tomcat实例,适用于peovider调用,创建tomcat实例后,provider可以进行服务注册等操作。runServer用于异步启动服务,wait用于进入等待,避免程序结束运行。
实现类如下:
package com.demo.http;
import com.demo.basic.http.IHttpServer;
import org.apache.catalina.*;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.http.HttpServlet;
import java.util.concurrent.CompletableFuture;
public class HttpServer implements IHttpServer {
@Override
public void startServer(String hostname, Integer port, HttpServlet httpServlet) {
// 启动一个tomcat服务
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(port);
Engine engine = new StandardEngine();
engine.setDefaultHost(hostname);
Host host = new StandardHost();
host.setName(hostname);
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", httpServlet);
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
tomcat.getServer().await();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Tomcat createServer(String hostname, Integer port, HttpServlet httpServlet) {
// 启动一个tomcat服务
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(port);
Engine engine = new StandardEngine();
engine.setDefaultHost(hostname);
Host host = new StandardHost();
host.setName(hostname);
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", httpServlet);
context.addServletMappingDecoded("/*", "dispatcher");
return tomcat;
}
@Override
public void runServer(Tomcat tomcat) throws LifecycleException {
tomcat.start();
}
@Override
public void wait(Tomcat tomcat) {
tomcat.getServer().await();
}
}
至此,启动网络服务的代码完成了,可以发现,startServer和createServer都接收了一个HttpServlet类型的入参,也就是创建或者启动服务的时候需要一个HttpServlet实例,这个实例是做什么的呢?
HttpServlet实例就是用来处理网络请求的,具体的处理逻辑就写在HttpServlet的service方法内。当tomcat接收到一个请求时,会调用httpServlet的service方法。
注册中心的servlet需要具有的功能就是注册和服务发现,provider的servlet需要实现的功能就是目标服务调用。自己实现的servlet需要继承于javax.servlet.http.HttpServlet,我们只需要重写其中的service方法即可。
首先实现注册中心的servlet:
package com.demo.http.servlet;
import com.demo.basic.datacode.EnCode;
import com.demo.register.CenterServiceManage;
import com.demo.common.Invocation;
import com.demo.common.URL;
import org.apache.commons.io.IOUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.List;
public class DemoCenterServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理请求的逻辑
// 首先将req中携带的请求信息进行解析
// 因为项目已经定义Invocation作为请求信息的载体
// 所以默认中心端接收的请求中携带的数据就是Invocation的实例
try {
Invocation invocation =(Invocation) new ObjectInputStream(req.getInputStream()).readObject();
// 接下来进行请求的分析
// 约定provider和consumer调用中心端时,将所请求的类型(注册还是服务发现)存放于methodName
switch (invocation.getMethodName()) {
case "register":
// 服务注册 参数列表: 服务名、服务版本、服务地址(URL实例来表示)
CenterServiceManage.register(invocation.getParameters()[0].toString(), invocation.getParameters()[1].toString(), (URL) invocation.getParameters()[2]);
IOUtils.write("注册成功", resp.getOutputStream());
break;
case "discover":
// 服务发现
List<URL> urls = CenterServiceManage.get(invocation.getParameters()[0].toString(), invocation.getParameters()[1].toString());
String result = EnCode.encode(urls);
IOUtils.write(result, resp.getOutputStream());
break;
default:
IOUtils.write("处理失败:所请求的方法在服务器上未发现", resp.getOutputStream());
}
} catch (Exception e) {
// 当发生异常时,将错误信息返回
IOUtils.write(e.getMessage(), resp.getOutputStream());
}
}
}
CenterServiceManage是中心端服务管理器,后续会进行实现。
接下来是provider的servlet,所要做的就是定位到目标方法并执行,将返回值进行编码后写入流中返回:
package com.demo.http.servlet;
import com.demo.basic.datacode.EnCode;
import com.demo.register.ProviderServiceManage;
import com.demo.common.Invocation;
import org.apache.commons.io.IOUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
public class ProviderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理请求的逻辑
try {
// 获取请求信息 服务名、版本号 -》 定位一个服务 然后通过methodName和parameterTypes定位方法
Invocation invocation = (Invocation) new ObjectInputStream(req.getInputStream()).readObject();
// 获取到请求信息后 ProviderServiceManage是provider的服务管理器 getServiceImplClass是根据服务名和版本号获取服务实现类的方法
Class serviceImplClass = ProviderServiceManage.getServiceImplClass(invocation.getServiceName(), invocation.getVersion());
// 获取方法
Method method = serviceImplClass.getMethod(invocation.getMethodName(), invocation.getParameterTypes());
// 执行
Object result = method.invoke(serviceImplClass.newInstance(), invocation.getParameters());
// 结果编码
String resultAfterEnCode = EnCode.encode(result);
IOUtils.write(resultAfterEnCode, resp.getOutputStream());
} catch (Exception e) {
// 当发生异常时,将错误信息返回
IOUtils.write(e.getMessage(), resp.getOutputStream());
}
}
}
ProviderServiceManage.getServiceImplClass方法是是根据服务名和版本号获取服务实现类的方法,在接下来的服务注册和发现中会进行实现。
3.4实现服务注册
provider和center提供网络服务、provider和consumer发送网络请求的代码都已经实现了,接下来就是服务的注册。
首先实现服务注册功能:provider通过HttpClient发送请求,将服务名、服务版本号、自己的地址信息发送给注册中心center,然后在本地也进行服务名、版本号、实现类信息的保存,这样当consumer调用过来的时候才能找到对应的实现类。center需要做的就是将服务名、版本号相同的地址,存放在一起,当consumer来进行服务发现时,将对应服务名和版本号的所有provider地址都返回给consumer,由consumer来决定调用哪个provider(也就是负载均衡)。
首先实现provider的服务注册,定义一个provider的服务管理器,当发生注册行为的时候,自动向中心和本地进行注册,当相应consumer调用时,根据服务名和版本号获取对应实现类:
package com.demo.register;
import com.demo.common.Invocation;
import com.demo.common.DemoCenterInfo;
import com.demo.common.URL;
import com.demo.http.HttpClient;
import java.util.HashMap;
import java.util.Map;
public class ProviderServiceManage {
private static Map<String, Class> serviceReflectMap;
public static void register(String serviceName, String version, URL url, Class implClass) {
if (serviceReflectMap == null) {
serviceReflectMap = new HashMap<>();
}
// 首先向中心进行注册
HttpClient httpClient = new HttpClient();
Invocation invocation = new Invocation(null, "register", new Class[]{String.class, String.class, URL.class}, new Object[]{serviceName, version, url}, null);
httpClient.send(DemoCenterInfo.getHostname(), DemoCenterInfo.getPort(), invocation);
// 本地注册
serviceReflectMap.put(serviceName + version, implClass);
}
public static Class getServiceImplClass(String serviceName, String version) {
// 获取服务
return serviceReflectMap.getOrDefault(serviceName + version, null);
}
}
接下来实现注册中心使用的服务管理器:
package com.demo.register;
import com.demo.common.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CenterServiceManage {
private static Map<String, List<URL>> serviceReflectMap;
// 提供一个初始化方法,可以在启动服务的时候进行初始化,避免多线程处理register的时候发生覆盖的情况
public static void init() {
if (serviceReflectMap == null) {
serviceReflectMap = new HashMap<>();
}
}
public static void register(String serviceName, String version, URL url) {
if (serviceReflectMap == null) {
serviceReflectMap = new HashMap<>();
}
serviceReflectMap.computeIfAbsent(serviceName + version, k -> new ArrayList<>()).add(url);
}
public static List<URL> get(String serviceName, String version) {
if (serviceReflectMap == null) {
serviceReflectMap = new HashMap<>();
}
return serviceReflectMap.computeIfAbsent(serviceName +version, k -> new ArrayList<>());
}
}
3.5实现代理、服务发现、负载均衡
接下来就是consumer的核心代码,代理的实现,这里使用java自带的反射机制实现。代理的获取使用代理工厂实现,工厂接收一个接口类和版本,然后进行服务发现和负载均衡后,生成一个代理返回给调用者。
代理工厂代码如下:
package com.demo.proxy;
import com.demo.common.DemoCenterInfo;
import com.demo.common.Invocation;
import com.demo.common.URL;
import com.demo.http.HttpClient;
import com.demo.loadbalance.LoadBalance;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
public class ProxyFactory {
// 这里我们约定,provider进行注册时,服务名称serviceName就是interfaceClass的全名
public static <T> T getProxy(Class interfaceClass, String version) {
Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取到这个方法的目的是 进行返回值的解析
Method discoverMethod = ProxyFactory.class.getMethod("discover", String.class, String.class);
Invocation discoverInvocation = new Invocation("","discover", new Class[]{String.class, String.class}, new Object[]{interfaceClass.getName(), version}, null);
// 通过网络发送请求
HttpClient httpClient = new HttpClient();
// 服务发现
List<URL> urls = httpClient.send(DemoCenterInfo.getHostname(), DemoCenterInfo.getPort(), discoverInvocation, discoverMethod);
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(), method.getParameterTypes(), args, version);
// 负载均衡
URL url = LoadBalance.get(urls);
return httpClient.send(url.getHostname(), url.getPort(), invocation, method);
}
});
return (T) proxyInstance;
}
public List<URL> discover(String serviceName, String version) {
return null;
}
}
代理工厂中使用到了一个负载均衡,代码如下:
package com.demo.loadbalance;
import com.demo.common.URL;
import java.util.List;
import java.util.Random;
public class LoadBalance {
public static URL get(List<URL> urls) {
// 随机获取
Random random = new Random();
int i = random.nextInt(urls.size());
return urls.get(i);
}
}
至此,一个简单的RPC框架就完成了
3.6启动
定义一个启动中心服务的类如下:
package com.demo;
import com.demo.http.HttpServer;
import com.demo.http.servlet.DemoCenterServlet;
public class CenterApplication {
public static void main(String[] args) {
HttpServer httpServer = new HttpServer();
httpServer.startServer("localhost", 9090, new DemoCenterServlet());
}
}
运行main方法,可以看到服务已经启动:
4.测试
4.1定义一个接口包
首先定义一个存放接口的工程,provider和consumer都引用这个工程,命名为provider-api,定义测试接口如下:
package com.provider.api;
import com.demo.common.Invocation;
import java.util.List;
public interface TestService {
public List<Invocation> testOne(int nums);
}
项目结构如下:
4.2实现类
定义provider工程,实现上述接口并启动服务,注册服务到中心,监听consumer的请求,项目结构如下:
实现类的代码如下:
package com.provider;
import com.demo.common.Invocation;
import com.provider.api.TestService;
import java.util.ArrayList;
import java.util.List;
public class TestServiceImpl implements TestService {
@Override
public List<Invocation> testOne(int nums) {
List<Invocation> result = new ArrayList<>();
while (nums > 0) {
result.add(new Invocation(String.valueOf(nums), String.valueOf(nums), new Class[]{}, new Object[]{}));
nums --;
}
return result;
}
}
启动类的代码如下:
package com.provider;
import com.demo.common.DemoCenterInfo;
import com.demo.common.URL;
import com.demo.http.HttpServer;
import com.demo.http.servlet.ProviderServlet;
import com.demo.register.ProviderServiceManage;
import com.provider.api.TestService;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
public class ProviderApplication {
public static void main(String[] args) throws LifecycleException {
// 首先配置注册中心信息
DemoCenterInfo.setHostName("localhost");
DemoCenterInfo.setPort(9090);
// 启动服务
HttpServer httpServer = new HttpServer();
// 配置使用ProviderServlet处理请求
Tomcat tomcat = httpServer.createServer("localhost", 9091, new ProviderServlet());
httpServer.runServer(tomcat);
// 注册服务
URL url = new URL();
url.setHostname("localhost");
url.setPort(9091);
ProviderServiceManage.register(TestService.class.getName(), "1.0.0", url, TestServiceImpl.class);
// 等待请求,不退出程序
httpServer.wait(tomcat);
}
}
运行启动类,如下,服务启动成功:
4.3consumer调用
定义一个consumer模块,定义一个测试程序,项目结构如下:
测试程序如下:
package com.consumer;
import com.demo.common.DemoCenterInfo;
import com.demo.common.Invocation;
import com.demo.proxy.ProxyFactory;
import com.provider.api.TestService;
import java.util.List;
public class TestApplication {
public static void main(String[] args) {
DemoCenterInfo.setHostName("localhost");
DemoCenterInfo.setPort(9090);
TestService testService = ProxyFactory.getProxy(TestService.class, "1.0.0");
List<Invocation> result1 = testService.testOne(5);
List<Invocation> result2 = testService.testOne(8);
System.out.println(result1);
System.out.println("============================");
System.out.println(result2);
}
}
运行测试程序,结果如下:
consumer调用到了provider提供的服务,测试通过。