Our company’s current RPC framework is developed based on Java Rest, and the form can refer to the implementation of SpringCloud Feign. With the rise of microservices architecture, Spring MVC has almost become the Rest development specification, and the threshold for Spring users is relatively low.

REST vs. RPC-style development

A simple implementation of the RPC framework in a feign-like manner is as follows:

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

The service provider uses Spring MVC directly to expose the service interface:

@RestControllerpublic class HelloController {    @Autowired    private HelloService helloService;    @GetMapping("/message")    public HelloMessage getMessage(@RequestParam(name="name")String name) {        HelloMessage hello = helloService.gen(name);        return hello;    }}
Copy the code

The REST-style approach to development has many advantages. First, the barrier to use is low. The server side is completely based on Spring MVC, and the writing way of the client API is compatible with most Spring annotations, including @requestParam, @RequestBody, etc. The second is the decoupling feature. Microservice applications focus on service autonomy and provide loosely-coupled REST interfaces externally. This approach is more flexible and can reduce the pain points brought by historical burden.

Of course, this approach also brings a lot of trouble in practice. First, inconsistent client and server apis introduce the possibility of errors, where the return value type of the Controller interface and the return value type of the RpcClient may be written inconsistently, resulting in deserialization failures. Secondly, although RpcClient writing is compatible with Spring annotations, there is still a large threshold for some developers. For example, @requestParam annotations are often forgotten when writing URL param. @requestBody is forgotten when writing body param, @requestBody is used to annotate String parameters, method types are not specified, etc. (basically the same threshold as using Feign).

Also, the REST approach is equivalent to writing an extra layer of Controller than the common RPC approach, rather than directly exposing the Service as an interface. In DDD practice, when splitting a monolithic application into bounded contexts, the Service method of the old code is often split. The REST style means that more controllers need to be written to access the presentation layer, and in the case of internal microservice applications calling each other, Exposing the application service layer or even the domain service layer to the caller may be an easier way to satisfy DDD while being more consistent with RPC semantics.

We wanted to develop microservices elegantly and easily through a transparent RPC-style development approach.

First of all, we want the service interface to be defined more easily without unnecessary comments and information:

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

We can then implement the service and simply publish it with annotations:

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

This allows the client to invoke the Hello () method directly into the HelloImpl implementation class on the server side to obtain a HelloMessage object. Improved simplicity and consistency over previous REST implementations.

Implicit service contracts

A service contract is a description of an interface between a client and a server. In reST-style development, we use Spring MVC annotations to declare the interface’s request and return parameters. However, in the transparent RPC development approach, theoretically we can not write any RESTful annotation, then how to define the service contract.

In fact, implicit service contracts can be used here. Instead of defining contracts and interfaces in advance, implementation classes can be directly defined, according to which default contracts can be automatically generated and registered to the service center.

The default service contract content includes method type selection, URL address, and parameter annotation processing. The method type is determined based on the type of the input parameter. If the input type contains a custom type, Object, or collection suitable for the Body, the POST method is judged to be used. If the input parameter has only a String or primitive type, the GET method is judged to be used. The POST method passes all the parameters as the Body, while the GET method passes the parameters as the URL PARAM. The default rule for URL addresses is/class name/method type + method name. Unannotated methods are registered with the service center based on this URL.

REST programming model on the server side

We can see that the biggest change in the two development styles is the change in the server-side programming model from the REST-style SpringMVC programming model to the transparent RPC programming model. How can we achieve this?

Our current operating architecture is shown in the figure above, with the programming model on the server side completely based on Spring MVC and the communication model based on servlets. We expect that the programming model of the server side can be converted to RPC, so it is inevitable that we need to make some changes to the communication model.

From the DispatcherServlet

So first, we need to do some understanding of Spring MVC servlet specification DispatcherServlet, know how it handles a request.

The DispatcherServlet mainly contains three parts of logic, namely, HandlerMapping, HandlerAdapter and ViewResolver. The DispatcherServlet finds the appropriate Handler through HandlerMapping, ADAPTS through HandlerAdapter, and finally returns the ModelAndView to the front end through ViewResolver processing.

Back to the topic, there are two ways to transform this part of the communication model so as to realize the PROGRAMMING model of RPC. One is to write a new Servlet directly to achieve the effect of REST over Servlet, so as to get a complete control over the server side communication logic. This allows us to add custom runtime models for the server (server flow limiting, call chain handling, and so on). The other is to modify only part of the HandlerMapping code to make the request mapping adaptable to the RPC programming model.

In view of workload and realistic conditions, we choose the latter method, continue to use DispatcherServlet, but modify part of HandlerMapping code.

  1. First of all, we will scan the interface annotated with @rpCClient annotation and its implementation class through Scanner. We will register it in HandlerMapping, so first we need to see if there is any place in HandlerMapping that can extend the registration logic.

  2. Next we need to think about handling requests. We need HandlerMapping to be able to select different argumentResolver argument handlers for different arguments without Spring Annotation. This is done in springMVC by annotating annotations (RequestMapping, RequestBody, etc.), so we need to see if HandlerMapping extends the parameter annotation logic.

With these two goals in mind, let’s first look at the logic of HandlerMapping.

Initialization of HandlerMapping

The HandlerMapping initialization source code is quite long, so we’ll skip over some of the less important parts. First the RequestMappingHandlerMapping the superclass AbstractHandlerMethodMapping class implements the InitializingBean interface, After the property is initialized, the afterPropertiesSet() method is called, where initHandlerMethods() is called to initialize the HandlerMethod. DetectHandlerMethods is used in the InitHandlerMethods method to find handlerMethod from the bean based on the bean name. RegisterHandlerMethod is called in this method to register a normal handlerMethod.

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

We found that this method is protected, so the first step we found where the registered RPC method into the RequestMappingHandlerMapping. The interface can see that the incoming parameter is a handler method, but the actual handlerMethod object is registered in handlerMapping, obviously this part of the logic is in the Register method of mappingRegistry. In the register method we find the key method for conversion:

HandlerMethod handlerMethod = createHandlerMethod(handler, method);
Copy the code

The constructor of the handlerMethod object is called in this method to construct a handlerMethod object. The property of the handlerMethod contains an array of methodParameter objects called Parameters. We know that the handlerMethod object corresponds to an implementation method, so the methodParameter object corresponds to an input parameter. And then we look inside the methodParameter object, and we see an Annotation array called parameterAnnotations, which looks like the second thing we need to look at. So to summarize, the initialization of handlerMapping looks like this, filtering out the unnecessary parts:

Request processing by the HandlerAdapter

The dispatcherServlet actually uses the handlerAdapter to process the request and then returns the ModelAndView object, but all related objects are registered in the handlerMapping. We directly to see RequestMappingHandlerAdapter processing logic, handlerAdapter call handleInternal method in the handle method, and call the invokeHandlerMethod method, This method used in createInvocableHandlerMethod method will handlerMethod packaging has become a servletInvocableHandlerMethod object, This object ends up calling the invokeAndHandle method to process the corresponding request logic. We focus only on the invokeForRequest method inside invokeAndHandle, which is our goal as handling of the input parameter. Finally we see the logic for the input annotation in the getMethodArgumentValues method of this method:

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; }}Copy the code

Obviously, the supportsParameter method is used as the argumentResolver, and the underlying logic is simply to walk through the argument handlers that actually support the input arguments. Actually RequestMappingHandlerAdapte at initialization time registered just a heap of parameters of the processor:

	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));...}
Copy the code

We adjust a familiar RequestResponseBodyMethodProcessor to see its supportsParameter methods:

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

So we’re calling hasParameterAnnotation, the public method on MethodParameter itself, to see if there’s an annotation, Such as the RequestBody comments then we will choose RequestResponseBodyMethodProcessor processor as its parameters.

Again filtering out useless logic, the whole process is as follows:

RPC programming model on the server side

Above we know that DispatcherServlet is part of logic in REST programming model, now we modify part of HandlerMapping code to adapt to RPC programming model according to the above mentioned.

RPC method registration

First we need to register the method to handlerMapping, which is known from the initialization of RequestHandlerMapping above by calling the registerHandlerMethod method directly. Combined with our scanning logic, the general code is as follows:

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; 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; / / the implicit contract: method type and 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(); / / registered to spring MVC enclosing registerHandlerMethod (handler, method, requestMappingInfo); }}}}Copy the code

We have custom registration methods that only need to be called when the container is started.

RPC request Processing

We need to do something with the input annotation. For example, we didn’t write the @requestBody User annotation, but we still want the handlerAdapter to think we did. Using RequestResponseBodyMethodProcessor parser for processing parameters.

We rewrite the RequestMappingHandlerMapping createHandlerMethod method directly:

@Overrideprotected 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); }Copy the code

We define our own HandlerMethod object:

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); }}}Copy the code

As you can easily see, the point here is to initialize the custom MethodParameter object:

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(); }}Copy the code

The custom MethodParameter object overrides the getParameterAnnotations method, which argumentResolver uses to determine its suitability for the parameter. We’ve tweaked it so that the appropriate parameters will be “mistakenly” annotated by the appropriate parameter parser, which will do the normal argument processing logic itself. The whole process is as follows, and the pink part is the point we extended:

RPC programming model

After the transformation, we were able to implement the transparent RPC described at the beginning of this article to develop microservices, and the entire operating architecture became the following:

** More attention to the public account: JAVA architecture advanced road, reply ‘ali’ to obtain Ali interviewer manual **