实现一个基于Http和Spring的简易RPC
客户端与服务端的请求,主要是对服务端资源的增删改查,所以大多数人会想到REST
,而服务与服务之间的通信,主要是传入参数,调用方法,得到结果,所以大多数人会想到RPC
。
提到RPC
的话,网络上已经有各种成熟的方案,诸如grpc
,brpc
等等。这些方案通常用在大型项目上,如果只是小项目乃至单机部署,不需要集群,使用网络上的方案颇有种大炮打蚊子的感觉。所以我们可以自己实现一个简易的RPC
框架。
需求分析
开始前我们先来想想要实现一个怎样的效果。
服务端
@Service("demoService")
public class DemoServiceImpl implements DemoService {
@Override
public String testHttpRpc(String msg) {
return "hello ! " + msg;
}
}
这是服务端的一个serivce
,有一个testHttpRpc
方法,我们想要在另一个服务端上调用该方法,最直观的方式是什么?
为这个方法写一个接口/rpc/demo/test_http_rpc
,这样当然是可行的,但如果有100个,1000个方法,那代码中就会充斥着大量相似的垃圾代码,这显然是不优雅的。
因此,我们可以只暴露一个接口/rpc
,根据传入的字段去调用对用的方法,然后返回。所以我们要为这套rpc
系统设计一个简单的协议,请求的时候传入服务名service
,方法名method
,以及调用的参数args
,返回的时候返回一个结果码code
和方法调用结果response
。
public class HttpRpcRequestEntity {
private String service;
private String method;
private Object[] args;
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
}
public class HttpRpcResponseEntity {
int code;
Object response;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Object getResponse() {
return response;
}
public void setResponse(Object response) {
this.response = response;
}
}
客户端
说完了服务端(被调用端)如何接收,然后分析一下客户端(调用端)如何调用。
既然服务端提供了接口,客户端使用Http
请求即可,可以在客户端封装一个方法testHttpRpc(String msg)
,方法内构建一次http
请求。
这样可行吗?这样当然可行,但是这样优雅吗?同样的,如果有成百上千的方法,那代码中又会多出上千行的垃圾代码。
可以想象一下,这些垃圾代码都是非常相似的,创建一个Http
请求对象,然后构建请求参数,既然如此,我们可以使用动态代理,动态生成重复的操作,而只需要编写跟方法定义有关的代码。
public interface DemoServiceClient {
String testHttpRpc(String msg);
}
就像这样,定义一个接口,通过动态代理,在调用这个接口的时候对服务端发起http
请求,并取出返回值。
异常
因为这只是一个简单的rpc
调用,对异常的处理我们不做过多的文章,仅定义两种异常,HttpRpcProxyException
即代理异常,发生在代码有误的情况,HttpRpcExecException
即方法执行过程中出现的异常。
代码实现
先贴完整代码链接spring-rpc-demo
客户端
代码实现我们从客户端入手,由需求我们可以知道,发起rpc
需要三个参数,method
和args
我们可以在动态代理中直接获取,而service
则得通过其他方式传入,我们可以通过注解的方式。
定义两个注解,HttpRpcService
加在接口上,配置value
对应服务端上的service
。HttpRpcMethod
则加在方法上,表示该方法用于rpc
。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HttpRpcService {
String value();
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HttpRpcMethod {
}
还记得之前的DemoServiceClient
吗,现在它变成了这样,这个接口对应服务端的demoService
,且testHttpRpc
方法是服务端的方法。
@HttpRpcService("demoService")
public interface DemoServiceClient {
@HttpRpcMethod
String testHttpRpc(String msg);
}
前置工作做完了,就是动态代理类的编写了。
public class HttpRpcProxy implements InvocationHandler {
private final String httpRpcServerBaseUrl;
private final String service;
private final RestTemplate restTemplate;
public HttpRpcProxy(RestTemplate restTemplate,String httpRpcServerBaseUrl, String service) {
this.httpRpcServerBaseUrl = httpRpcServerBaseUrl;
this.restTemplate = restTemplate;
this.service = service;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
HttpRpcRequestEntity req = new HttpRpcRequestEntity();
req.setService(service);
req.setMethod(method.getName());
req.setArgs(args);
HttpRpcResponseEntity res;
try {
res = restTemplate.postForObject(URI.create(httpRpcServerBaseUrl), req, HttpRpcResponseEntity.class);
} catch (Exception e) {
throw new HttpRpcExecException("http request failed");
}
if (res == null) {
throw new HttpRpcExecException("http request failed");
}
if (res.getCode() == 1) {
throw new HttpRpcExecException("service not found");
}
if (res.getCode() == 2) {
throw new HttpRpcExecException("method not found");
}
if ( res.getCode() == 3) {
throw new HttpRpcExecException("method exec exception");
}
return res.getResponse();
}
}
这个类很简单,invoke
就是实现一次http
请求,再对返回的结果简单的处理了一下。如上面所说的,method
和args
参数都能直接在动态代理中获得,而service
则需要外部传入,除此外还需要传入restTemplate
和httpRpcServerBaseUrl
。
有了Proxy
类,我们需要找个地方生成代理实例,我在这里编写了一个HttpRpcFactory
类,传入带有@HttpRpcService
注解的接口,如上面的DemoServiceClient
,返回代理实例。也就是在这一步,向代理类传入了restTemplate
,httpRpcServerBaseUrl
以及httpRpcService.value()
。
@Component
public class HttpRpcFactory {
private RestTemplate restTemplate;
@Value("${httpRpc.server.rpcUrl}")
private String httpRpcServerBaseUrl;
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@SuppressWarnings("unchecked")
public <T> T refer(Class<T> stubClass) {
HttpRpcService httpRpcService = stubClass.getAnnotation(HttpRpcService.class);
if (httpRpcService == null) {
throw new HttpRpcProxyException("proxy service is not a http rpc service");
}
HttpRpcProxy p = new HttpRpcProxy(restTemplate, httpRpcServerBaseUrl, httpRpcService.value());
return (T) Proxy.newProxyInstance(stubClass.getClassLoader(), new Class[]{stubClass}, p);
}
}
最后把DemoServiceClient
注册成Bean
即可。
@Component
public class RpcClientConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public DemoServiceClient demoServiceClient(HttpRpcFactory httpRpcFactory) {
return httpRpcFactory.refer(DemoServiceClient.class);
}
}
服务端
服务端的实现要简单的多,只需要一个http
接口,然后根据参数调用方法。但是如何根据参数定位方法?如果用if else
的方法去定位是很蠢的,这里可以通过反射的方式。
@RestController
public class RpcController {
ApplicationContext applicationContext;
@Autowired
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@PostMapping("/rpc")
public HttpRpcResponseEntity rpc(@RequestBody HttpRpcRequestEntity request) {
HttpRpcResponseEntity response = new HttpRpcResponseEntity();
String service = request.getService();
String methodName = request.getMethod();
if (!applicationContext.containsBean(service)) {
response.setCode(1);
return response;
}
Object proxyObject = applicationContext.getBean(service);
Method method = getMethod(proxyObject.getClass(), methodName, request.getArgs());
if (method == null) {
response.setCode(2);
return response;
}
try {
Object res = method.invoke(proxyObject, request.getArgs());
response.setCode(0);
response.setResponse(res);
return response;
} catch (IllegalAccessException | InvocationTargetException e) {
response.setCode(3);
return response;
}
}
private Method getMethod(Class<?> proxyObject, String methodStr, Object[] args) {
Method[] methods = proxyObject.getMethods();
int argsLength = args == null ? 0 : args.length;
for(Method method : methods) {
if(method.getName().equalsIgnoreCase(methodStr) && method.getParameterCount() == argsLength) {
return method;
}
}
return null;
}
}
我们这里通过applicationContext
拿到指定service
类的Bean
,然后通过方法名和参数数量拿到指定方法,最后通过反射调用。
测试
编写一个简单的单例测试。
@SpringBootTest
class SpringRpcDemoApplicationTests {
@Autowired
private DemoServiceClient demoServiceClient;
@Test
void testRpc() {
String res = demoServiceClient.testHttpRpc("test");
System.out.println(res);
}
}
先运行服务端,然后执行单例测试,非常完美:)
总结
这只是一个非常简单的rpc
实现方式,现有的框架要比这复杂的多得多。虽然简单,但是用到了java
的很多特性,包括动态代理,反射,这些都是平时写业务代码几乎不会接触的,写这些代码和这篇文章时,也是我对这些特性的一个加深理解,希望能对你有所帮助。
Or you can contact me by Email