基于HTTP协议实现简单RPC框架的方法详解
作者:耶耶耶耶yeyeye
什么是RPC
RPC全名(Remote Procedure Call),翻译过来就是远程过程调用,这里特别说明一点,RPC是一种远程调用方式,而不是一种协议
。那么,我们其实很容易联想到与之对应的应该是本地过程调用(Local Procedure Call),而不是什么什么http协议、tcp协议.....
本地调用与远程调用
我们最常见的本地调用是什么样的?
public interface UserService { String doSomething(); } @Service public class UserServiceImpl implements UserService { @Override public String doSomething() { System.out.println("doSomething......"); } } @Controller public class UserController { @Autowired private UserService userService; public void test(){ String response = userService.doSomething(); } }
我们以最常见的思维来模拟,这就是一种对UserService
最简单的本地调用
,service实现类以及对应暴露出的接口都在本地机器上,不需要网络之间的通信就能调用服务执行。
那么,怎么样又算对UserService
的远程调用
呢?我们假设有 A, B两台机器,A为客户端,B为服务端。此时,假设我们对于UserService来说,他的impl实现类在服务端B上,客户端只知道对应的暴露接口,也就是说,对于客户端A,没有办法直接使用spring帮我们自动注入对应service使用。
此时环境下 客户端A的代码示例:
public interface UserService { //假设这是服务端暴露出来的接口 String doSomething(); } @Controller public class UserController { @Autowired private UserService userService;//对于客户端来说,此时没有实现类,无法直接使用 public void test(){ String response = userService.doSomething(); } }
RPC框架就可以很好地解决这种场景下的问题,下面简单提供一个实现思路。
实现思路
我们同样以最直接最暴力的想法去思考怎么解决这个问题:只要我客户端调用 UserService 的 doSomething() 后,客户端再发送个请求并携带相关参数(如果有的话)给服务端,服务端处理后再响应给客户端即可。那怎么实现这个过程呢?由于服务端已经对我们暴露出了接口,那我们实际上可以用代理类
的方式去实现,把请求响应的过程放在代理类中实现即可,只要生成了代理类,我们再将这个代理类交给spring容器管理
,让他正常注入,我们就可以实现在客户端没有impl类的情况下也能正常使用service方法。
我们模仿Mybatis的@MapperScan
注解,自己弄一个@RPC
注解,只要将这个注解加在配置类上,即可将对应包下的接口生成RPC代理类
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import({ServiceBeanDefinitionRegistry.class}) //注意看这个地方比较重要 public @interface RPC { String value() default ""; //扫描的路径 } //简单用法 @SpringBootApplication @RPC("com.yeyeye.client.service") //扫描对应包下的所有接口,service包下就一个上面示例中的 UserService 接口 public class RpcClientApplication { public static void main(String[] args) { SpringApplication.run(RpcClientApplication.class, args); } }
@Import 注解就不在这注释了,大概就是spring在启动的时候会导入这个注解里面写的类,这里详细介绍一下 ServiceBeanDefinitionRegistry
public class ServiceBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { //获取RPC注解里的路径 Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(RPC.class.getName()); if (MapUtil.isEmpty(attributes)) { return; } String basePackages = (String) attributes.get("value"); if (basePackages == null) { return; } //扫描路径 RpcScanner rpcScanner = new RpcScanner(registry); rpcScanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> true); rpcScanner.scan(basePackages); } }
这个类主要就做了两件事,很好理解,就是:
- 获取
@RPC
注解中的路径 - 让spring帮我们扫描这个路径,将扫描到的符合需求的bean注册到registry中。
RpcScanner
这个类是利用Spring的扫描机制写的,扫描机制具体原理不是重点,这里也不赘述。
public class RpcScanner extends ClassPathBeanDefinitionScanner { public RpcScanner(BeanDefinitionRegistry registry) { super(registry); } @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { //是不是一个接口,是的话就添加为候选组件 return beanDefinition.getMetadata().isInterface(); } @Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { //这里就是具体利用spring帮我们扫描 Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages); //对扫描出来的结果进行处理 for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) { //这里获取的beanDefinition都是接口,并没有实现类 BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition(); //注册一个我们自己的beanDefinition,这个beanDefinition是一个bean工厂,工厂用于生成接口的代理类 beanDefinition.setBeanClassName(ServiceBeanFactory.class.getName()); //这里就是工厂bean构造方法需要的参数 beanDefinition.getConstructorArgumentValues() .addGenericArgumentValue(beanDefinition.getBeanClassName()); } return beanDefinitionHolders; } }
这个类比较重要,主要就干了几件事:
- 把对应包下的接口全部扫描出来,包装成beanDefinition。
- 由于此时的beanDefinition里面的类是一个接口,spring没办法帮我们生成,所以我们需要用一个beanFactory代替beanDefinition里的类,也就是用bean工厂帮我们生成一个代理类给spring管理
- 添加实例化工厂需要的参数
工厂bean类,用jdk代理机制简单实现
public class ServiceBeanFactory<T> implements FactoryBean<T> { private Class<T> interFaceClazz; public ServiceBeanFactory(Class<T> interFaceClazz) { this.interFaceClazz = interFaceClazz; } @Override public T getObject() throws Exception { return (T) Proxy.newProxyInstance( interFaceClazz.getClassLoader(), new Class[]{interFaceClazz}, new ServiceInvocationHandler()); } @Override public Class<?> getObjectType() { return interFaceClazz; } } public class ServiceInvocationHandler implements InvocationHandler { /** * 这里就直接进行远程RPC调用了 */ @Override public Object invoke(Object proxy, Method method, Object[] args) { //指定URL String url = "http://localhost:8888/" + method.getName(); //发送get请求并接收响应数据 return HttpUtil.createGet(url).execute().body(); } }
因此,spring注入的时候实际上注入的是这个对接口的代理类,我们在执行doSomething()方法时,实际上会通过这个代理类向服务端发一个HTTP请求。
服务端
对于服务端来说,我只需要正常写一个controller处理一下来自客户端的请求就好了。这里就不过多演示
仓库地址
有需要的朋友也可以直接去我的仓库看相关代码。如有错漏,还请指正。github.com/NopeDl/ez-rpc
到此这篇关于基于HTTP协议实现简单RPC框架的方法详解的文章就介绍到这了,更多相关RPC框架内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!