fredal的博客

专业修电脑,副业写代码

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

RPC 558度 0评

我司目前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<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList<HandlerMethodReturnValueHandler>();

        // 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<Class<?>, Set<MethodTemplate>> 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<MethodTemplate> 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<Annotation> 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来开发微服务了,整个运行架构变成了下面这样:

2019云栖大会—互联网中间件
Comments
Write a Comment