使用基于 SpringMVC 的透明 RPC 开发微服务

JAVA葵花宝典

共 8493字,需浏览 17分钟

 ·

2020-12-31 18:45

来源:fredal.xin/develop-with-transparent-rpc

我司目前 RPC 框架是基于 Java Rest 的方式开发的,形式上可以参考 SpringCloud Feign 的实现。Rest 风格随着微服务的架构兴起,Spring MVC 几乎成为了 Rest 开发的规范,同时对于 Spring 的使用者门槛也比较低。

REST 与 RPC 风格的开发方式

RPC 框架采用类 Feign 方式的一个简单的实现例子如下:

@RpcClient(schemaId="hello")
public interface Hello {
    @GetMapping("/message")
    HelloMessage hello(@RequestParam String name);
}

而服务提供者直接使用 spring mvc 来暴露服务接口:

@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    @GetMapping("/message")
    public HelloMessage getMessage(@RequestParam(name="name")String name) {
        HelloMessage hello = helloService.gen(name);
        return hello;
    }
}

基于 REST 风格开发的方式有很多优点。一是使用门槛较低,服务端完全基于 Spring MVC,客户端 api 的书写方式也兼容了大部分 Spring 的注解,包括@RequestParam、@RequestBody 等。二是带来的解耦特性,微服务应用注重服务自治,对外则提供松耦合的 REST 接口,这种方式更灵活,可以减轻历史包袱带来的痛点,同时除了提供给类 SDK 的消费者服务外,还可提供浏览器等非 SDK 的消费者服务。

当然这种方式在实际运用中也带来了很多麻烦。首先,不一致的客户端与服务端 API 带来了出错的可能性,Controller 接口的返回值类型与 RpcClient 的返回值类型可能写的不一致从而导致反序列化失败。其次,RpcClient 的书写虽然兼容了 Spring 的注解,但对于某些开发同学仍然存在不小的门槛,例如写 url param 时@RequestParam 注解常常忘写,写 body param 时候@RequestBody 注解忘记写,用@RequestBody 注解来标注 String 参数,方法类型不指定等等(基本上和使用 Feign 的门槛一样)。

还有一点,就是比起常见的 RPC 方式,REST 方式相当于多写了一层 Controller,而不是直接将 Service 暴露成接口。DDD 实践中,将一个巨石应用拆分成各个限界上下文时,往往是对旧代码的 Service 方法进行拆分,REST 风格意味着需要多写 Controller 接入表示层,而在内部微服务应用间相互调用的场景下,暴露应用服务层甚至领域服务层给调用者可能是更简便的方法,在满足 DDD 的同时更符合 RPC 的语义。

那么我们希望能通过一种基于透明 RPC 风格的开发方式来优雅简便地开发微服务。

首先我们希望服务接口的定义能更简便,不用写多余的注解和信息:

@RpcClient(schemaId="hello")
public interface Hello {
        HelloMessage hello(String name);
}

然后我们就可以实现这个服务,并通过使用注解的方式简单的发布服务:

@RpcService(schemaId="hello")
public class HelloImpl implements Hello{
        @Override
        HelloMessage hello(String name){
            return new HelloMessage(name);
        }
}

这样客户端在引用 Hello 接口后可以直接使用里面的 hello()方法调用到服务端的实现类 HelloImpl 中,从而获得一个 HelloMessage 对象。相比之前的 REST 实现方式,在简洁性以及一致性上都得到了提升。

隐式的服务契约

服务契约指客户端与服务端之间对于接口的描述定义。REST 风格开发方式中,我们使用 Spring MVC annotation 来声明接口的请求、返回参数。但是在透明 RPC 开发方式中,理论上我们可以不用写任何 RESTful 的 annotation 的,这时候怎么去定义服务契约呢。

其实这里运用了隐式的服务契约,可以不事先定义契约和接口,而是直接定义实现类,根据实现类去自动生成默认的契约,注册到服务中心。

默认的服务契约内容包括方法类型的选择、URL 地址以及参数注解的处理。方法类型的判断基于入参类型,如果入参类型中包含自定义类型、Object 或者集合等适合放在 Body 中的类型,则会判断为使用 POST 方法,而如果入参仅有 String 或者基本类型等,则判断使用 GET 方法。POST 方法会将所有参数作为 Body 进行传送,而 GET 方法则将参数作为 URL PARAM 进行传送。URL 地址的默认规则为/类名/方法类型+方法名,未被注解的方法都会按此 URL 注册到服务中心。

服务端的 REST 编程模型

我们可以发现,两种开发风格最大的改变是服务端编程模型的改变,从 REST 风格的 SpringMVC 编程模型变成了透明 RPC 编程模型。我们应该怎样去实现这一步呢?

我们目前的运行架构如上图,服务端的编程模型完全基于 Spring MVC,通信模型则是基于 servlet 的。我们期望服务端的编程模型可以转换为 RPC,那么势必需要我们对通信模型做一定的改造。

从 DispatcherServlet 说起

那么首先,我们需要对 Spring MVC 实现的 servlet 规范 DispatcherServlet 做一定的了解,知道它是怎么处理一个请求的。

DispatcherServlet 主要包含三部分逻辑,映射处理器(HandlerMapping),映射适配器(HandlerAdapter),视图处理器(ViewResolver)。DispatcherServlet 通过 HandlerMapping 找到合适的 Handler,再通过 HandlerAdapter 进行适配,最终返回 ModelAndView 经由 ViewResolver 处理返回给前端。

回到主题上,我们想要改造这部分通信模型从而能够实现 RPC 的编程模型有两种办法,一是直接编写一个新的 Servlet,实现 REST over Servlet 的效果,从而对服务端通信逻辑得到一个完整的控制,这样我们可以为服务端添加自定义的运行模型(服务端限流、调用链处理等)。二是仅仅修改一部分 HandlerMapping 的代码,将请求映射变得可以适配 RPC 的编程模型。

鉴于工作量与现实条件,我们选择后一种方法,继续沿用 DispatcherServlet,但改造部分 HandlerMapping 的代码。

  1. 首先我们会通过 Scanner 扫描到标注了@RpcClient 注解的接口以及其实现类,我们会将其注册到 HandlerMapping 中,所以首先我们要看 HandlerMapping 中有没有能扩展注册逻辑的地方。

  2. 接着我们再考虑处理请求的事儿,我们需要 HandlerMapping 能够做到在没有 Spring Annotation 的情况下也能为不同的参数选择不同的 argumentResolver 参数处理器,这一点在 springMVC 中是通过标注注解来区分的(RequestMapping、RequestBody 等),所以我们还需要看看 HandlerMapping 中有没有能扩展参数注解逻辑的地方。

带着这两点目的,我们先来看 HandlerMapping 的逻辑。

HandlerMapping 的初始化

HandlerMapping 的初始化源码比较长,我们直接一笔略过不是很重要的部分了。首先 RequestMappingHandlerMapping 的父类 AbstractHandlerMethodMapping 类实现了 InitializingBean 接口,在属性初始化完成后会调用 afterPropertiesSet()方法,在该方法中调用 initHandlerMethods()进行 HandlerMethod 初始化。InitHandlerMethods 方法中使用 detectHandlerMethods 方法从 bean 中根据 bean name 查找 handlerMethod,此方法中调用 registerHandlerMethod 来注册正常的 handlerMethod。

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
  this.mappingRegistry.register(mapping, handler, method);
 }

我们发现这个方法是 protected 的,那么第一步我们找到了去哪注册我们的 RPC 方法到 RequestMappingHandlerMapping 中。接口可以看到入参是 handler 方法,但在 handlerMapping 中真正被注册的 handlerMethod 对象,显然这部分逻辑在 mappingRegistry 的 register 方法中。register 方法中我们找到了转换的关键方法:

HandlerMethod handlerMethod = createHandlerMethod(handler, method);

此方法中调用了 handlerMethod 对象的构造器来构造一个 handlerMethod
对象。handlerMethod 的属性中包含一个叫 parameters 的 methodParameter 对象数组。我们知道 handlerMethod 对象对应的是一个实现方法,那么 methodParameter 对象对应的就是入参了。

接着往 methodParameter 对象里看,发现了一个叫 parameterAnnotations 的 Annotation 数组,看样子这就是我们第二个需要关注的地方了。那么总结一下,滤去无需关注的部分,handlerMapping 的初始化整个如下图所示:

HandlerAdapter 的请求处理

这边 dispatcherServlet 在真正处理请求的时候是用 handlerAdapter 去处理再返回 ModelAndView 对象的,但是所有相关对象都是注册在 handlerMapping 中。

我们直接来看看 RequestMappingHandlerAdapter 的处理逻辑吧,handlerAdapter 在 handle 方法中调用 handleInternal 方法,并调用 invokeHandlerMethod 方法,此方法中使用 createInvocableHandlerMethod 方法将 handlerMethod 对象包装成了一个 servletInvocableHandlerMethod 对象,此对象最终调用 invokeAndHandle 方法完成对应请求逻辑的处理。我们只关注 invokeAndHandle 里面的 invokeForRequest 方法,该方法作为对入参的处理正是我们的目标。最终我们看到了此方法中的 getMethodArgumentValues 方法中的一段对入参注解的处理逻辑:

    if (this.argumentResolvers.supportsParameter(parameter)) {
                    try {
                        args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                    } catch (Exception var9) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug(this.getArgumentResolutionErrorMessage("Error resolving argument", i), var9);
                        }

                        throw var9;
                    }
                }

显然,这里使用 supportsParameter 方法来作为判断依据选择 argumentResolver,里层的逻辑就是一个简单的遍历选择真正支持入参的参数处理器。实际上 RequestMappingHandlerAdapte 在初始化时候就注册了一堆参数处理器:

 private List getDefaultReturnValueHandlers() {
  List handlers = new ArrayList();

  // Single-purpose return value types
  handlers.add(new ModelAndViewMethodReturnValueHandler());
  handlers.add(new ModelMethodProcessor());
  handlers.add(new ViewMethodReturnValueHandler());
  handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
  handlers.add(new StreamingResponseBodyReturnValueHandler());
  handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
    this.contentNegotiationManager, this.requestResponseBodyAdvice));
  handlers.add(new HttpHeadersReturnValueHandler());
  handlers.add(new CallableMethodReturnValueHandler());
  handlers.add(new DeferredResultMethodReturnValueHandler());
  handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
...
}

我们调个眼熟的 RequestResponseBodyMethodProcessor 来看看其 supportsParameter 方法:

@Override
 public boolean supportsParameter(MethodParameter parameter) {
  return parameter.hasParameterAnnotation(RequestBody.class);
 }

这里直接调用了 MethodParameter 自身的 public 方法 hasParameterAnnotation 方法来判断是否有相应的注解,比如有 RequestBody 注解那么我们就选用 RequestResponseBodyMethodProcessor 来作为其参数处理器。

还是滤去无用逻辑,整个流程如下:

服务端的 RPC 编程模型

以上我们了解了 DispatcherServlet 在 REST 编程模型中是部分逻辑,现在我们依据之前讲的改造部分 HandlerMapping 的代码从而使其适配 RPC 编程模型。

RPC 方法注册

首先我们需要将方法注册到 handlerMapping,而这点由上述 RequestHandlerMapping 的初始化流程得知直接调用 registerHandlerMethod 方法即可。结合我们的扫描逻辑,大致代码如下:

public class RpcRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
     public void registerRpcToMvc(final String prefix) {
        final AdvancedApiToMvcScanner scanner = new AdvancedApiToMvcScanner(
                RpcService.class);
        scanner.setBasePackage(basePackage);
        Map, Set> mvcMap;
        //扫描到注解了@RpcService的接口及method元信息
        try {
            mvcMap = scanner.scan();
        } catch (final IOException e) {
            throw new FatalBeanException("failed to scan");
        }
        for (final Class clazz : mvcMap.keySet()) {
            final Set methodTemplates = mvcMap.get(clazz);
            for (final MethodTemplate methodTemplate : methodTemplates) {
                if (methodTemplate == null) {
                    continue;
                }
                final Method method = methodTemplate.getMethod();
                Http.HttpMethod httpMethod;
                String uriTemplate = null;
                //隐式契约:方法类型和url地址
                httpMethod = MvcFuncUtil.judgeMethodType(method);
                uriTemplate = MvcFuncUtil.genMvcFuncName(clazz, httpMethod.name(), method);

                final RequestMappingInfo requestMappingInfo = RequestMappingInfo
                        .paths(this.resolveEmbeddedValuesInPatterns(new String[]{uriTemplate}))
                        .methods(RequestMethod.valueOf(httpMethod.name()))
                        .build();

                //注册到spring mvc
                this.registerHandlerMethod(handler, method, requestMappingInfo);
            }
        }
    }
}

我们自定义了注册方法,只需在容器启动时调用即可。

RPC 请求处理

以上所说,光完成注册是不够的,我们需要对入参注解做一些处理,例如我们虽然没有写注解@RequestBody User user,我们仍然希望 handlerAdapter 在处理的时候能够以为我们写了,并用 RequestResponseBodyMethodProcessor 参数解析器来进行处理。

我们直接重写 RequestMappingHandlerMapping 的 createHandlerMethod 方法:

@Override
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
    HandlerMethod handlerMethod;
    if (handler instanceof String) {
        String beanName = (String) handler;
        handlerMethod = new HandlerMethod(beanName, this.getApplicationContext().getAutowireCapableBeanFactory(), method);
    } else {
        handlerMethod = new HandlerMethod(handler, method);
    }
    return new RpcHandlerMethod(handlerMethod);
}

我们自定义了自己的 HandlerMethod 对象:

public class RpcHandlerMethod extends HandlerMethod {

    protected RpcHandlerMethod(HandlerMethod handlerMethod) {
        super(handlerMethod);
        initMethodParameters();
    }

    private void initMethodParameters() {
        MethodParameter[] methodParameters = super.getMethodParameters();
        Annotation[][] parameterAnnotations = null;
        for (int i = 0; i < methodParameters.length; i++) {
            SynthesizingMethodParameter methodParameter = (SynthesizingMethodParameter) methodParameters[i];
            methodParameters[i] = new RpcMethodParameter(methodParameter);
        }
    }
}

很容易看到,这里的重点是初始化了自定义的 MethodParameter 对象:

public class RpcMethodParameter extends SynthesizingMethodParameter {

    private volatile Annotation[] annotations;

    protected RpcMethodParameter(SynthesizingMethodParameter original) {
        super(original);
        this.annotations = initParameterAnnotations();
    }

    private Annotation[] initParameterAnnotations() {
        List annotationList = new ArrayList<>();
        final Class parameterType = this.getParameterType();
        if (MvcFuncUtil.isRequestParamClass(parameterType)) {
            annotationList.add(MvcFuncUtil.newRequestParam(MvcFuncUtil.genMvcParamName(this.getParameterIndex())));
        } else if (MvcFuncUtil.isRequestBodyClass(parameterType)) {
            annotationList.add(MvcFuncUtil.newRequestBody());
        }
        return annotationList.toArray(new Annotation[]{});
    }

    @Override
    public Annotation[] getParameterAnnotations() {
        if (annotations != null && annotations.length > 0) {
            return annotations;
        }
        return super.getParameterAnnotations();
    }
}

自定义的 MethodParameter 对象中重写了 getParameterAnnotations 方法,而次方法正是 argumentResolver 用来判断自己是否适合该参数的方法。我们做了些改造使得合适的参数会被合适的参数解析器"误以为"加了对应的注解,从而自己会去进行正常的参数处理逻辑。整个处理流程如下,粉红色部分也正是我们所扩展的点了:

RPC 编程模型

经过改造之后,我们已经可以实现文章开头所描述的透明 RPC 来开发微服务了,整个运行架构变成了下面这样:

END

推荐阅读

GitHub 下载神器强势回归!

巧用枚举来干掉if-else,代码更优雅!

如何正确访问Redis中的海量数据?服务才不会挂掉!

超硬核!1.6W 字 Redis 面试知识点总结,建议收藏!

浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报