客户端与服务端的请求,主要是对服务端资源的增删改查,所以大多数人会想到REST,而服务与服务之间的通信,主要是传入参数,调用方法,得到结果,所以大多数人会想到RPC

提到RPC的话,网络上已经有各种成熟的方案,诸如grpcbrpc等等。这些方案通常用在大型项目上,如果只是小项目乃至单机部署,不需要集群,使用网络上的方案颇有种大炮打蚊子的感觉。所以我们可以自己实现一个简易的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需要三个参数,methodargs我们可以在动态代理中直接获取,而service则得通过其他方式传入,我们可以通过注解的方式。

定义两个注解,HttpRpcService加在接口上,配置value对应服务端上的serviceHttpRpcMethod则加在方法上,表示该方法用于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请求,再对返回的结果简单的处理了一下。如上面所说的,methodargs参数都能直接在动态代理中获得,而service则需要外部传入,除此外还需要传入restTemplatehttpRpcServerBaseUrl

有了Proxy类,我们需要找个地方生成代理实例,我在这里编写了一个HttpRpcFactory类,传入带有@HttpRpcService注解的接口,如上面的DemoServiceClient,返回代理实例。也就是在这一步,向代理类传入了restTemplatehttpRpcServerBaseUrl以及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 test.png

总结

这只是一个非常简单的rpc实现方式,现有的框架要比这复杂的多得多。虽然简单,但是用到了java的很多特性,包括动态代理,反射,这些都是平时写业务代码几乎不会接触的,写这些代码和这篇文章时,也是我对这些特性的一个加深理解,希望能对你有所帮助。