前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于SpringMVC的API灰度方案

基于SpringMVC的API灰度方案

原创
作者头像
后台技术汇
发布2024-09-25 16:57:23
870
发布2024-09-25 16:57:23
举报
文章被收录于专栏:后台技术汇

一、背景

在微服务重构时,我们常遇到这个业务场景:同样是/api/test,我们实现了新逻辑和老逻辑,然后根据定制的灰度策略,通常灰度API和老API两者都需要支持用户使用。

那么是否有比较好解决方案,协助我们完成同名同方法同参数列表的API灰度动态路由的方案呢?

我们就基于SpringMVC,通过对底层RequestMappingInfo的参数定制化,实现了methodHandler的动态路由决策,从而达到API灰度动态路由目的。

二、实现原理

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

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

三、实现方案

1、定义决策器注解:@PathRouterDecisionMaker

代码语言:txt
复制

@Target({ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PathRouterDecisionMaker {
    Class<? extends RouterDecisionMaker> decision();
    String resourceCondition() default "";
    int order() default 0;
}

分析:

  • decision():决策属性,指向路由决策器类,即RouterDecisionMaker接口

注解(Annotation):仅提供附加元数据支持,并 不能实现任何操作,需要另外的 Scanner 根据元数据执行相应操作。

2、路由决策器:RouterDecisionMaker接口

RouterDecisionMaker接口,定义了决策器的匹配策略方法

代码语言:txt
复制

public interface RouterDecisionMaker {

    /**
     * 路由决策器的最终决策方法
     * @param pathPartRequest
     * @return 匹配返回的资源类型
     */
    boolean matches(PathPartRequest pathPartRequest);
}

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

代码语言:txt
复制

@Component(value = "ApiGrayDecisionMaker")
public class ApiGrayDecisionMaker implements RouterDecisionMaker {

    @Override
    public boolean matches(PathPartRequest pathPartRequest) {
        return Boolean.TRUE;
    }

}

代码分析:

  • 为了方便实践,我们写死一个决策匹配策略为TRUE

2.2 灰度决策器-ApiNotGrayDecisionMaker(返回false)

代码语言:txt
复制

@Component("ApiNotGrayDecisionMaker")
public class ApiNotGrayDecisionMaker implements RouterDecisionMaker {

    /**
     * 取反,跟 ApiGrayDecision#matches 互斥
     * @param pathPartRequest
     * @return
     */
    @Override
    public boolean matches(PathPartRequest pathPartRequest) {
        return Boolean.FALSE;
    }
}

代码分析:

  • 为了方便实践,我们写死一个决策匹配策略为FALSE

2.3 决策信息

RouterPathRequest,提供数据给决策器

代码语言:txt
复制

public class RouterPathRequest {

    private final String pattern;
    private final String url;
    private final Map<String, String> pathVariables;
    private final RouterPatternKey routerPatternKey;
    private final String routeCondition;
    private final HttpServletRequest request;

    public RouterPathRequest(HttpServletRequest request, String pattern, String url, Map<String, String> pathVariables,
            RouterPatternKey routerPatternKey, String routeCondition) {
        this.request = request;
        this.pattern = pattern;
        this.pathVariables = pathVariables;
        this.url = url;
        this.routerPatternKey = routerPatternKey;
        this.routeCondition = routeCondition;
    }

    public static RouterPathRequest build(HttpServletRequest request, String pattern, String url,
            Map<String, String> pathVariables, RouterPatternKey routerPatternKey, String routeCondition) {
        return new RouterPathRequest(request, pattern, url, pathVariables, routerPatternKey, routeCondition);
    }
    //...getter&setter
}

代码分析:

  • 实际是对HttpServletRequest的二次封装,并提取了一些常用上下文数据到属性

2.4 决策注解探测器

WebRouterDecisionMakerDetection

代码语言:txt
复制
/**
 *  PathRouterDecisionMaker注解提取器
 *   - 从方法注释提取注解
 */
public class WebRouterDecisionMakerDetection {
    public PathRouterDecisionMaker detect(Method handlerMethod) {
        if (Objects.isNull(handlerMethod)) {
            return null;
        }
        return AnnotatedElementUtils.findMergedAnnotation(handlerMethod, PathRouterDecisionMaker.class);
    }
}

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):将当前条件与其他条件组合。

代码语言:txt
复制

/**
 * 自定义的路由匹配条件
 */
public class WebRouterDecisionCondition extends AbstractRequestCondition<WebRouterDecisionCondition> {

    private final PathRouterDecisionMaker pathRouterDecisionMaker;

     // getter/setter/constructor...

    /**
     * 创建 RequestMappingInfo 的时候,会进行两件事情:
     * 1. 查看 method 上的 @RequestMapping 信息,同时根据 method 类型,创建 condition。
     * WebRequestMappingHandlerMapping#getCustomMethodCondition(Method)
     * - 创建相应的 condition
     * 2. 查看 Controller 上的 @RequestMapping 信息,同时根据 Controller 类型,创建 condition。
     * WebRequestMappingHandlerMapping#getCustomTypeCondition(Class)
     * - 创建相应的 condition
     *
     * 查看:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(AnnotatedElement)
     *
     * @param other
     * @return
     */
    @Override
    public WebRouterDecisionCondition combine(WebRouterDecisionCondition other) {
        return other;
    }

    @Override
    public WebRouterDecisionCondition getMatchingCondition(HttpServletRequest request) {
        // 【1】
        boolean isDirectUrlMatched = this.checkRequestMappingInfo(request);
            // 【2】
        if (!isDirectUrlMatched) {
            return this;
        }
        // 【3】
        return checkPathRouterDecisionMaker(request, this.pathRouterDecisionMaker) ? this : null;
    }

    /**
    * 分析RequestMappingInfo是否有定制化信息customCondition
    */
    public static boolean checkRequestMappingInfo(HttpServletRequest request) {
        // 【1.1】
        String lookupUrl = getLookupUrl(request);
         // 【1.2】   
        Object requestMappingInfoObject = request.getAttribute(REQUEST_MAPPING_MATCHING);
        if (!(requestMappingInfoObject instanceof RequestMappingInfo)) {
            return false;
        }
       //【1.3】    
        RequestMappingInfo requestMappingInfo = (RequestMappingInfo) requestMappingInfoObject;
        PatternsRequestCondition patternsRequestCondition = requestMappingInfo.getPatternsCondition();
        if (ObjectUtils.isEmpty(patternsRequestCondition)) {
            return false;
        }
        Set<String> patterns = patternsRequestCondition.getPatterns();
        if (CollectionUtils.isEmpty(patterns)) {
            return false;
        }
        for (String pattern : patterns) {
            // 【1.4】
            if (StringUtils.equals(pattern, lookupUrl)) {
                return true;
            }
        }
        return false;
    }

    /**
    * 检查决策器注解
    */
    public static boolean checkPathRouterDecisionMaker(HttpServletRequest request, PathRouterDecisionMaker pathRouterDecisionMaker) {
        String lookupUrl = DirectPathRouterMatchCondition.getLookupUrl(request);

        // 【3.1】
        if (ObjectUtils.isEmpty(pathRouterDecisionMaker)) {
            return true;
        }

        RouterDecisionMaker routerDecisionMaker = RouterConstraintsUtils.getRouterConstraint(pathRouterDecisionMaker.decision());
        // 【3.2】
        if (ObjectUtils.isEmpty(routerDecisionMaker)) {
            return true;
        }
        // 【3.3】
        RouterPathRequest routerPathRequest = 
                RouterPathRequest.build(request, lookupUrl, lookupUrl, new HashMap<>(),
                    new RouterPatternKey(lookupUrl, pathRouterDecisionMaker), null);

        // 【3.4】
        return routerDecisionMaker.matches(routerPathRequest);
    }

    public static String getLookupUrl(HttpServletRequest request) {
        Object lookupUrl = request.getAttribute(HandlerMapping.LOOKUP_PATH);
        return ObjectUtils.isEmpty(lookupUrl) ? "" : lookupUrl.toString();
    }

}

代码分析:

服务运行时,获取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

代码语言:txt
复制

@RestController
public class ConstraintController {

    @PathRouterDecisionMaker(decision = ApiNotGrayDecisionMaker.class)
    @GetMapping("/test_constraint")
    public String test() {
        return "非灰度:老API..";
    }

    @PathRouterDecisionMaker(decision = ApiGrayDecisionMaker.class)
    @GetMapping("/test_constraint")
    public String test2() {
        return "灰度:新API..";
    }
}

代码分析:

  • 可见上面定义了两个同为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 是一个配置类

代码语言:txt
复制

@Configuration
public class WebRequestMappingRegistrationsConfig implements WebMvcRegistrations {
    /**
     * 返回一个自定义的 RequestMappingHandlerMapping 实例,用于处理 HTTP 请求映射。
     * 
     * 具体怎么对请求进行映射呢?,参考 WebRequestMappingHandlerMapping
     * - WebRequestMappingHandlerMapping 有路径匹配器:WebRouterPathConstraintMatcher
     *    - WebRouterPathConstraintMatcher 有路径匹配器:PathMatcher#match
     */
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        WebRequestMappingHandlerMapping requestMappingHandlerMapping = new WebRequestMappingHandlerMapping();
        requestMappingHandlerMapping.setPathMatcher(routerPathConstraintMatcher());
        return requestMappingHandlerMapping;
    }

    @Bean
    public WebRouterPathConstraintMatcher routerPathConstraintMatcher() {
        return new WebRouterPathConstraintMatcher();
    }
}

代码分析:

  • 定义了一个WebRequestMappingHandlerMapping,它负责:请求定制化映射处理工作
    • 具体的处理逻辑,交给手下的WebRouterPathConstraintMatcher来办
    • 且看4.2讲解WebRequestMappingHandlerMapping
    • 且看4.3讲解WebRouterPathConstraintMatcher

4.2 路由映射器裁剪:

AbstractRequestMappingHandlerMapping

WebRequestMappingHandlerMapping

  • 继承了抽象类,抽象类AbstractRequestMappingHandlerMapping实现了InitializingBean接口,最终会回调业务的initHandlerMethods实现

在Spring MVC中,请求条件用于决定一个特定的HTTP请求是否应该被一个控制器方法处理。

代码语言:txt
复制


@ScanPackagePathConstraint(basePackageNames = {"com.bryant"})
public class WebRequestMappingHandlerMapping extends AbstractRequestMappingHandlerMapping {

    private WebRouterDecisionMakerDetection webRouterDecisionMakerDetection;

    public WebRouterDecisionMakerDetection getPathConstraintDetection() {
        return this.webRouterDecisionMakerDetection;
    }

    public void setPathConstraintDetection(WebRouterDecisionMakerDetection webRouterDecisionMakerDetection) {
        this.webRouterDecisionMakerDetection = webRouterDecisionMakerDetection;
    }

    @Override
    protected void initHandlerMethods() {
        // 加入决策探测器
        this.setPathConstraintDetection(new WebRouterDecisionMakerDetection(this.packageRouterConstraintRegistry));
        // 初始化HandlerMethods
        super.initHandlerMethods();
    }

    /**
     * 循环遍历所有的 @RequestMapping 对一个的路由元信息,进行匹配,匹配到最佳 RequestMappingInfo
     * ```
     * for (RequestMappingInfo mapping : mappings) {
     * // 依次匹配 method\produces\consumes\header\...\patterns\customCondition
     * }
     * ```
     */
    @Override
    protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
        // 这里步骤是必须的(参考【4.3】步骤的),用来填充HttpServletRequest的上下文,将RequestMappingInfo数据透传往下,key = REQUEST_MAPPING_MATCHING
        request.setAttribute(REQUEST_MAPPING_MATCHING, info);
        RequestMappingInfo requestMappingInfo = super.getMatchingMapping(info, request);
        // 获取结束后,记得移除上下文数据
        request.removeAttribute(REQUEST_MAPPING_MATCHING);
        return requestMappingInfo;
    }

    /**
     * 直接调用 AbstractHandlerMethodMapping#lookupHandlerMethod(java.lang.String, javax.servlet.http.HttpServletRequest) 即可,没有做特殊处理
     * @param lookupPath 请求路径,通过 {@link org.springframework.web.util.UrlPathHelper} 获取
     * @param request
     * @return
     * @throws Exception
     */
    @Override
    public HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        return super.lookupHandlerMethod(lookupPath, request);
    }

    /**
     * 为每一个请求方法对应的 RequestMappingInfo 路由元信息,创建一个 RequestCondition
     *
     * @param method
     * @return
     */
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {

        // 非灰度接口
        PathConstraintDetection pathConstraintDetection = this.getPathConstraintDetection();
        if (ObjectUtils.isEmpty(pathConstraintDetection)) {
            return new WebRouterDecisionMakerDetection(null);
        }

        // 灰度接口处理
        PathRouterDecisionMaker pathRouterDecisionMaker = pathConstraintDetection.detect(method);
        if (ObjectUtils.isNotEmpty(pathRouterDecisionMaker)) {
            return new WebRouterDecisionMakerDetection(pathRouterDecisionMaker);
        }

        // 兜底处理
        return new WebRouterDecisionMakerDetection(null);
    }

    @Override
    protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
        return super.handleNoMatch(infos, lookupPath, request);
    }
}

代码分析:

  • 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

代码语言:txt
复制

public class WebRouterPathConstraintMatcher implements PathMatcher {

    /**
     * 自定义了一个key到request的attribute,如此通过HttpServletRequest的上下文透传数据
     */
    String REQUEST_MAPPING_MATCHING = PathMatchedConstant.class.getName() + ".requestMappingMatching";

    @Override
    public boolean match(String pattern, String path) {

        // 【0】从上下文获取请求,本质上是Spring内置的工具类实现的
        HttpServletRequest request = ServletRequestUtil.getCurrentRequest();

        // 【1】提取 @PathRouterDecisionMaker注解元信息
        PathRouterDecisionMaker pathRouterDecisionMaker = getRouterConstraintTypeByMatchingRequestMappingInfo(request);
        Class<? extends RouterDecisionMaker> routerConstraintClass = ObjectUtils.isEmpty(pathRouterDecisionMaker) ? null : pathRouterDecisionMaker.decision();

        // 【2】 封装了一层 RouterPatternKey
        RouterPatternKey routerPatternKey = new RouterPatternKey(pattern, pathRouterDecisionMaker);

        // 【3】有约束条件处理
        if (!ObjectUtils.isEmpty(routerConstraintClass)) {
            // 【3.1】提取 RouterDecisionMaker决策器(灰度决策器-可能是ApiNotGrayDecisionMaker,也可能是ApiGrayDecisionMaker)
            RouterDecisionMaker routerDecisionMaker = RouterConstraintsUtils.getRouterConstraint(routerConstraintClass);
            // 【3.2】构造决策信息 RouterPathRequest
            RouterPathRequest routerPathRequest = RouterPathRequest.build(request, pattern, path, null, routerPatternKey, pathRouterDecisionMaker.resourceCondition());
            // 【3.3】路由决策器执行
            if (!routerDecisionMaker.matches(routerPathRequest)) {
                return false;
            }
        }
        // 不存在约束条件
        return true;
    }

    /**
     * 每次在匹配的时候,当前匹配的 RequestMappingInfo 存储在 request 上下文中:
     * ```
     * for (RequestMappingInfo mapping : mappings) {
     *     request.setAttribute(REQUEST_MAPPING_MATCHING, mapping);
     *     // 依次匹配 method\produces\consumes\header\...\patterns\customCondition
     *     request.removeAttribute(REQUEST_MAPPING_MATCHING, mapping);
     * }
     * ```
     * 以下方法获取每一个正在匹配的 RequestMappingInfo 中 customCondition 对应的 RouterConstraint 约束条件
     * @param request
     * @return
     */
    private PathRouterDecisionMaker getRouterConstraintTypeByMatchingRequestMappingInfo(HttpServletRequest request) {
        Object requestMappingInfoObject = request.getAttribute(REQUEST_MAPPING_MATCHING);
        // 【1.1】取出 RequestMappingInfo 对象
        if (!(requestMappingInfoObject instanceof RequestMappingInfo)) {
            return null;
        }

        RequestMappingInfo requestMappingInfo = (RequestMappingInfo) requestMappingInfoObject;

        // 【1.2】RequestMappingInfo对象的customCondition不为空,说明有灰度决策器or非灰度决策器,
        // 通过【4.2】步骤之后,自定义的路由匹配条件,会塞到一个新的WebRouterDecisionCondition对象,
        // 因此要读出,自然是从 WebRouterDecisionCondition 提取出来 PathRouterDecisionMaker注解元信息
        RequestCondition<?> requestCondition = requestMappingInfo.getCustomCondition();
        if (!(requestCondition instanceof WebRouterDecisionCondition)) {
            return null;
        }

        // 【1.3】从 WebRouterDecisionCondition 提取出来 PathRouterDecisionMaker注解元信息
        WebRouterDecisionCondition condition = (WebRouterDecisionCondition) requestCondition;
        Collection<PathRouterDecisionMaker> pathRouterDecisionMakers = condition.getContent();
        return CollectionUtils.isEmpty(pathRouterDecisionMakers) ? null : new ArrayList<>(pathRouterDecisionMakers).get(0);
    }
}

代码分析:

  • 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

六、相关文章

Kafka消息堆积问题排查

微服务重构:Mysql+DTS+Kafka+ElasticSearch解决跨表检索难题

基于SpringMVC的API灰度方案

SQL治理经验谈:索引覆盖

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

大模型安装部署、测试、接入SpringCloud应用体系

一文带你看懂:亿级大表垂直拆分的工程实践

亿级大表冷热分级的工程实践

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、实现原理
  • 三、实现方案
    • 1、定义决策器注解:@PathRouterDecisionMaker
      • 2、路由决策器:RouterDecisionMaker接口
        • 2.1 灰度决策器-ApiGrayDecisionMaker(返回true)
        • 2.2 灰度决策器-ApiNotGrayDecisionMaker(返回false)
        • 2.3 决策信息
        • 2.4 决策注解探测器
        • 2.5 自定义-路由匹配策略
      • 3、定义Controller:灰度和非灰度API
        • 4、MVC框架定制化工作
          • 4.1 MVC配置器裁剪:WebMvcRegistrations
          • 4.2 路由映射器裁剪:
          • AbstractRequestMappingHandlerMapping
          • 4.3 路由匹配器裁剪:PathMatcher
          • 4.4 总结
        • 5、测试
        • 四、源码解析
          • 1、预加载:RequestMappingInfo
            • 2、动态路由:
              • AbstractHandlerMethodMapping#lookupHandlerMethod
              • 五、总结
              • 六、相关文章
              相关产品与服务
              腾讯云代码分析
              腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档