RPC即远程过程调用,也叫远程方法调用,RPC框架可以实现调用方可以像调用本地方法一样调用远程服务的方法。
步骤思路:
1、想要在Consumer中调用Provider中sayHello方法,文件组织如下:
Provider-common模块:定义暴露接口供提供者和消费者使用(Consumer不希望直接使用Provider的实现类,而只是使用其接口进行调用),然后让Consumer和Provider依赖于Provider-common包。
java
public interface HelloService {
String sayHello(String name);
}
Provider实现HelloServiceImpl实现接口
java
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello: " + name;
}
}
Consumer指定实现类并执行方法调用:
java
public class Comsumer {
public static void main(String[] args) {
HelloService helloService = ?;
String result = helloService.sayHello("test");
}}
现在想做的事情就是Consumer能够调用到Provider中的实现类方法HelloServiceImpl
2、实现思路:在Provider启动时首先将自己要开放的接口本地注册到RPC,然后调用RPC server 中start方法开启Tomcat接受网络请求,然后由RPC servlet解析req得到接口名、调用方法、参数类型、方法参数等信息,然后通过接口注册信息找到相应接口,然后通过反射执行此接口方法并将结果封装到resp并返回。
Provider类:
java
public class Provider {
public static void main(String[] args) {
//将提供的服务注册进行本地注册以便后续进行反射调用
LocalRegister.regist(HelloService.class.getName(), "1.0", HelloServiceImpl.class);
// 启动Netty、Tomcat服务
HttpServer httpServer = new HttpServer();
httpServer.start(url.getHostname(), url.getPort());
}
}
RPC模块:
HttpServer类:负责接收处理请求并返回结果
java
public class HttpServer {
public void start(String hostname, Integer port){
// 读取用户的配置 server.name=netty
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", new DispatcherServlet());
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
tomcat.getServer().await();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
public class DispatcherServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
new HttpServerHandler().handler(req, resp);
}
}
public class HttpServerHandler {
public void handler(HttpServletRequest req, HttpServletResponse resp){
// 处理请求-->接口、方法、方法参数
try {
Invocation invocation = (Invocation) new ObjectInputStream(req.getInputStream()).readObject();
String interfaceName = invocation.getInterfaceName();
Class classImpl = LocalRegister.get(interfaceName, "1.0");
Method method = classImpl.getMethod(invocation.getMethodName(), invocation.getParameterTypes());
String result = (String) method.invoke(classImpl.newInstance(), invocation.getParameters());
IOUtils.write(result, resp.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
Invocation类:传递参数包装类
java
public class Invocation implements Serializable {
private String interfaceName;
private String methodName;
private Class[] parameterTypes;
private Object[] parameters;
public Invocation(String interfaceName, String methodName, Class[] parameterTypes, Object[] parameters) {
this.interfaceName = interfaceName;
this.methodName = methodName;
this.parameterTypes = parameterTypes;
this.parameters = parameters;
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
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;
}
}
LocalRegister类:保存接口注册信息
java
public class LocalRegister {
private static Map<String, Class> map = new HashMap<>();
public static void regist(String interfaceName, String version, Class implClass){
map.put(interfaceName+version, implClass);
}
public static Class get(String interfaceName, String version) {
return map.get(interfaceName+version);
}
}
3、Consumer启动时调用RPC client 发送http网络请求传递接口名、方法、参数信息,并堵塞等待,RPC server收到请求处理,然后得到返回结果实现调用,这就实现了RPC的基本功能。
Consumer:
java
public class Consumer {
public static void main(String[] args) {
Invocation invocation = new Invocation(HelloService.class.getName(), "sayHello", new Class[]{String.class}, new Object[]{"lizhihui"});
HttpClient httpClient = new HttpClient();
try {
String result = httpClient.send("localhost", 8080, invocation);
System.out.println("result = " + result);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4、但是有个问题,这种调用方式不是我们希望达到的效果(即,通过实例化对象然后执行方法进行调用),我们就需要得到一个具体的对象。怎么得到?解决方法:让RPC能够根据接口产生并返回接口代理对象。
Comsumer启动方法:
java
public class Comsumer {
public static void main(String[] args) {
HelloService helloService = ProxyFactory.getProxy(HelloService.class);
String result = helloService.sayHello("zhouyu123123123");
System.out.println(result);
// Invocation invocation = new Invocation(HelloService.class.getName(), "sayHello", new Class[]{String.class}, new Object[]{"lizhihui"});
// HttpClient httpClient = new HttpClient();
// try {
// String result = httpClient.send("localhost", 8080, invocation);
// System.out.println("result = " + result);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
}
}
ProxyFactory类:
java
public class ProxyFactory {
public static <T> T getProxy(Class interfaceClass) {
// 用户配置
Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
method.getParameterTypes(), args);
HttpClient httpClient = new HttpClient();
String result = httpClient.send("localhost", 8080, invocation);
return result;
}
});
return (T) proxyInstance;
}
}
5、地址和端口号硬编码问题,"localhost"和8080为Provider的ip地址和端口号,如果我们能确定Provider应用的ip地址和端口号就能够解决这个问题。解决方法:注册中心,服务提供者Provider需要将自己的ip地址和端口号存到注册中心(Map实现)上去。服务消费者从注册中心拿到此ip地址和端口号就能解决这个问题。
URL类:
java
public class URL implements Serializable {
private String hostname;
private Integer port;
public URL(String hostname, Integer port) {
this.hostname = hostname;
this.port = port;
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
}
注册中心:(注册中心三要素:注意这里是不能实现的,因为Provider和Consumer是两个不同的线程,Provider存的Map是不能被Consumer取到的,所以需要redis、zookeeper等进行共享 数据。此外,注册中心还需要有能力(心跳机制)监听 挂掉的服务 从而下线其注册数据。此外,消费者为了不是每一次都到注册中心读取数据影响速度,服务消费者还需要本地缓存,这就会引入本地缓存和注册中心之间的数据一致性问题,注册中心需要有数据变更的监听机制。)为了解决共享问题们这里采用简化方法,保存到本地文件进行共享的方式(savfile和getfile方法)。
java
public class MapRemoteRegister {
private static Map<String, List<URL>> map = new HashMap<>();
public static void regist(String interfaceName, URL url){
List<URL> list = map.get(interfaceName);
if (list == null) {
list = new ArrayList<>();
}
list.add(url);
map.put(interfaceName, list);
saveFile();
}
public static List<URL> get(String interfaceName) {
map = getFile();
return map.get(interfaceName);
}
private static void saveFile() {
try {
FileOutputStream fileOutputStream = new FileOutputStream("/temp.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(map);
} catch (IOException e) {
e.printStackTrace();
}
}
private static Map<String, List<URL>> getFile() {
try {
FileInputStream fileInputStream = new FileInputStream("/temp.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
return (Map<String, List<URL>>) objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
Provider类:
java
public class Provider {
public static void main(String[] args) {
LocalRegister.regist(HelloService.class.getName(), "1.0", HelloServiceImpl.class);
// 注册中心注册 服务注册
URL url = new URL("localhost", 8080);
MapRemoteRegister.regist(HelloService.class.getName(), url);
// Netty、Tomcat
HttpServer httpServer = new HttpServer();
httpServer.start(url.getHostname(), url.getPort());
}
}
ProxyFactory类:
java
public class ProxyFactory {
public static <T> T getProxy(Class interfaceClass) {
// 用户配置
Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
method.getParameterTypes(), args);
HttpClient httpClient = new HttpClient();
// 服务发现
List<URL> list = MapRemoteRegister.get(interfaceClass.getName());
//负载均衡
URL url = Loadbalance.random(list);
// 服务调用
String result = httpClient.send(url.getHostname(), url.getPort(), invocation);
return result;
}
});
return (T) proxyInstance;
}
}
这里获得了List<URL>是一个列表,我们需要选择一个rul进行服务调用,这就需要负载均衡,这里采用随机算法。
Loadbalance类:
java
public class Loadbalance {
public static URL random(List<URL> urls){
Random random = new Random();
int i = random.nextInt(urls.size());
return urls.get(i);
}
}
6、封装Provider启动类 Bootstrap
静态类
java
public class Provider {
public static void main(String[] args) {
// LocalRegister.regist(HelloService.class.getName(), "1.0", HelloServiceImpl.class);
//
// // 注册中心注册 服务注册
// URL url = new URL("localhost", 8080);
// MapRemoteRegister.regist(HelloService.class.getName(), url);
//
//
// // Netty、Tomcat
// HttpServer httpServer = new HttpServer();
// httpServer.start(url.getHostname(), url.getPort());
Bootstrap.start(
HelloService.class.getName(), // 服务接口名称
"1.0", // 服务版本号
HelloServiceImpl.class, // 服务实现类
"localhost", // 主机名
8080 // 端口号
);
}
}
class Bootstrap {
// 启动服务的静态方法,接收参数
public static void start(String serviceName, String version, Class<?> serviceImplClass, String hostname, int port) {
// 1. 本地注册服务
registerLocalService(serviceName, version, serviceImplClass);
// 2. 远程注册服务
URL serviceUrl = registerRemoteService(serviceName, hostname, port);
// 3. 启动 HTTP 服务器
startHttpServer(serviceUrl);
}
// 本地注册服务
private static void registerLocalService(String serviceName, String version, Class<?> serviceImplClass) {
LocalRegister.regist(serviceName, version, serviceImplClass);
System.out.println("Service registered locally: " + serviceName + " (Version: " + version + ")");
}
// 远程注册服务
private static URL registerRemoteService(String serviceName, String hostname, int port) {
URL url = new URL(hostname, port);
MapRemoteRegister.regist(serviceName, url);
System.out.println("Service registered remotely at: " + url);
return url;
}
// 启动 HTTP 服务器
private static void startHttpServer(URL url) {
HttpServer httpServer = new HttpServer();
httpServer.start(url.getHostname(), url.getPort());
System.out.println("HTTP server started on " + url.getHostname() + ":" + url.getPort());
}
}
6、服务调用时的服务容错(假设Provider未启动,Consumer先启动肯定会报错,我们可以写一些额外的逻辑,例如调用别的方法、记录日志等)
ProxyFactory类:修改 ProxyFactory
以接收 ErrorCallback
参数 : 我们将 ErrorCallback
作为一个参数传递给 ProxyFactory.getProxy()
方法,这样消费者可以在调用时自定义容错逻辑。
java
public class ProxyFactory {
public static <T> T getProxy(Class interfaceClass,ErrorCallback errorCallback) {
// 用户配置
Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
method.getParameterTypes(), args);
HttpClient httpClient = new HttpClient();
// 服务发现
List<URL> list = MapRemoteRegister.get(interfaceClass.getName());
URL url = Loadbalance.random(list);
String result=null;
// 服务调用
try {
result = httpClient.send(url.getHostname(), url.getPort(), invocation);
}
catch (Exception e)
{
// 如果传入了 ErrorCallback,则调用
result = handleFailure(e, invocation, url, errorCallback);
}
return result;
}
// 服务容错逻辑的处理方法
private String handleFailure(Exception e, Invocation invocation, URL url, ErrorCallback errorCallback) {
String result = null;
int retryCount = 3; // 定义重试次数
long retryDelay = 2000; // 定义重试间隔(毫秒)
// 重试策略:尝试多次重试
for (int i = 0; i < retryCount; i++) {
try {
Thread.sleep(retryDelay); // 等待一段时间后重试
HttpClient httpClient = new HttpClient();
result = httpClient.send(url.getHostname(), url.getPort(), invocation);
if (result != null) {
return result; // 成功返回
}
} catch (Exception retryException) {
// 继续重试
if (i == retryCount - 1) {
// 重试失败,执行回调策略
result = fallback(invocation, retryException, errorCallback);
}
}
}
return result;
}
// 调用回调策略
private String fallback(Invocation invocation, Exception e, ErrorCallback errorCallback) {
if (errorCallback != null) {
return errorCallback.handleError(invocation, e); // 调用消费者传递的回调
}
// 如果没有回调,返回一个默认的错误信息
return "服务不可用,请稍后再试";
}
});
return (T) proxyInstance;
}
}
RPC包下ErrorCallback接口:
java
public interface ErrorCallback {
String handleError(Invocation invocation, Exception e);
}
Comsumer类:消费者传递 ErrorCallback
: 在 Consumer
类中,我们可以传递一个自定义的 ErrorCallback
实现。
java
public class Comsumer {
public static void main(String[] args) {
// 创建一个自定义的 ErrorCallback 实现
ErrorCallback customErrorCallback = new ErrorCallback() {
@Override
public String handleError(Invocation invocation, Exception e) {
// 这里可以定义自己的容错逻辑
System.out.println("自定义错误处理:服务调用失败,错误信息:" + e.getMessage());
// 返回自定义的错误信息或执行其他操作
return "自定义错误响应: 服务不可用";
}
};
HelloService helloService = ProxyFactory.getProxy(HelloService.class,customErrorCallback);
String result = helloService.sayHello("zhouyu123123123");
System.out.println(result);
}
}
7、服务mock,服务没有开发完成先返回一个mock结果。然后通过配置文件设置返回值。
java
public class ProxyFactory {
public static <T> T getProxy(Class interfaceClass,ErrorCallback errorCallback) {
// 用户配置
Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String mock = System.getProperty("mock");
if (mock!=null && mock.startsWith("retuen:"))
{
String result = mock.replace("return:", "");
return result;
}
.................