<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          使用基于 SpringMVC 的透明 RPC 開發(fā)微服務(wù)

          共 8608字,需瀏覽 18分鐘

           ·

          2020-12-25 09:17

          走過路過不要錯過

          點擊藍(lán)字關(guān)注我們


          我司目前 RPC 框架是基于 Java Rest 的方式開發(fā)的,形式上可以參考 SpringCloud Feign 的實現(xiàn)。Rest 風(fēng)格隨著微服務(wù)的架構(gòu)興起,Spring MVC 幾乎成為了 Rest 開發(fā)的規(guī)范,同時對于 Spring 的使用者門檻也比較低。

          REST 與 RPC 風(fēng)格的開發(fā)方式

          RPC 框架采用類 Feign 方式的一個簡單的實現(xiàn)例子如下:

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

          而服務(wù)提供者直接使用 spring mvc 來暴露服務(wù)接口:

          @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 風(fēng)格開發(fā)的方式有很多優(yōu)點。一是使用門檻較低,服務(wù)端完全基于 Spring MVC,客戶端 api 的書寫方式也兼容了大部分 Spring 的注解,包括@RequestParam、@RequestBody 等。二是帶來的解耦特性,微服務(wù)應(yīng)用注重服務(wù)自治,對外則提供松耦合的 REST 接口,這種方式更靈活,可以減輕歷史包袱帶來的痛點,同時除了提供給類 SDK 的消費者服務(wù)外,還可提供瀏覽器等非 SDK 的消費者服務(wù)。

          當(dāng)然這種方式在實際運用中也帶來了很多麻煩。首先,不一致的客戶端與服務(wù)端 API 帶來了出錯的可能性,Controller 接口的返回值類型與 RpcClient 的返回值類型可能寫的不一致從而導(dǎo)致反序列化失敗。其次,RpcClient 的書寫雖然兼容了 Spring 的注解,但對于某些開發(fā)同學(xué)仍然存在不小的門檻,例如寫 url param 時@RequestParam 注解常常忘寫,寫 body param 時候@RequestBody 注解忘記寫,用@RequestBody 注解來標(biāo)注 String 參數(shù),方法類型不指定等等(基本上和使用 Feign 的門檻一樣)。

          還有一點,就是比起常見的 RPC 方式,REST 方式相當(dāng)于多寫了一層 Controller,而不是直接將 Service 暴露成接口。DDD 實踐中,將一個巨石應(yīng)用拆分成各個限界上下文時,往往是對舊代碼的 Service 方法進(jìn)行拆分,REST 風(fēng)格意味著需要多寫 Controller 接入表示層,而在內(nèi)部微服務(wù)應(yīng)用間相互調(diào)用的場景下,暴露應(yīng)用服務(wù)層甚至領(lǐng)域服務(wù)層給調(diào)用者可能是更簡便的方法,在滿足 DDD 的同時更符合 RPC 的語義。

          那么我們希望能通過一種基于透明 RPC 風(fēng)格的開發(fā)方式來優(yōu)雅簡便地開發(fā)微服務(wù)。

          首先我們希望服務(wù)接口的定義能更簡便,不用寫多余的注解和信息:

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

          然后我們就可以實現(xiàn)這個服務(wù),并通過使用注解的方式簡單的發(fā)布服務(wù):

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

          這樣客戶端在引用 Hello 接口后可以直接使用里面的 hello()方法調(diào)用到服務(wù)端的實現(xiàn)類 HelloImpl 中,從而獲得一個 HelloMessage 對象。相比之前的 REST 實現(xiàn)方式,在簡潔性以及一致性上都得到了提升。

          隱式的服務(wù)契約

          服務(wù)契約指客戶端與服務(wù)端之間對于接口的描述定義。REST 風(fēng)格開發(fā)方式中,我們使用 Spring MVC annotation 來聲明接口的請求、返回參數(shù)。但是在透明 RPC 開發(fā)方式中,理論上我們可以不用寫任何 RESTful 的 annotation 的,這時候怎么去定義服務(wù)契約呢。

          其實這里運用了隱式的服務(wù)契約,可以不事先定義契約和接口,而是直接定義實現(xiàn)類,根據(jù)實現(xiàn)類去自動生成默認(rèn)的契約,注冊到服務(wù)中心。

          默認(rèn)的服務(wù)契約內(nèi)容包括方法類型的選擇、URL 地址以及參數(shù)注解的處理。方法類型的判斷基于入?yún)㈩愋?,如果入?yún)㈩愋椭邪远x類型、Object 或者集合等適合放在 Body 中的類型,則會判斷為使用 POST 方法,而如果入?yún)H有 String 或者基本類型等,則判斷使用 GET 方法。POST 方法會將所有參數(shù)作為 Body 進(jìn)行傳送,而 GET 方法則將參數(shù)作為 URL PARAM 進(jìn)行傳送。URL 地址的默認(rèn)規(guī)則為/類名/方法類型+方法名,未被注解的方法都會按此 URL 注冊到服務(wù)中心。

          服務(wù)端的 REST 編程模型

          我們可以發(fā)現(xiàn),兩種開發(fā)風(fēng)格最大的改變是服務(wù)端編程模型的改變,從 REST 風(fēng)格的 SpringMVC 編程模型變成了透明 RPC 編程模型。我們應(yīng)該怎樣去實現(xiàn)這一步呢?

          我們目前的運行架構(gòu)如上圖,服務(wù)端的編程模型完全基于 Spring MVC,通信模型則是基于 servlet 的。我們期望服務(wù)端的編程模型可以轉(zhuǎn)換為 RPC,那么勢必需要我們對通信模型做一定的改造。

          從 DispatcherServlet 說起

          那么首先,我們需要對 Spring MVC 實現(xiàn)的 servlet 規(guī)范 DispatcherServlet 做一定的了解,知道它是怎么處理一個請求的。

          DispatcherServlet 主要包含三部分邏輯,映射處理器(HandlerMapping),映射適配器(HandlerAdapter),視圖處理器(ViewResolver)。DispatcherServlet 通過 HandlerMapping 找到合適的 Handler,再通過 HandlerAdapter 進(jìn)行適配,最終返回 ModelAndView 經(jīng)由 ViewResolver 處理返回給前端。

          回到主題上,我們想要改造這部分通信模型從而能夠?qū)崿F(xiàn) RPC 的編程模型有兩種辦法,一是直接編寫一個新的 Servlet,實現(xiàn) REST over Servlet 的效果,從而對服務(wù)端通信邏輯得到一個完整的控制,這樣我們可以為服務(wù)端添加自定義的運行模型(服務(wù)端限流、調(diào)用鏈處理等)。二是僅僅修改一部分 HandlerMapping 的代碼,將請求映射變得可以適配 RPC 的編程模型。

          鑒于工作量與現(xiàn)實條件,我們選擇后一種方法,繼續(xù)沿用 DispatcherServlet,但改造部分 HandlerMapping 的代碼。

          1. 首先我們會通過 Scanner 掃描到標(biāo)注了@RpcClient 注解的接口以及其實現(xiàn)類,我們會將其注冊到 HandlerMapping 中,所以首先我們要看 HandlerMapping 中有沒有能擴(kuò)展注冊邏輯的地方。

          2. 接著我們再考慮處理請求的事兒,我們需要 HandlerMapping 能夠做到在沒有 Spring Annotation 的情況下也能為不同的參數(shù)選擇不同的 argumentResolver 參數(shù)處理器,這一點在 springMVC 中是通過標(biāo)注注解來區(qū)分的(RequestMapping、RequestBody 等),所以我們還需要看看 HandlerMapping 中有沒有能擴(kuò)展參數(shù)注解邏輯的地方。

          帶著這兩點目的,我們先來看 HandlerMapping 的邏輯。

          HandlerMapping 的初始化

          HandlerMapping 的初始化源碼比較長,我們直接一筆略過不是很重要的部分了。首先 RequestMappingHandlerMapping 的父類 AbstractHandlerMethodMapping 類實現(xiàn)了 InitializingBean 接口,在屬性初始化完成后會調(diào)用 afterPropertiesSet()方法,在該方法中調(diào)用 initHandlerMethods()進(jìn)行 HandlerMethod 初始化。InitHandlerMethods 方法中使用 detectHandlerMethods 方法從 bean 中根據(jù) bean name 查找 handlerMethod,此方法中調(diào)用 registerHandlerMethod 來注冊正常的 handlerMethod。

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

          我們發(fā)現(xiàn)這個方法是 protected 的,那么第一步我們找到了去哪注冊我們的 RPC 方法到 RequestMappingHandlerMapping 中。接口可以看到入?yún)⑹?handler 方法,但在 handlerMapping 中真正被注冊的 handlerMethod 對象,顯然這部分邏輯在 mappingRegistry 的 register 方法中。register 方法中我們找到了轉(zhuǎn)換的關(guān)鍵方法:

          HandlerMethod handlerMethod = createHandlerMethod(handler, method);

          此方法中調(diào)用了 handlerMethod 對象的構(gòu)造器來構(gòu)造一個 handlerMethod
          對象。handlerMethod 的屬性中包含一個叫 parameters 的 methodParameter 對象數(shù)組。我們知道 handlerMethod 對象對應(yīng)的是一個實現(xiàn)方法,那么 methodParameter 對象對應(yīng)的就是入?yún)⒘?。接著?methodParameter 對象里看,發(fā)現(xiàn)了一個叫 parameterAnnotations 的 Annotation 數(shù)組,看樣子這就是我們第二個需要關(guān)注的地方了。那么總結(jié)一下,濾去無需關(guān)注的部分,handlerMapping 的初始化整個如下圖所示:

          HandlerAdapter 的請求處理

          這邊 dispatcherServlet 在真正處理請求的時候是用 handlerAdapter 去處理再返回 ModelAndView 對象的,但是所有相關(guān)對象都是注冊在 handlerMapping 中。我們直接來看看 RequestMappingHandlerAdapter 的處理邏輯吧,handlerAdapter 在 handle 方法中調(diào)用 handleInternal 方法,并調(diào)用 invokeHandlerMethod 方法,此方法中使用 createInvocableHandlerMethod 方法將 handlerMethod 對象包裝成了一個 servletInvocableHandlerMethod 對象,此對象最終調(diào)用 invokeAndHandle 方法完成對應(yīng)請求邏輯的處理。我們只關(guān)注 invokeAndHandle 里面的 invokeForRequest 方法,該方法作為對入?yún)⒌奶幚碚俏覀兊哪繕?biāo)。最終我們看到了此方法中的 getMethodArgumentValues 方法中的一段對入?yún)⒆⒔獾奶幚磉壿?

              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 方法來作為判斷依據(jù)選擇 argumentResolver,里層的邏輯就是一個簡單的遍歷選擇真正支持入?yún)⒌膮?shù)處理器。實際上 RequestMappingHandlerAdapte 在初始化時候就注冊了一堆參數(shù)處理器:

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

          我們調(diào)個眼熟的 RequestResponseBodyMethodProcessor 來看看其 supportsParameter 方法:

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

          這里直接調(diào)用了 MethodParameter 自身的 public 方法 hasParameterAnnotation 方法來判斷是否有相應(yīng)的注解,比如有 RequestBody 注解那么我們就選用 RequestResponseBodyMethodProcessor 來作為其參數(shù)處理器。

          還是濾去無用邏輯,整個流程如下:

          服務(wù)端的 RPC 編程模型

          以上我們了解了 DispatcherServlet 在 REST 編程模型中是部分邏輯,現(xiàn)在我們依據(jù)之前講的改造部分 HandlerMapping 的代碼從而使其適配 RPC 編程模型。

          RPC 方法注冊

          首先我們需要將方法注冊到 handlerMapping,而這點由上述 RequestHandlerMapping 的初始化流程得知直接調(diào)用 registerHandlerMethod 方法即可。結(jié)合我們的掃描邏輯,大致代碼如下:

          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);
          }
          }
          }
          }

          我們自定義了注冊方法,只需在容器啟動時調(diào)用即可。

          RPC 請求處理

          以上所說,光完成注冊是不夠的,我們需要對入?yún)⒆⒔庾鲆恍┨幚恚缥覀冸m然沒有寫注解@RequestBody User user,我們?nèi)匀幌M?handlerAdapter 在處理的時候能夠以為我們寫了,并用 RequestResponseBodyMethodProcessor 參數(shù)解析器來進(jìn)行處理。

          我們直接重寫 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 用來判斷自己是否適合該參數(shù)的方法。我們做了些改造使得合適的參數(shù)會被合適的參數(shù)解析器"誤以為"加了對應(yīng)的注解,從而自己會去進(jìn)行正常的參數(shù)處理邏輯。整個處理流程如下,粉紅色部分也正是我們所擴(kuò)展的點了:

          RPC 編程模型

          經(jīng)過改造之后,我們已經(jīng)可以實現(xiàn)文章開頭所描述的透明 RPC 來開發(fā)微服務(wù)了,整個運行架構(gòu)變成了下面這樣:



          往期精彩推薦



          騰訊、阿里、滴滴后臺面試題匯總總結(jié) — (含答案)

          面試:史上最全多線程面試題 !

          最新阿里內(nèi)推Java后端面試題

          JVM難學(xué)?那是因為你沒認(rèn)真看完這篇文章


          END


          關(guān)注作者微信公眾號 —《JAVA爛豬皮》


          了解更多java后端架構(gòu)知識以及最新面試寶典


          你點的每個好看,我都認(rèn)真當(dāng)成了


          看完本文記得給作者點贊+在看哦~~~大家的支持,是作者源源不斷出文的動力


          作者:fredalxin
          地址:https://fredal.xin/develop-with-transparent-rpc

          瀏覽 25
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操逼动漫在线观看 | 色婷婷在线无码精品 | 日本中文在线视频 | 日日夜夜超碰 | 伊人大香蕉在线观看视频 |