搜索 K
Appearance
Appearance
版本路由由两个类协作完成:ApiVersionConfig 负责装配,APIVersionHandlerMapping 负责在路由注册阶段注入版本前缀。
Spring MVC 在启动时会遍历所有 @Controller Bean,把每个 @RequestMapping 注解解析成 RequestMappingInfo,然后调用 registerHandlerMethod(handler, method, mapping) 注册到内部的路由表。本模块在 registerHandlerMethod 这一步切入,对带 @APIVersion 的方法或类的 RequestMappingInfo 做一次「前缀合并」。
WebMvcRegistrations 是 Spring Boot 提供的官方扩展点,允许业务方在不放弃 Spring Boot 默认配置的前提下替换 MVC 内部的几个核心组件。ApiVersionConfig 实现 getRequestMappingHandlerMapping() 返回我们的自定义 mapping,Spring Boot 自动注册。
@Configuration
public class ApiVersionConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new APIVersionHandlerMapping();
}
}APIVersionHandlerMapping 只重写两个方法:
public class APIVersionHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
APIVersion apiVersion = AnnotationUtils.findAnnotation(method, APIVersion.class);
if (apiVersion == null) {
apiVersion = AnnotationUtils.findAnnotation(method.getDeclaringClass(), APIVersion.class);
}
if (apiVersion != null) {
String[] versionPaths = apiVersion.value().path();
RequestMappingInfo.BuilderConfiguration options = new RequestMappingInfo.BuilderConfiguration();
if (getPatternParser() != null) {
options.setPatternParser(getPatternParser());
} else {
configureLegacyOptions(options);
}
RequestMappingInfo prefixInfo = RequestMappingInfo.paths(versionPaths)
.options(options)
.build();
mapping = prefixInfo.combine(mapping);
}
super.registerHandlerMethod(handler, method, mapping);
}
}三个关键点:
PathPatternParser,但允许通过 spring.mvc.pathmatch.matching-strategy=ant-path-matcher 回退到 AntPathMatcher。两套策略下构造 RequestMappingInfo 的选项不同,所以代码同时支持两条分支prefixInfo.combine(mapping):Spring 内置的合并语义会把前缀的路径与原 mapping 的路径做笛卡尔拼接,例如 /v1 combine /user/{id} 得到 /v1/user/{id}。同时方法、参数、消费/生产媒体类型等约束也会按 Spring 规则合并public enum APIVersionEnum {
V1("/v1", "v1"),
V2("/v2", "这里可以提供备注,例如:权限模块需要统一迁移到v2,v1版本即将废弃");
private final String[] path;
private final String displayName;
APIVersionEnum(String path, String displayName) {
this.path = new String[]{path};
this.displayName = displayName;
}
}把版本前缀做成枚举而不是任意字符串,至少带来三个好处:
path 是 String[] 而不是单个 String,是为了和 Spring RequestMappingInfo.paths(String...) 的签名直接对齐,方便后续支持「同一版本多前缀」的需求。
Spring 5 引入的 configurePathMatch().addPathPrefix(predicate) 也能给一批 Controller 加前缀,但它的局限:
Class<?> -> Boolean,按类粒度匹配,不支持方法粒度的细分所以选择更底层的 RequestMappingHandlerMapping 扩展,自己控制每一步。
只需在 APIVersionEnum 加一行:
V3("/v3", "v3");业务代码 @APIVersion(APIVersionEnum.V3) 即可立刻使用,不需要改 mapping、不需要改 config。
如果未来要支持「同一 URL 通过 Accept-Version Header 区分版本」,可以在 registerHandlerMethod 内不修改 path,而是用 RequestMappingInfo 的 headers(...) 约束,把 Accept-Version=v1 拼进 mapping。逻辑同样收敛在这个方法里,业务代码无感。
可以仿照 MutualExclusionProcessor 的思路(详见 设计文档 · 编译期互斥校验),编写一个 APT 处理器,扫描 @APIVersion 元素并在类与方法同时标注时给出编译错误。