前言
在实际开发中,由于业务需求的变更或协议本身的优化,HTTP接口的设计可能会经历显著的变化。为了确保这些变化不会对现有系统造成破坏,同时支持新功能的引入,我们需要在接口URL中明确指定版本号,以此来实现相同接口在不同版本下支持不同业务逻辑的目的。例如,原本用于添加用户的接口可能从/v1/user/add
演变为/v2/user/add
,以反映接口字段属性或逻辑的重大变更。为了有效管理这些不同版本的接口,在代码层面,我们应当采取一系列措施来确保每个版本的接口都能被清晰地区分、独立地维护,并且能够平滑地过渡。这包括但不限于使用模块化设计来隔离不同版本的业务逻辑,采用接口适配器模式来减少代码重复和耦合,以及利用路由工厂来动态地注册和管理路由。同时,我们还需要为每个版本的接口编写详尽的文档和测试,以确保其功能的正确性和稳定性,并随着时间的推移,逐步淘汰那些不再需要或维护的旧版本接口。
例如:
http://localhost:8080/v1/my/test
http://localhost:8080/v2/my/test 不兼容上一个版本
二、版本控制
对于历史版本的API支持,必须设定明确的时间限制和用户迁移策略。即,老版本的API在支持一段时间后应当被逐步淘汰,并鼓励新用户采用新版本API。否则,若API版本无限制地累积,达到10个甚至更多,将会给平台的维护带来极大的负担和复杂性。
API的变更在日常开发中虽属常态,但若变更频率过高,则需重新审视初始需求是否明确,以及每次变更API版本的真正目的何在。同时,还需关注不同版本API的使用者群体,确保变更不会对他们造成不必要的困扰。
值得注意的是,若API版本的更新仅仅是为了增加功能(如从不支持附件上传到支持附件上传),这种变更可能并不足以成为版本升级的理由。在此情况下,更合理的做法是单独发布一个专门用于附件上传的接口,以此降低不同功能之间的耦合度,使系统更加清晰和易于维护。
在开发接口的过程中,我们应始终铭记:功能的颗粒度应尽量细化(这需要在需求分析和模块设计阶段就予以考虑)。避免创建功能过于复杂、多合一的接口,这样的接口往往难以维护和扩展。
此外,为了保持版本的向下兼容性,我们应尽量通过增加字段和功能来实现新版本的迭代,而非删减现有功能。这样做可以确保旧版本客户端在升级前仍能正常使用API,减少因版本变更而带来的用户迁移成本和风险。
三、代码示例
在SpringMVC中RequestMappingHandlerMapping是比较重要的一个角色,它决定了每个URL分发至哪个Controller。
Spring Boot加载过程如下,所以我们可以通过自定义WebMvcRegistrationsAdapter来改写RequestMappingHandlerMapping。
通过在controller类和方法上添加注解@RequestMapping和自定义的@APIVersion(假设这是一个自定义注解用于处理版本信息),我们可以分别指定接口的URL和所需的版本号。DispatcherServlet作为Spring MVC的核心组件,负责解析进入的HTTP请求,并查找与之匹配的method handler(即控制器中的方法)。
DispatcherServlet的内部逻辑首先会遍历所有通过@RequestMapping注解标记的method handler,检查它们是否与当前请求的URL(包括path、method、header等条件,如果@RequestMapping中指定了这些条件)相匹配。在找到匹配的method handler候选集后,DispatcherServlet会进一步检查这些候选方法上是否使用了@APIVersion注解,并验证注解中指定的版本号是否与请求URL中指定的版本号相匹配。
最终,DispatcherServlet会选择那个既满足URL匹配条件又满足版本号匹配条件的method handler来处理当前的HTTP请求。这种方式确保了不同版本的API能够被正确地路由到相应的处理逻辑上,同时也为API的版本管理提供了一种灵活且易于扩展的机制。
1.自定义版本控制的注解
/**
* API版本控制注解
* Created on 2019/4/18 11:17.
* @author caogu
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
//标识版本号
int value();
}
2.自定义url匹配逻辑
当方法级别和类级别都有ApiVersion注解时,二者将进行合并(ApiVersionRequestCondition.combine)。最终将提取请求URL中版本号,与注解上定义的版本号进行比对,判断url是否符合版本要求。
/**
* 自定义url匹配逻辑
* Created on 2019/4/18 14:07.
*
* @author caogu
*/
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
// 路径中版本的前缀, 这里用 /v[1-9]/的形式
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");
//api的版本
private int apiVersion;
public ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
//将不同的筛选条件合并
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
//根据request查找匹配到的筛选条件
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
//return null;
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
Integer version = Integer.valueOf(m.group(1));
if (version >= this.apiVersion) {
return this;
}
}
return null;
}
//不同筛选条件比较,用于排序
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
//return 0;
// 优先匹配最新的版本号
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
}
5.测试结果