【深入理解SpringCloud微服务】深入理解微服务中的远程调用,并手写一个微服务RPC框架
远程过程调用
远程过程调用简称RPC,也就是本地调用远程服务器上的方法。比如本地调用UserService的getUser(String userId)方法,经过网络通信,调用到远程服务器上的UserServiceImpl的getUser(String userId)方法。
一般的RPC框架,会屏蔽掉网络通信的细节,通过动态代理返回一个代理对象,我们调用这个代理对象,RPC框架就会帮我们向服务器发送RPC请求。
然后服务器那一端接收到RPC请求后,还要解析出接口类型、方法、参数等信息,然后调用对象的实现类进行处理。
如果一个接口有多个提供者提供服务,那么客户端的RPC框架还要处理负载均衡的逻辑。
上面说到的只是RPC中的服务调用流程,在此之前,还有服务暴露(或者叫服务注册)和服务引入(或者叫服务发现)两个过程。比如服务暴露就是把服务提供方的ip地址端口号,以及相关的接口信息发布到注册中心。而服务引入则是从注册中心把服务提供者发布上去的信息拉取到本地,根据该信息生成对应接口的代理对象。
以上这些,都是作为一个RPC框架需要实现的,可以看出来实现一个完整RPC框架还是比较复杂的。而我们作为使用者只要做相应的配置,既可以像调用本地方法一样调用远程服务。
微服务中的RPC框架
比起实现一个完整的RPC框架(比如Duboo),实现一个微服务中的RPC框架则要简单的多。由于微服务本身自带了注册中心和注册中心客户端等组件,服务暴露和服务引入就不需要RPC框架的实现者去考虑了。并且微服务还自带了负载均衡(比如Ribbon),RPC框架的实现者也可以复用,无需重复造轮子。
因此作为微服务中的RPC框架,只需要扫描接口生成代理对象,然后代理对象中复用微服务的负载均衡器即可。
如何实现一个微服务中的RPC框架
接口扫描
接口扫描这一步要做的事情就是扫描出所有需要生成代理对象的接口,这些接口在我们本地没有实现类,都是通过代理对象调用底层的远程调用逻辑请求到远程服务器上的接口实现类。
我们可以定义一个注解(比如OpenFeign的@FeignClient注解),注解需要指定服务名(表示这个接口是请求哪个微服务的),然后通过Spring的ClassPathBeanDefinitionScanner扫描我们自定义的注解修饰的接口,然后生成BeanDefinition,注册到Spring容器中。
生成代理对象
由于要根据扫描到的接口生成代理对象,因此扫描接口时生成的BeanDefinition的beanClass属性需要设置为Spring提供的FactoryBean类型。但是FactoryBean是一个接口,因此我们要实现自己的FactoryBean实现类,重写FactoryBean的getObject()方法,getObject()方法生成接口的代理对象,我们可以使用JDK动态代理。
代理对象处理逻辑
如果我们使用的是JDK动态代理,那么就会进入我们实现的InvocationHandler的handle()方法。
首先要解析修饰接口的自定义注解,获取注解上的服务名。
除服务名外,我们还要有请求的url,因此一般接口方法上也要有注解,我们们可以复用SpringMVC的注解(@RequestMapping、@GetMapping等),也可以自己重写定义一套。
获取到服务名后,然后要读取接口方法上的注解,解析注解上的url,从注解解析出来的url前面拼接上修饰接口的自定义注解的服务名,就形成了完成的请求地址。
得到请求地址后,我们复用负载均衡微服组件进行客户端负载均衡,然后通过RestTemplate或者OkHttp等其他的http工具发出http请求即可。
手写一个微服务RPC框架
@RPCClient
@RPCClient是自定义的用于修饰接口的注解,功能相当于OpenFegin的@FeignClient,用于被扫描并生成代理对象。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface RPCClient {
String serviceName() default "";
}
@EnableRPCClient
@EnableRPCClient注解也是自定义注解,功能与OpenFeign的@EnableFeignClients注解类型。用于修饰启动类,表示启动微服务RPC功能。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Import({MicroServiceRPCClientRegistrar.class})
public @interface EnableRPCClient {
String[] scanBasePackages() default {};
}
scanBasePackages属性指定了包扫描路径,@Import引入了一个MicroServiceRPCClientRegistrar,由MicroServiceRPCClientRegistrar进行包扫描并生成BeanDefinition。
MicroServiceRPCClientRegistrar
MicroServiceRPCClientRegistrar 实现了Spring的ImportBeanDefinitionRegistrar接口,并重写了registerBeanDefinitions方法。在registerBeanDefinitions方法中进行包扫描。
public class MicroServiceRPCClientRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware,
ResourceLoaderAware {
private Environment environment;
private ResourceLoader resourceLoader;
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 创建包扫描使用的scanner
ClassPathBeanDefinitionScanner scanner = new RPCClientBeanDefinitionScanner(registry, false, environment, resourceLoader);
// 设置扫描@RPCClient注解
scanner.addIncludeFilter(new AnnotationTypeFilter(RPCClient.class));
// 解析@EnableRPCClient注解,获取包扫描路径
Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
for (String packageToScan : packagesToScan) {
// 调用scan方法进行包扫描
scanner.scan(packageToScan);
}
}
public void setEnvironment(Environment environment) {
this.environment = environment;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
// 获取@EnableRPCClient注解中的属性
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(EnableRPCClient.class.getName()));
// 获取@EnableRPCClient注解属性中的包扫描路径scanBasePackages
String[] basePackages = attributes.getStringArray("scanBasePackages");
Set<String> packagesToScan = new LinkedHashSet<String>();
packagesToScan.addAll(Arrays.asList(basePackages));
return packagesToScan;
}
}
Spring会在处理@Import注解时会调用ImportBeanDefinitionRegistrar的registerBeanDefinitions()方法。
MicroServiceRPCClientRegistrar的registerBeanDefinitions()方法:
- 创建一个RPCClientBeanDefinitionScanner,它继承了Spring的包扫描器ClassPathBeanDefinitionScanner
- 解析@EnableRPCClient注解中的包扫描路径scanBasePackages
- 调用RPCClientBeanDefinitionScanner的scan方法进行包扫描
RPCClientBeanDefinitionScanner
RPCClientBeanDefinitionScanner是自定义的包扫描器,继承了Spring的包扫描器ClassPathBeanDefinitionScanner,我们重写doScan()方法,当调用scan()方法时,就会调用到我们重写的doScan()方法。
public class RPCClientBeanDefinitionScanner extends ClassPathBeanDefinitionScanner implements BeanClassLoaderAware {
private ClassLoader classLoader;
public RPCClientBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, Environment environment, ResourceLoader resourceLoader) {
super(registry, useDefaultFilters, environment, resourceLoader);
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// 只有接口才会被加载
return beanDefinition.getMetadata().isInterface();
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 调用父类ClassPathBeanDefinitionScanner的doScan()方法进行扫描
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
// 处理扫描结果
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
ScannedGenericBeanDefinition beanDefinition = (ScannedGenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
String beanClassName = beanDefinition.getBeanClassName();
Class<?> beanClass = ClassUtils.resolveClassName(beanClassName, classLoader);
// 修改beanDefinition 的beanClass属性为RPCClientFactoryBean
beanDefinition.setBeanClass(RPCClientFactoryBean.class);
// 设置RPCClientFactoryBean中的type字段值为接口的类型
beanDefinition.getPropertyValues().addPropertyValue("type", beanClass);
// 设置自动装配类型为byType
beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
}
return beanDefinitionHolders;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
}
首先调用父类ClassPathBeanDefinitionScanner的doScan()方法进行扫描,获取扫描到的BeanDefinition集合。
然后处理扫描到的BeanDefinition集合:
- 修改BeanDefinition的beanClass属性为RPCClientFactoryBean,RPCClientFactoryBean实现了FactoryBean接口,后续Spring实例化Bean时会调用RPCClientFactoryBean的getObject()方法。我们会在getObject()方法方法中进行动态代理
- 设置RPCClientFactoryBean中的type字段值为接口的类型,type是动态代理时指定的接口类型
- 设置自动装配类型为byType
RPCClientFactoryBean
RPCClientFactoryBean实现了Spring的FactoryBean接口,并重写了getObject()方法。Spring在实例化Bean时,如果BeanDefinition的beanClass属性是FactoryBean类型的实现类,会调用getObject()方法进行实例化。我们在RPCClientFactoryBean的getObject()方法中使用JDK动态代理生成代理对象。
public class RPCClientFactoryBean<T> implements FactoryBean<T> {
@Autowired
private LoadBalanceClient loadBalanceClient;
private Class<T> type;
public T getObject() throws Exception {
// JDK动态代理生成代理对象
// InvocationHandler是RPCInvocationHandler
// RPCInvocationHandler中持有LoadBalanceClient,用于做负载均衡
return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{type}, new RPCInvocationHandler(type, loadBalanceClient));
}
...
}
RPCInvocationHandler
当我们调用代理对象的接口方法时,就会进入RPCInvocationHandler的invoke()方法。invoke()方法会进行url拼接、负载均衡重写url、收集方法参数、OkHttp发送请求、解析返回结果等处理。
由于我们这里直接服用了SpringMVC的注解,因此会涉及到很多SpringMVC的注解比如@RequestMapping、@GetMapping等的解析操作。
public class RPCInvocationHandler<T> implements InvocationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RPCInvocationHandler.class);
private Class<T> type;
private LoadBalanceClient loadBalanceClient;
public RPCInvocationHandler(Class<T> type, LoadBalanceClient loadBalanceClient) {
this.type = type;
this.loadBalanceClient = loadBalanceClient;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 处理接口上的@RPCClient注解,得到服务名serviceName
RPCClient rpcClient = type.getAnnotation(RPCClient.class);
String serviceName = rpcClient.serviceName();
// 拼接请求url用的StringBuilder
StringBuilder urlBuilder = new StringBuilder();
// http://拼接上服务名
urlBuilder.append("http://").append(serviceName);
// 处理接口上的@RequestMapping注解,得到@RequestMapping注解中的url,然后拼接到urlBuilder上
RequestMapping requestMapping = type.getAnnotation(RequestMapping.class);
appendUrlByRequestMapping(urlBuilder, requestMapping);
// 处理方法上的@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping注解
// 也是获取注解上的url进行拼接
// 此外还会解析出RequestMethod
RequestMethod requestMethod = null;
if (method.getAnnotation(RequestMapping.class) != null) {
requestMapping = method.getAnnotation(RequestMapping.class);
appendUrlByRequestMapping(urlBuilder, requestMapping);
RequestMethod[] methods = requestMapping.method();
if (methods != null && methods.length > 0) {
requestMethod = methods[0];
}
} else if (method.getAnnotation(GetMapping.class) != null) {
...
} else if (method.getAnnotation(PostMapping.class) != null) {
...
} else if (method.getAnnotation(PutMapping.class) != null) {
...
} else if (method.getAnnotation(DeleteMapping.class) != null) {
...
}
// 完整的请求url(负载均衡重写前)
String requestUrl = urlBuilder.toString().trim();
// 调用LoadBalanceClient进行客户端负载均衡,重写url,
// LoadBalanceClient是我们的微服务负载均衡组件,这里直接服用
requestUrl = loadBalanceClient.reconstructUrl(serviceName, requestUrl);
// 处理参数,注解包括:@PathVariable、@RequestParam、@RequestBody
Map<String, Object> requestParamMap = new HashMap<>();
...
// 适用OkHttp发送请求
OkHttpClient okHttpClient=new OkHttpClient();
...
// 处理返回结果
okhttp3.ResponseBody responseBody = null;
...
}
private void appendUrl(StringBuilder urlBuilder, String[] paths) {
if (paths.length > 0) {
String path = paths[0];
if (!path.startsWith("/")) {
urlBuilder.append("/");
}
urlBuilder.append(path);
}
}
private void appendUrlByRequestMapping(StringBuilder urlBuilder, RequestMapping requestMapping) {
if (requestMapping != null) {
String[] paths = requestMapping.value();
appendUrl(urlBuilder, paths);
}
}
}
我们使用一个StringBuilder进行url拼接,首先是获取到@RPCClient上的服务名,http://加上服务名作为url的开头拼接到StringBuilder上;然后解析接口上的@RequestMapping注解得到注解上的url,拼接到StringBuilder上;然后是解析方法上的注解@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping等,解析得到注解上的url,同样的拼接到StringBuilder上。
然后调用LoadBalanceClient进行负载均衡操作,重写url,LoadBalanceClient是直接复用了我们的微服务负载均衡组件。
然后就是收集方法参数,使用OkHttp发送请求,处理返回结果。都是一些简单但是繁琐的代码,因此直接省略掉了。
代码已上传至https://gitee.com/huang_junyi/simple-microservice,如果想看详细代码的话可以到上面去下载。