第6章 构建 RESTful 服务
6.1 RESTful 简介
6.2 构建 RESTful 应用接口
6.3 使用 Swagger 生成 Web API 文档
6.4 实战:实现 Web API 版本控制
6.4 实战:实现 Web API 版本控制
如果业务需求变更,Web API 功能发生变化时应该如何处理呢?总不能通知所有的调用方修改吧?接下来好好研究一下 Web API 的版本控制。
6.4.1 为什么进行版本控制
一般来说,Web API 是提供给其他系统或其他公司使用的,不能随意频繁地变更。然而,由于需求和业务不断变化,Web API 也会随之不断修改。如果直接对原来的接口修改,势必会影响其他系统的正常运行。
例如,系统中用户添加的接口 /api/user 由于业务需求的变化,接口的字段属性也发横了变化,而且可能与之前的功能不兼容。为了保证原有的接口调用方不受影响,只能重新定义一个新的接口:/api/user2,这使得接口非常臃肿难看,而且极难维护。
那么如何做到在不影响现有调用方的情况下,优雅地更新接口的功能呢?最简单高效的办法就是对 Web API 进行有效的版本控制。通过增加版本号来区分对应的版本,来满足各个接口调用方的需求。版本号的使用有以下几种方式:
- 通过
域名
进行区分,即不同的版本使用不同的域名,如 v1.api.test.com、v2.api.test.com。 - 通过
请求URL路径
进行区分,在同一个域名下使用不同的 URL 路径,如 test.com/api/v1/、test.com/api/v2/。 - 通过
请求参数
进行区分,在同一个 URL 路径下增加 version=v1 或 v2 等,然后根据不同的版本选择执行不同的方法。
在实际的项目中,一般选择第二种方式,因为这样既能保证水平扩展,又不影响以前的老版本。
6.4.2 Web API 的版本控制
Spring Boot 对 RESTful 的支持非常全面,因而实现 RESTful API 非常简单,同样对于 API 版本控制也有相应的实现方案,实现步骤如下:
1、API版本控制配置
(1)创建自定义的 @ApiVersion 注解。
(2)创建自定义 URL 匹配规则 ApiVersionCondition 类(实现 RequestCondition 接口)。
(3)创建自定义的映射处理程序 ApiRequestMappingHandlerMapping 类(继承 RequestMappingHandlerMapping 类)。
(4)创建 WebMvcRegistrationsConfig 配置类(实现 WebMvcRegistrations 接口),将自定义的映射处理程序 ApiRequestMappingHandlerMapping 注册到系统中。
2、配置实现接口
编写测试的控制器,实现相关接口的测试。
1、API版本控制配置
(1)创建自定义注解
创建一个自定义版本号标记注解 @ApiVersion。
ApiVersion.java
package com.example.restfulproject.comm.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义版本号标记注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ApiVersion {
/**
* @return 版本号
*/
int value() default 1;
}
在上面的示例中,创建了 @ApiVersion 自定义注解用于 API 版本控制,并返回了对应的版本号。
(2)自定义 URL 匹配逻辑
创建 ApiVersionCondition 类并实现 RequestCondition 接口,重写 combine()、getMatchingCondition()、compareTo() 方法,其作用是进行版本号筛选,将提取请求URL中的版本号
与注解上定义的版本号
进行对比,以此来判断某个请求应落在哪个控制器上。
ApiVersionCondition.java
package com.example.restfulproject.comm.condition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 自定义 URL 匹配逻辑
*/
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");
private int apiVersion;
public ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
private int getApiVersion() {
return apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if(m.find()) {
Integer version = Integer.valueOf(m.group(1));
if(version >= this.apiVersion) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return other.getApiVersion() - this.apiVersion;
}
}
(3)自定义匹配的处理程序
创建 ApiRequestMappingHandlerMapping 类并继承 RequestMappingHandlerMapping 类,重写 getCustomTypeCondition()、getCustomMethodCondition() 方法,实现自定义的匹配处理程序。
ApiRequestMappingHandlerMapping.java
package com.example.restfulproject.comm.processor;
import com.example.restfulproject.comm.annotation.ApiVersion;
import com.example.restfulproject.comm.condition.ApiVersionCondition;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
/**
* 自定义匹配的处理程序
*/
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final String VERSION_FLAG = "{version}";
private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) {
RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
if(classRequestMapping == null) {
return null;
}
StringBuilder mappingUrlBuilder = new StringBuilder();
if(classRequestMapping.value().length > 0) {
mappingUrlBuilder.append(classRequestMapping.value()[0]);
}
String mappingUrl = mappingUrlBuilder.toString();
if (!mappingUrl.contains(VERSION_FLAG)) {
return null;
}
ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
}
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return createCondition(handlerType);
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return createCondition(method.getClass());
}
}
(4)配置注册自定义的 RequestMappingHandlerMapping
创建 WebMvcRegistrationsConfig 配置类并实现 WebMvcRegistrations 接口,重写 getRequestMappingHandlerMapping() 方法,将上面创建的 ApiRequestMappingHandlerMapping 类注册到系统中。
WebMvcRegistrationsConfig.java
package com.example.restfulproject.comm.config;
import com.example.restfulproject.comm.processor.ApiRequestMappingHandlerMapping;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* 配置注册自定义的 RequestMappingHandlerMapping
*/
@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiRequestMappingHandlerMapping();
}
}
2、配置实现接口
配置完成之后,接下来编写测试的控制器(Controller),实现相关接口的测试。在 Controller 目录下分别创建 OrderV1Controller 和 OrderV2Controller。示例代码如下:
OrderV1Controller.java
package com.example.restfulproject.controller;
import com.example.restfulproject.comm.annotation.ApiVersion;
import com.example.restfulproject.comm.utils.JSONResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* V1 版本的接口定义
*/
@ApiVersion(value = 1)
@RestController
@RequestMapping("/api/{version}/order") // 使用 @RequestMapping({"/api/order", "/api/{version}/order"}),可以保证升级接口后,原有接口 /api/order 的调用方不受影响。
public class OrderV1Controller {
@GetMapping("/delete/{orderId}")
public JSONResult deleteOrderById(@PathVariable String orderId) {
System.out.println("V1 删除订单成功:" + orderId);
return JSONResult.ok("V1 删除订单成功");
}
@GetMapping("/detail/{orderId}")
public JSONResult queryOrderById(@PathVariable String orderId) {
System.out.println("V1 获取订单详情成功:" + orderId);
return JSONResult.ok("V1 获取订单详情成功");
}
}
备注:上面的示例中,假设原有接口是 /api/order,使用 @RequestMapping({“/api/order”, “/api/{version}/order”}),可以保证升级接口后,原有接口的调用方不受影响。
OrderV2Controller.java
package com.example.restfulproject.controller;
import com.example.restfulproject.comm.annotation.ApiVersion;
import com.example.restfulproject.comm.utils.JSONResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* V2 版本的接口定义
*/
@ApiVersion(value = 2)
@RestController
@RequestMapping("/api/{version}/order")
public class OrderV2Controller {
@GetMapping("/detail/{orderId}")
public JSONResult queryOrderById(@PathVariable String orderId) {
System.out.println("V2 获取订单详情成功:" + orderId);
return JSONResult.ok("V2 获取订单详情成功");
}
@GetMapping("/list")
public JSONResult list() {
System.out.println("V2 新增list订单列表接口");
return JSONResult.ok(200, "V2 新增list订单列表接口");
}
}
3、验证测试
启动项目,查看版本控制是否生效。
(1)删除订单接口:
http://localhost:8080/api/v1/order/delete/20011
http://localhost:8080/api/v2/order/delete/20011
(2)获取订单详情接口:
http://localhost:8080/api/v1/order/detail/20011
http://localhost:8080/api/v2/order/detail/20011
(3)新增订单列表接口:
http://localhost:8080/api/v1/order/list
http://localhost:8080/api/v2/order/list
(4)获取V5版本(假设V5版本不存在,最新版本只有V2)的订单详情接口:
http://localhost:8080/api/v5/order/detail/20011
以上验证情况说明 Web API 版本控制配置成功,实现了旧版本的稳定和新版本的更新。
- 当请求正确的版本地址时,会自动匹配版本的对应接口。(示例:“获取订单详情接口”)
- 当请求的版本大于当前版本时,默认匹配最新的版本。(示例:“获取V5版本的订单详情接口”)
- 高版本会默认继承低版本的所有接口。实现版本升级只关注变化的部分,没有变化的部分会自动平滑升级,这就是所谓的版本继承。(示例:“删除订单接口”)
- 高版本的接口的新增和修改不会影响低版本。(示例:“新增订单列表接口”)
这些特性使得在升级接口时,原有接口不受影响,只关注变化的部分,没有变化的部分自动平滑升级。这样使得 Web API 更加简洁,这就是实现 Web API 版本控制的意义所在。
来源:《Spring Boot 从入门到实战》学习笔记