基于SpringMVC的API灰度方案

打印 上一主题 下一主题

主题 530|帖子 530|积分 1590



  一、配景

  在微服务重构时,我们常碰到这个业务场景:同样是/api/test,我们实现了新逻辑和老逻辑,然后根据定制的灰度计谋,通常灰度API和老API两者都必要支持用户使用。
  那么是否有比力好解决方案,帮忙我们完成同名同方法同参数列表的API灰度动态路由的方案呢?
  我们就基于SpringMVC,通过对底层RequestMappingInfo的参数定制化,实现了methodHandler的动态路由决策,从而到达API灰度动态路由目标。
  

  二、实现原理

  我们总的来说,干了两件事情:
  

  

  • 第一件事
    服务启动时,在initMethodHandler实行时,对RequestMappingInfo初始化时,就将灰度决策器RouterDecisionMaker,以@PathRouterDecisionMaker决策器注解的情势,预加载到customInfo里
  • 第二件事
    服务运行期,在路由匹配器PathMatcher里,会剖析RequestMappingInfo,最终实行灰度决策器RouterDecisionMaker,并挑选最符合的RequestMappingInfo映射的methodHandler去实行响应逻辑
  三、实现方案

  1、界说决策器注解:@PathRouterDecisionMaker

  1. @Target({ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface PathRouterDecisionMaker {
  4.     Class<? extends RouterDecisionMaker> decision();
  5.     String resourceCondition() default "";
  6.     int order() default 0;
  7. }
复制代码
分析:
  

  • decision():决策属性,指向路由决策器类,即RouterDecisionMaker接口
     注解(Annotation):仅提供附加元数据支持,并 不能实现任何操作,必要另外的 Scanner 根据元数据实行相应操作。
    2、路由决策器:RouterDecisionMaker接口

  RouterDecisionMaker接口,界说了决策器的匹配计谋方法
  1. public interface RouterDecisionMaker {
  2.     /**
  3.      * 路由决策器的最终决策方法
  4.      * @param pathPartRequest
  5.      * @return 匹配返回的资源类型
  6.      */
  7.     boolean matches(PathPartRequest pathPartRequest);
  8. }
复制代码



  2.1 灰度决策器-ApiGrayDecisionMaker(返回true)

  1. @Component(value = "ApiGrayDecisionMaker")
  2. public class ApiGrayDecisionMaker implements RouterDecisionMaker {
  3.     @Override
  4.     public boolean matches(PathPartRequest pathPartRequest) {
  5.         return Boolean.TRUE;
  6.     }
  7. }
复制代码
代码分析:
  

  • 为了方便实践,我们写死一个决策匹配计谋为TRUE
  2.2 灰度决策器-ApiNotGrayDecisionMaker(返回false)

  1. @Component("ApiNotGrayDecisionMaker")
  2. public class ApiNotGrayDecisionMaker implements RouterDecisionMaker {
  3.     /**
  4.      * 取反,跟 ApiGrayDecision#matches 互斥
  5.      * @param pathPartRequest
  6.      * @return
  7.      */
  8.     @Override
  9.     public boolean matches(PathPartRequest pathPartRequest) {
  10.         return Boolean.FALSE;
  11.     }
  12. }
复制代码
代码分析:
  

  • 为了方便实践,我们写死一个决策匹配计谋为FALSE
  2.3 决策信息

  RouterPathRequest,提供数据给决策器
  1. public class RouterPathRequest {
  2.     private final String pattern;
  3.     private final String url;
  4.     private final Map<String, String> pathVariables;
  5.     private final RouterPatternKey routerPatternKey;
  6.     private final String routeCondition;
  7.     private final HttpServletRequest request;
  8.     public RouterPathRequest(HttpServletRequest request, String pattern, String url, Map<String, String> pathVariables,
  9.             RouterPatternKey routerPatternKey, String routeCondition) {
  10.         this.request = request;
  11.         this.pattern = pattern;
  12.         this.pathVariables = pathVariables;
  13.         this.url = url;
  14.         this.routerPatternKey = routerPatternKey;
  15.         this.routeCondition = routeCondition;
  16.     }
  17.     public static RouterPathRequest build(HttpServletRequest request, String pattern, String url,
  18.             Map<String, String> pathVariables, RouterPatternKey routerPatternKey, String routeCondition) {
  19.         return new RouterPathRequest(request, pattern, url, pathVariables, routerPatternKey, routeCondition);
  20.     }
  21.     //...getter&setter
  22. }
复制代码
代码分析:
  

  • 实际是对HttpServletRequest的二次封装,并提取了一些常用上下文数据到属性
  2.4 决策注解探测器

  WebRouterDecisionMakerDetection
  1. /**
  2. *  PathRouterDecisionMaker注解提取器
  3. *   - 从方法注释提取注解
  4. */
  5. public class WebRouterDecisionMakerDetection {
  6.     public PathRouterDecisionMaker detect(Method handlerMethod) {
  7.         if (Objects.isNull(handlerMethod)) {
  8.             return null;
  9.         }
  10.         return AnnotatedElementUtils.findMergedAnnotation(handlerMethod, PathRouterDecisionMaker.class);
  11.     }
  12. }
复制代码
2.5 自界说-路由匹配计谋

  WebRouterDecisionCondition 
  继续了 AbstractRequestCondition,会在创建RequestMappingInfo的填入customCondition条件时,被回调使用。
  

  • 抽象类AbstractRequestCondition实现了 RequestCondition 接口,最终回调业务的getMatchingCondition实现
  • RequestCondition具体实现类都继续自AbstractRequestCondition抽象基类,都是针对请求匹配的某一个方面:请求路径,请求头部,请求方法,请求参数,可消费MIME,可生成MIME等等。
     1、AbstractRequestCondition:这是一个抽象类,实现了 RequestCondition 接口,并提供了一1些默认实现。它简化了自界说条件的实现过程。 
   2、RequestCondition:这是一个接口,界说了用于匹配请求的条件。它包罗两个主要方法: 
   - getMatchingCondition(HttpServletRequest request):返回与给定请求匹配的条件。
   - combine(RequestCondition<?> other):将当前条件与其他条件组合。
   
  1. /**
  2. * 自定义的路由匹配条件
  3. */
  4. public class WebRouterDecisionCondition extends AbstractRequestCondition<WebRouterDecisionCondition> {
  5.     private final PathRouterDecisionMaker pathRouterDecisionMaker;
  6.      // getter/setter/constructor...
  7.     /**
  8.      * 创建 RequestMappingInfo 的时候,会进行两件事情:
  9.      * 1. 查看 method 上的 @RequestMapping 信息,同时根据 method 类型,创建 condition。
  10.      * WebRequestMappingHandlerMapping#getCustomMethodCondition(Method)
  11.      * - 创建相应的 condition
  12.      * 2. 查看 Controller 上的 @RequestMapping 信息,同时根据 Controller 类型,创建 condition。
  13.      * WebRequestMappingHandlerMapping#getCustomTypeCondition(Class)
  14.      * - 创建相应的 condition
  15.      *
  16.      * 查看:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(AnnotatedElement)
  17.      *
  18.      * @param other
  19.      * @return
  20.      */
  21.     @Override
  22.     public WebRouterDecisionCondition combine(WebRouterDecisionCondition other) {
  23.         return other;
  24.     }
  25.     @Override
  26.     public WebRouterDecisionCondition getMatchingCondition(HttpServletRequest request) {
  27.         // 【1】
  28.         boolean isDirectUrlMatched = this.checkRequestMappingInfo(request);
  29.             // 【2】
  30.         if (!isDirectUrlMatched) {
  31.             return this;
  32.         }
  33.         // 【3】
  34.         return checkPathRouterDecisionMaker(request, this.pathRouterDecisionMaker) ? this : null;
  35.     }
  36.     /**
  37.     * 分析RequestMappingInfo是否有定制化信息customCondition
  38.     */
  39.     public static boolean checkRequestMappingInfo(HttpServletRequest request) {
  40.         // 【1.1】
  41.         String lookupUrl = getLookupUrl(request);
  42.          // 【1.2】   
  43.         Object requestMappingInfoObject = request.getAttribute(REQUEST_MAPPING_MATCHING);
  44.         if (!(requestMappingInfoObject instanceof RequestMappingInfo)) {
  45.             return false;
  46.         }
  47.        //【1.3】   
  48.         RequestMappingInfo requestMappingInfo = (RequestMappingInfo) requestMappingInfoObject;
  49.         PatternsRequestCondition patternsRequestCondition = requestMappingInfo.getPatternsCondition();
  50.         if (ObjectUtils.isEmpty(patternsRequestCondition)) {
  51.             return false;
  52.         }
  53.         Set<String> patterns = patternsRequestCondition.getPatterns();
  54.         if (CollectionUtils.isEmpty(patterns)) {
  55.             return false;
  56.         }
  57.         for (String pattern : patterns) {
  58.             // 【1.4】
  59.             if (StringUtils.equals(pattern, lookupUrl)) {
  60.                 return true;
  61.             }
  62.         }
  63.         return false;
  64.     }
  65.     /**
  66.     * 检查决策器注解
  67.     */
  68.     public static boolean checkPathRouterDecisionMaker(HttpServletRequest request, PathRouterDecisionMaker pathRouterDecisionMaker) {
  69.         String lookupUrl = DirectPathRouterMatchCondition.getLookupUrl(request);
  70.         // 【3.1】
  71.         if (ObjectUtils.isEmpty(pathRouterDecisionMaker)) {
  72.             return true;
  73.         }
  74.         RouterDecisionMaker routerDecisionMaker = RouterConstraintsUtils.getRouterConstraint(pathRouterDecisionMaker.decision());
  75.         // 【3.2】
  76.         if (ObjectUtils.isEmpty(routerDecisionMaker)) {
  77.             return true;
  78.         }
  79.         // 【3.3】
  80.         RouterPathRequest routerPathRequest =
  81.                 RouterPathRequest.build(request, lookupUrl, lookupUrl, new HashMap<>(),
  82.                     new RouterPatternKey(lookupUrl, pathRouterDecisionMaker), null);
  83.         // 【3.4】
  84.         return routerDecisionMaker.matches(routerPathRequest);
  85.     }
  86.     public static String getLookupUrl(HttpServletRequest request) {
  87.         Object lookupUrl = request.getAttribute(HandlerMapping.LOOKUP_PATH);
  88.         return ObjectUtils.isEmpty(lookupUrl) ? "" : lookupUrl.toString();
  89.     }
  90. }
复制代码
代码分析:
  服务运行时,获取methodHandler,会回调 WebRouterDecisionCondition#getMatchingCondition
  

  • 【1】checkRequestMappingInfo 路由匹配分析:分析RequestMappingInfo是否和url相匹配

    • 【1.1】getLookupUrl获取url
    • 【1.2】获取RequestMappingInfo
    • 【1.3】获取RequestMappingInfo对象中的PatternsRequestCondition对象,然后获取其中的URL模式聚集
    • 【1.4】遍历URL模式聚集,如果找到与当前请求URL相匹配的模式,则返回true

  • 【2】如果不满足路径匹配,也没有RequestMappingInfo没有特别的customCondition要填充,就直接返回把
  • 【3】如果满足路径匹配,checkPathRouterDecisionMaker:检查是否包罗灰度路由器,即:判断有没有@PathRouterDecisionMaker注解元信息

    • 【3.1】路由 Controller 方法没有被 @PathRouterDecisionMaker注解修饰
    • 【3.2】@PathRouterDecisionMaker 注解中没有 RouterDecisionMaker 决策器,默认视为 true
    • 【3.3】直接路径匹配,则 lookupUrl 和 pattern 相匹配
    • 【3.4】实行决策器matches方法

  3、界说Controller:灰度和非灰度API

  1. @RestController
  2. public class ConstraintController {
  3.     @PathRouterDecisionMaker(decision = ApiNotGrayDecisionMaker.class)
  4.     @GetMapping("/test_constraint")
  5.     public String test() {
  6.         return "非灰度:老API..";
  7.     }
  8.     @PathRouterDecisionMaker(decision = ApiGrayDecisionMaker.class)
  9.     @GetMapping("/test_constraint")
  10.     public String test2() {
  11.         return "灰度:新API..";
  12.     }
  13. }
复制代码
代码分析:
  

  • 可见上面界说了两个同为GET方法,参数列表相同,RequestMapping的Url也一样,只是方法名不同的一组灰度和非灰度API。
  • 同时两个API,都用了@PathRouterDecisionMaker注解修饰,但指定了不同的决策器(非灰度API是ApiNotGrayDecisionMaker,灰度API是ApiGrayDecisionMaker)
  4、MVC框架定制化工作

  4.1 MVC设置器裁剪:WebMvcRegistrations

     WebMvcRegistrations 是 Spring MVC 框架中的一个接口,用于自界说 Spring MVC 的设置。通过实现这个接口,你可以注册自界说的 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 和其他与 Spring MVC 相关的组件。
    WebRequestMappingRegistrationsConfig 是一个设置类
  1. @Configuration
  2. public class WebRequestMappingRegistrationsConfig implements WebMvcRegistrations {
  3.     /**
  4.      * 返回一个自定义的 RequestMappingHandlerMapping 实例,用于处理 HTTP 请求映射。
  5.      *
  6.      * 具体怎么对请求进行映射呢?,参考 WebRequestMappingHandlerMapping
  7.      * - WebRequestMappingHandlerMapping 有路径匹配器:WebRouterPathConstraintMatcher
  8.      *    - WebRouterPathConstraintMatcher 有路径匹配器:PathMatcher#match
  9.      */
  10.     @Override
  11.     public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
  12.         WebRequestMappingHandlerMapping requestMappingHandlerMapping = new WebRequestMappingHandlerMapping();
  13.         requestMappingHandlerMapping.setPathMatcher(routerPathConstraintMatcher());
  14.         return requestMappingHandlerMapping;
  15.     }
  16.     @Bean
  17.     public WebRouterPathConstraintMatcher routerPathConstraintMatcher() {
  18.         return new WebRouterPathConstraintMatcher();
  19.     }
  20. }
复制代码
代码分析:
  

  • 界说了一个WebRequestMappingHandlerMapping,它负责:请求定制化映射处置处罚工作

    • 具体的处置处罚逻辑,交给手下的WebRouterPathConstraintMatcher来办
    • 且看4.2教学WebRequestMappingHandlerMapping
    • 且看4.3教学WebRouterPathConstraintMatcher

  4.2 路由映射器裁剪:

  AbstractRequestMappingHandlerMapping

  WebRequestMappingHandlerMapping
  

  • 继续了抽象类,抽象类AbstractRequestMappingHandlerMapping实现了InitializingBean接口,最终会回调业务的initHandlerMethods实现
     在Spring MVC中,请求条件用于决定一个特定的HTTP请求是否应该被一个控制器方法处置处罚。
   
  1. @ScanPackagePathConstraint(basePackageNames = {"com.bryant"})
  2. public class WebRequestMappingHandlerMapping extends AbstractRequestMappingHandlerMapping {
  3.     private WebRouterDecisionMakerDetection webRouterDecisionMakerDetection;
  4.     public WebRouterDecisionMakerDetection getPathConstraintDetection() {
  5.         return this.webRouterDecisionMakerDetection;
  6.     }
  7.     public void setPathConstraintDetection(WebRouterDecisionMakerDetection webRouterDecisionMakerDetection) {
  8.         this.webRouterDecisionMakerDetection = webRouterDecisionMakerDetection;
  9.     }
  10.     @Override
  11.     protected void initHandlerMethods() {
  12.         // 加入决策探测器
  13.         this.setPathConstraintDetection(new WebRouterDecisionMakerDetection(this.packageRouterConstraintRegistry));
  14.         // 初始化HandlerMethods
  15.         super.initHandlerMethods();
  16.     }
  17.     /**
  18.      * 循环遍历所有的 @RequestMapping 对一个的路由元信息,进行匹配,匹配到最佳 RequestMappingInfo
  19.      * ```
  20.      * for (RequestMappingInfo mapping : mappings) {
  21.      * // 依次匹配 method\produces\consumes\header\...\patterns\customCondition
  22.      * }
  23.      * ```
  24.      */
  25.     @Override
  26.     protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
  27.         // 这里步骤是必须的(参考【4.3】步骤的),用来填充HttpServletRequest的上下文,将RequestMappingInfo数据透传往下,key = REQUEST_MAPPING_MATCHING
  28.         request.setAttribute(REQUEST_MAPPING_MATCHING, info);
  29.         RequestMappingInfo requestMappingInfo = super.getMatchingMapping(info, request);
  30.         // 获取结束后,记得移除上下文数据
  31.         request.removeAttribute(REQUEST_MAPPING_MATCHING);
  32.         return requestMappingInfo;
  33.     }
  34.     /**
  35.      * 直接调用 AbstractHandlerMethodMapping#lookupHandlerMethod(java.lang.String, javax.servlet.http.HttpServletRequest) 即可,没有做特殊处理
  36.      * @param lookupPath 请求路径,通过 {@link org.springframework.web.util.UrlPathHelper} 获取
  37.      * @param request
  38.      * @return
  39.      * @throws Exception
  40.      */
  41.     @Override
  42.     public HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  43.         return super.lookupHandlerMethod(lookupPath, request);
  44.     }
  45.     /**
  46.      * 为每一个请求方法对应的 RequestMappingInfo 路由元信息,创建一个 RequestCondition
  47.      *
  48.      * @param method
  49.      * @return
  50.      */
  51.     @Override
  52.     protected RequestCondition<?> getCustomMethodCondition(Method method) {
  53.         // 非灰度接口
  54.         PathConstraintDetection pathConstraintDetection = this.getPathConstraintDetection();
  55.         if (ObjectUtils.isEmpty(pathConstraintDetection)) {
  56.             return new WebRouterDecisionMakerDetection(null);
  57.         }
  58.         // 灰度接口处理
  59.         PathRouterDecisionMaker pathRouterDecisionMaker = pathConstraintDetection.detect(method);
  60.         if (ObjectUtils.isNotEmpty(pathRouterDecisionMaker)) {
  61.             return new WebRouterDecisionMakerDetection(pathRouterDecisionMaker);
  62.         }
  63.         // 兜底处理
  64.         return new WebRouterDecisionMakerDetection(null);
  65.     }
  66.     @Override
  67.     protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
  68.         return super.handleNoMatch(infos, lookupPath, request);
  69.     }
  70. }
复制代码
代码分析:
  

  • setPathConstraintDetection:加入决策探测器,由于后续会用到
  • initHandlerMethods:初始化入口,实际走了AbstractRequestMappingHandlerMapping抽象类的initHandlerMethods

    • 步骤一:创建HandlerMethod
    • 步骤二:校验validateMethodMapping
    • 步骤三:放入mappingLookup
    • 预加载RequestMappingInfo:通过MappingRegistry,将原API和灰度API的RequestMappingInfo信息,注册到mappingLookup这个Map里(key是RequestMappingInfo,value是HandlerMethod)
    • 本质是:RequestMappingInfo里面包罗了路由计谋,我们通过在handlerMethod方法上打注解,如此透传到RequestMappingInfo#customConditionHolder;解决了SpringBoot启动服务,规避了RequestMappingHandler重复异常
    • 核心是:AbstractHandlerMethodMapping.MappingRegistry#register,分为三个步骤(受限篇幅)

  • getMatchingMapping:循环遍历所有的 @RequestMapping 聚集,根据路由注解元信息,进行匹配,匹配到最佳 RequestMappingInfo

    • for (RequestMappingInfo mapping : mappings)

  • lookupHandlerMethod:将HTTP请求映射到相应的methodHandler处置处罚器方法上;这里直接调用了父类AbstractHandlerMethodMapping的同名方法,没有进行任何额外的处置处罚(但真实业务可以按需裁剪功能)
  • getCustomMethodCondition:我们通过 WebRouterDecisionMakerDetection 完成自界说RequestMappingInfo#CustomCondition的注入和判断

    • 返回:一个WebRouterDecisionMakerDetection对象,里面包装了 @PathRouterDecisionMaker注解元信息,而注解则包罗了决策器对象PathRouterDecisionMaker,即该方法返回的Condition包罗了灰度决策器
    • WebRouterDecisionMakerDetection剖析见:【2.4】-决策注解探测器

  • handleNoMatch:兜底处置处罚:在处置处罚HTTP请求时,如果找不到完全匹配的RequestMappingInfo(即URL和HTTP方法等都不匹配),则根据不同的不匹配类型抛出相应的异常。
  4.3 路由匹配器裁剪:PathMatcher

  WebRouterPathConstraintMatcher,实现了PathMatcher接口,实现了从 HttpServletRequest 提取 @PathRouterDecisionMaker注解元信息,灰度决策器PathRouterDecisionMaker
  1. public class WebRouterPathConstraintMatcher implements PathMatcher {
  2.     /**
  3.      * 自定义了一个key到request的attribute,如此通过HttpServletRequest的上下文透传数据
  4.      */
  5.     String REQUEST_MAPPING_MATCHING = PathMatchedConstant.class.getName() + ".requestMappingMatching";
  6.     @Override
  7.     public boolean match(String pattern, String path) {
  8.         // 【0】从上下文获取请求,本质上是Spring内置的工具类实现的
  9.         HttpServletRequest request = ServletRequestUtil.getCurrentRequest();
  10.         // 【1】提取 @PathRouterDecisionMaker注解元信息
  11.         PathRouterDecisionMaker pathRouterDecisionMaker = getRouterConstraintTypeByMatchingRequestMappingInfo(request);
  12.         Class<? extends RouterDecisionMaker> routerConstraintClass = ObjectUtils.isEmpty(pathRouterDecisionMaker) ? null : pathRouterDecisionMaker.decision();
  13.         // 【2】 封装了一层 RouterPatternKey
  14.         RouterPatternKey routerPatternKey = new RouterPatternKey(pattern, pathRouterDecisionMaker);
  15.         // 【3】有约束条件处理
  16.         if (!ObjectUtils.isEmpty(routerConstraintClass)) {
  17.             // 【3.1】提取 RouterDecisionMaker决策器(灰度决策器-可能是ApiNotGrayDecisionMaker,也可能是ApiGrayDecisionMaker)
  18.             RouterDecisionMaker routerDecisionMaker = RouterConstraintsUtils.getRouterConstraint(routerConstraintClass);
  19.             // 【3.2】构造决策信息 RouterPathRequest
  20.             RouterPathRequest routerPathRequest = RouterPathRequest.build(request, pattern, path, null, routerPatternKey, pathRouterDecisionMaker.resourceCondition());
  21.             // 【3.3】路由决策器执行
  22.             if (!routerDecisionMaker.matches(routerPathRequest)) {
  23.                 return false;
  24.             }
  25.         }
  26.         // 不存在约束条件
  27.         return true;
  28.     }
  29.     /**
  30.      * 每次在匹配的时候,当前匹配的 RequestMappingInfo 存储在 request 上下文中:
  31.      * ```
  32.      * for (RequestMappingInfo mapping : mappings) {
  33.      *     request.setAttribute(REQUEST_MAPPING_MATCHING, mapping);
  34.      *     // 依次匹配 method\produces\consumes\header\...\patterns\customCondition
  35.      *     request.removeAttribute(REQUEST_MAPPING_MATCHING, mapping);
  36.      * }
  37.      * ```
  38.      * 以下方法获取每一个正在匹配的 RequestMappingInfo 中 customCondition 对应的 RouterConstraint 约束条件
  39.      * @param request
  40.      * @return
  41.      */
  42.     private PathRouterDecisionMaker getRouterConstraintTypeByMatchingRequestMappingInfo(HttpServletRequest request) {
  43.         Object requestMappingInfoObject = request.getAttribute(REQUEST_MAPPING_MATCHING);
  44.         // 【1.1】取出 RequestMappingInfo 对象
  45.         if (!(requestMappingInfoObject instanceof RequestMappingInfo)) {
  46.             return null;
  47.         }
  48.         RequestMappingInfo requestMappingInfo = (RequestMappingInfo) requestMappingInfoObject;
  49.         // 【1.2】RequestMappingInfo对象的customCondition不为空,说明有灰度决策器or非灰度决策器,
  50.         // 通过【4.2】步骤之后,自定义的路由匹配条件,会塞到一个新的WebRouterDecisionCondition对象,
  51.         // 因此要读出,自然是从 WebRouterDecisionCondition 提取出来 PathRouterDecisionMaker注解元信息
  52.         RequestCondition<?> requestCondition = requestMappingInfo.getCustomCondition();
  53.         if (!(requestCondition instanceof WebRouterDecisionCondition)) {
  54.             return null;
  55.         }
  56.         // 【1.3】从 WebRouterDecisionCondition 提取出来 PathRouterDecisionMaker注解元信息
  57.         WebRouterDecisionCondition condition = (WebRouterDecisionCondition) requestCondition;
  58.         Collection<PathRouterDecisionMaker> pathRouterDecisionMakers = condition.getContent();
  59.         return CollectionUtils.isEmpty(pathRouterDecisionMakers) ? null : new ArrayList<>(pathRouterDecisionMakers).get(0);
  60.     }
  61. }
复制代码
代码分析:
  

  • WebRouterPathConstraintMatcher#match:

    • 【0】从上下文获取请求,本质上是Spring内置的工具类实现的
    • 【1】提取 PathRouterDecisionMaker 决策器

      • 【1.1】取出 RequestMappingInfo 对象
      • 【1.2】从RequestMappingInfo 对象拿customCondition数据,即WebRouterDecisionCondition
      • 【1.3】从 WebRouterDecisionCondition 提取出来 PathRouterDecisionMaker注解元信息并返回

    • 【2】 在@PathRouterDecisionMaker注解元信息,封装了一层 RouterPatternKey对象
    • 【3】有束缚条件处置处罚

      • 【3.1】提取 RouterDecisionMaker决策器(灰度决策器-可能是ApiNotGrayDecisionMaker,也可能是ApiGrayDecisionMaker)
      • 【3.2】构造决策信息 RouterPathRequest
      • 【3.3】路由决策器实行,也是我们最核心灰度逻辑的实现了,现在我们是写死为true or false的返回值,但其实我们可以通过注入RestTemplate实现更复杂的灰度计谋。


  4.4 总结

  通过以上的设置,我们实现了MVC框架的定制化工作,通过ServeltHttpRequest,将@PathRouterDecisionMaker注解元信息透传给了路由映射器,再通过灰度决策器确认是否返回RequestMappingInfo。
  5、测试

  访问:http://localhost:8853/test_constraint,实现路由的动态匹配,请求抵达新API灰度的MethodHandler并被处置处罚完成。
  

  四、源码剖析

  上面分析了实现原理和实现方案,下面则从MVC初始化的角度来分析。
  1、预加载:RequestMappingInfo

  初始化入口是:AbstractRequestMappingHandlerMapping抽象类的initHandlerMethods
  预加载是:通过MappingRegistry,将原API和灰度API的RequestMappingInfo信息,注册到mappingLookup这个Map里(key是RequestMappingInfo,value是HandlerMethod)
  本质是:RequestMappingInfo里面包罗了路由计谋,我们通过在handlerMethod方法上打注解,如此透传到RequestMappingInfo#customConditionHolder;解决了SpringBoot启动服务,规避了RequestMappingHandler重复异常
  核心是:AbstractHandlerMethodMapping.MappingRegistry#register
  

  • 步骤一:创建HandlerMethod
  • 步骤二:校验validateMethodMapping
  • 步骤三:放入mappingLookup
  2、动态路由:

  AbstractHandlerMethodMapping#lookupHandlerMethod

  

  • org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
  • 步骤一:this.mappingRegistry.getMappingsByUrl(lookupPath);

    • 这里将灰度API和原API的RequestMappingInfo都取出来了

  • 步骤二:addMatchingMappings();
    1、里面会逐个RequestMappingInfo校验是否匹配乐成,这里会回调的WebRequestMappingHandlerMapping.java#getMatchingMapping,即,RequestMappingInfoHandlerMapping#getMatchingMapping,
  很简单,就是遍历request的请求附加参数,融合到RequestMappingInfo里面。
  另外,注意这个步骤:
  RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
    customConditionHolder 是持有决策器 ApiGrayDecisionMaker的
  ,沿着背面的链路一直debug,会回调到 WebRouterConstraintCondition#getMatchingCondition 方法,
  在这个自界说匹配方法里,可以直接用@PathRouterDecisionMaker
  注解的动态路由方法,对路由匹配规则校验并返回。
  于是,最终RequestConditionHolder custom是得到了生效的决策器[ApiGrayDecisionMaker]
  2、对于匹配乐成的RequestMappingInfo,包装一个Match对象
  步骤三:取出最终匹配到的Match
  步骤四:handleMatch
  1、会回调到WebRequestMappingHandlerMapping#handleMatch,即,RequestMappingInfoHandlerMapping#handleMatch,很简单,
  2、进入到AbstractHandlerMethodMapping#lookupHandlerMethod
  3、返回methodHandler
  

  

  • 步骤五:嵌入拦截器,构成实行责任链
  

  

  • 步骤六:通过署理实行invokeHandlerMethod,最终动态署理实行Controller#test方法,即:灰度逻辑
  

  五、总结

  以上是基于SpringMVC的接口动态灰度方案的一些教学,实际工程问题上,还会碰到一些其他问题,好比:
  

  • 如果通过缓存,加速动态路由的盘算,让每次请求都快速找到RequestMappingInfo,加速路由匹配
  • 如果匹配失败的请求,能否缓存起来,下次再有请求到后端,则快速失败
  • 对于批量接口灰度,是否有更好的办法呢?
  上面源码可以参考我的个人github项目:https://github.com/bryantmo/springcloud_test
  六、相关文章

  JDK源码教学:Bean生命周期案例(初始化/销毁)

  Mybatis链路分析:JDK动态署理和责任链模式的应用

  业务上云的容器排障与思考


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦应逍遥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表