在微服务重构时,我们常遇到这个业务场景:同样是/api/test,我们实现了新逻辑和老逻辑,然后根据定制的灰度策略,通常灰度API和老API两者都需要支持用户使用。
那么是否有比较好解决方案,协助我们完成同名同方法同参数列表的API灰度动态路由的方案呢?
我们就基于SpringMVC,通过对底层RequestMappingInfo的参数定制化,实现了methodHandler的动态路由决策,从而达到API灰度动态路由目的。
我们总的来说,干了两件事情:
@Target({ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PathRouterDecisionMaker {
Class<? extends RouterDecisionMaker> decision();
String resourceCondition() default "";
int order() default 0;
}
分析:
注解(Annotation):仅提供附加元数据支持,并 不能实现任何操作,需要另外的 Scanner 根据元数据执行相应操作。
RouterDecisionMaker接口,定义了决策器的匹配策略方法
public interface RouterDecisionMaker {
/**
* 路由决策器的最终决策方法
* @param pathPartRequest
* @return 匹配返回的资源类型
*/
boolean matches(PathPartRequest pathPartRequest);
}
@Component(value = "ApiGrayDecisionMaker")
public class ApiGrayDecisionMaker implements RouterDecisionMaker {
@Override
public boolean matches(PathPartRequest pathPartRequest) {
return Boolean.TRUE;
}
}
代码分析:
@Component("ApiNotGrayDecisionMaker")
public class ApiNotGrayDecisionMaker implements RouterDecisionMaker {
/**
* 取反,跟 ApiGrayDecision#matches 互斥
* @param pathPartRequest
* @return
*/
@Override
public boolean matches(PathPartRequest pathPartRequest) {
return Boolean.FALSE;
}
}
代码分析:
RouterPathRequest,提供数据给决策器
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
}
代码分析:
WebRouterDecisionMakerDetection
/**
* PathRouterDecisionMaker注解提取器
* - 从方法注释提取注解
*/
public class WebRouterDecisionMakerDetection {
public PathRouterDecisionMaker detect(Method handlerMethod) {
if (Objects.isNull(handlerMethod)) {
return null;
}
return AnnotatedElementUtils.findMergedAnnotation(handlerMethod, PathRouterDecisionMaker.class);
}
}
WebRouterDecisionCondition
继承了 AbstractRequestCondition,会在创建RequestMappingInfo的填入customCondition条件时,被回调使用。
1、AbstractRequestCondition:这是一个抽象类,实现了 RequestCondition 接口,并提供了一1些默认实现。它简化了自定义条件的实现过程。 2、RequestCondition:这是一个接口,定义了用于匹配请求的条件。它包含两个主要方法: - getMatchingCondition(HttpServletRequest request):返回与给定请求匹配的条件。 - combine(RequestCondition<?> other):将当前条件与其他条件组合。
/**
* 自定义的路由匹配条件
*/
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
@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..";
}
}
代码分析:
WebMvcRegistrations 是 Spring MVC 框架中的一个接口,用于自定义 Spring MVC 的配置。通过实现这个接口,你可以注册自定义的 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 和其他与 Spring MVC 相关的组件。
WebRequestMappingRegistrationsConfig 是一个配置类
@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
在Spring MVC中,请求条件用于决定一个特定的HTTP请求是否应该被一个控制器方法处理。
@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);
}
}
代码分析:
WebRouterPathConstraintMatcher,实现了PathMatcher接口,实现了从 HttpServletRequest 提取 @PathRouterDecisionMaker注解元信息,灰度决策器PathRouterDecisionMaker
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);
}
}
代码分析:
通过以上的配置,我们实现了MVC框架的定制化工作,通过ServeltHttpRequest,将@PathRouterDecisionMaker注解元信息透传给了路由映射器,再通过灰度决策器确认是否返回RequestMappingInfo。
访问:http://localhost:8853/test_constraint,实现路由的动态匹配,请求抵达新API灰度的MethodHandler并被处理完成。
上面分析了实现原理和实现方案,下面则从MVC初始化的角度来分析。
初始化入口是:AbstractRequestMappingHandlerMapping抽象类的initHandlerMethods
预加载是:通过MappingRegistry,将原API和灰度API的RequestMappingInfo信息,注册到mappingLookup这个Map里(key是RequestMappingInfo,value是HandlerMethod)
本质是:RequestMappingInfo里面包含了路由策略,我们通过在handlerMethod方法上打注解,如此透传到RequestMappingInfo#customConditionHolder;解决了SpringBoot启动服务,规避了RequestMappingHandler重复异常
核心是:AbstractHandlerMethodMapping.MappingRegistry#register
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对象
1、会回调到WebRequestMappingHandlerMapping#handleMatch,即,RequestMappingInfoHandlerMapping#handleMatch,很简单,
2、进入到AbstractHandlerMethodMapping#lookupHandlerMethod
3、返回methodHandler
以上是基于SpringMVC的接口动态灰度方案的一些讲解,实际工程问题上,还会遇到一些其他问题,比如:
上面源码可以参考我的个人github项目:https://github.com/bryantmo/springcloud_test
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。