java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot RESTful API版本控制

SpringBoot RESTful API版本控制最佳方式

作者:李少兄

本文介绍了六种主流API版本控制策略,包括URI路径版本控制、请求参数版本控制、自定义请求头版本控制、内容协商版本控制、媒体类型参数版本控制和域名或子域名版本控制,每种策略都有其优缺点,并提供了最佳实践和适用场景

前言

在微服务架构、SaaS 平台、移动优先开发的时代,API 已成为系统间通信的“通用语言”。然而,业务需求永不停歇,数据模型持续演进。

若无有效的版本控制机制,每一次接口变更都可能引发“雪崩式”客户端崩溃。

核心挑战:如何在不破坏现有客户端的前提下,安全、可控地引入新功能?

HTTP 协议本身并未强制规定 API 版本控制方式,但 RFC 7231(HTTP/1.1)明确支持通过 内容协商(Content Negotiation) 实现资源的不同表示形式。这为 RESTful API 的版本控制提供了理论基础。

一、为什么需要 API 版本控制?

核心原则不要破坏现有客户端。新增功能应通过新版本暴露,而非修改旧接口。

二、六种主流 API 版本控制策略

1. URI 路径版本控制(URI Path Versioning)

原理

将版本号直接嵌入 URL 路径中,如 /api/v1/users

这是最直观、最广泛采用的方式,GitHub、Stripe、AWS 等均采用此策略。

最佳实践代码(Spring Boot)

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/v1/users/{id}")
    public ResponseEntity<UserV1> getUserV1(@PathVariable Long id) {
        UserV1 user = new UserV1("Alice");
        return ResponseEntity.ok(user);
    }

    @GetMapping("/v2/users/{id}")
    public ResponseEntity<UserV2> getUserV2(@PathVariable Long id) {
        UserV2 user = new UserV2("Alice Smith");
        return ResponseEntity.ok(user);
    }

    // DTOs
    public static class UserV1 {
        public String name;
        public UserV1(String name) { this.name = name; }
    }

    public static class UserV2 {
        public String fullName;
        public UserV2(String fullName) { this.fullName = fullName; }
    }
}

优点

缺点

适用场景

2. 请求参数版本控制(Query Parameter Versioning)

原理

通过 URL 查询参数指定版本,如 /users?id=123&version=v2

最佳实践代码

@RestController
public class UserController {

    @GetMapping("/users")
    public ResponseEntity<?> getUser(
            @RequestParam(defaultValue = "v1") String version,
            @RequestParam Long id) {

        return switch (version) {
            case "v1" -> ResponseEntity.ok(new UserV1("Alice"));
            case "v2" -> ResponseEntity.ok(new UserV2("Alice Smith"));
            default -> ResponseEntity.badRequest()
                    .body("Unsupported version: " + version);
        };
    }

    // DTOs 同上
}

优点

缺点

适用场景

3. 自定义请求头版本控制(Custom Header Versioning)

原理

使用自定义 HTTP Header(如 X-API-Version: 2)传递版本信息。

最佳实践代码

@RestController
public class UserController {

    @GetMapping("/users")
    public ResponseEntity<?> getUser(
            @RequestHeader(name = "X-API-Version", defaultValue = "1") String versionStr) {

        int version;
        try {
            version = Integer.parseInt(versionStr);
        } catch (NumberFormatException e) {
            return ResponseEntity.badRequest().body("Invalid version format");
        }

        return switch (version) {
            case 1 -> ResponseEntity.ok(new UserV1("Alice"));
            case 2 -> ResponseEntity.ok(new UserV2("Alice Smith"));
            default -> ResponseEntity.badRequest().body("Unsupported version: " + version);
        };
    }
}

优点

缺点

适用场景

4. 内容协商版本控制(Content Negotiation via Accept Header)

⭐ 这是 最符合 HTTP/REST 规范 的方式。

原理

利用 HTTP 标准的 Accept 请求头,通过自定义媒体类型(Media Type) 表达版本需求:

Accept: application/vnd.mycompany.v2+json

其中:

最佳实践代码(单一方法处理多版本)

@RestController
public class UserController {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @GetMapping(
        value = "/users",
        produces = {
            "application/vnd.mycompany.v1+json",
            "application/vnd.mycompany.v2+json"
        }
    )
    public ResponseEntity<String> getUser(
            @RequestHeader("Accept") String acceptHeader) {

        String version = parseVersionFromAccept(acceptHeader);
        if (version == null) {
            return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE)
                .body("Accept header must specify v1 or v2.");
        }

        String json;
        String mediaType;

        if ("v1".equals(version)) {
            json = toJson(new UserV1("Alice"));
            mediaType = "application/vnd.mycompany.v1+json";
        } else if ("v2".equals(version)) {
            json = toJson(new UserV2("Alice Smith"));
            mediaType = "application/vnd.mycompany.v2+json";
        } else {
            return ResponseEntity.badRequest().body("Unexpected version: " + version);
        }

        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(mediaType))
            .body(json);
    }

    private String parseVersionFromAccept(String accept) {
        if (accept == null) return null;
        if (accept.contains("vnd.mycompany.v1")) return "v1";
        if (accept.contains("vnd.mycompany.v2")) return "v2";
        return null;
    }

    private String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Serialization error", e);
        }
    }

    // DTOs
    public static class UserV1 {
        public String name;
        public UserV1(String name) { this.name = name; }
    }

    public static class UserV2 {
        public String fullName;
        public UserV2(String fullName) { this.fullName = fullName; }
    }
}

优点

缺点

适用场景

提示:你也可以使用 application/json;version=2 格式,但需自定义 ContentNegotiationManager,本文以 IANA 推荐的 vnd 方式为准。

5. 媒体类型参数版本控制(Media Type Parameters)

这是内容协商的一种变体,使用 MIME 类型的参数传递版本:

Accept: application/json;version=2

实现要点(需自定义 ContentNegotiation)

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(false)
                  .ignoreAcceptHeader(false)
                  .defaultContentType(MediaType.APPLICATION_JSON);
    }

    @Bean
    public ContentNegotiationManager contentNegotiationManager() {
        ContentNegotiationManager manager = new ContentNegotiationManager();
        // 默认策略保留
        return manager;
    }
}

控制器中解析参数:

@GetMapping("/users")
public ResponseEntity<?> getUser(@RequestHeader("Accept") String acceptHeader) {
    // 解析: application/json;version=2
    Map<String, String> params = parseMediaTypeParams(acceptHeader);
    String version = params.get("version");

    // ... 根据 version 构造响应
}

辅助方法:

private Map<String, String> parseMediaTypeParams(String accept) {
    Map<String, String> params = new HashMap<>();
    if (accept != null && accept.contains(";")) {
        String[] parts = accept.split(";");
        for (int i = 1; i < parts.length; i++) {
            String[] kv = parts[i].trim().split("=");
            if (kv.length == 2) {
                params.put(kv[0], kv[1].replaceAll("\"", ""));
            }
        }
    }
    return params;
}

优点

缺点

适用场景

6. 域名或子域名版本控制(Domain-based Versioning)

原理

通过不同子域名区分版本:

实现方式

优点

缺点

适用场景

三、对比总结表

策略是否符合 REST可读性缓存友好实现难度推荐度
URI 路径⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
查询参数❌❌⭐⭐⭐
自定义 Header⚠️⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Accept(vnd)✅✅✅⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Accept(参数)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
域名⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐(特定场景)

✅✅✅ = 完全符合 HTTP/REST 规范

推荐度:⭐ 最低,⭐⭐⭐⭐⭐ 最高

四、最佳实践建议

  1. 优先考虑内容协商(Accept + vnd):如果你的团队具备一定 REST 素养,这是最规范的方式。
  2. 次选 URI 路径:简单、直观、兼容性好,适合大多数企业内部系统。
  3. 避免使用查询参数:除非是临时方案。
  4. 统一版本策略:整个系统应采用同一种版本控制方式,避免混用。
  5. 文档化:在 OpenAPI/Swagger 中明确标注版本策略。
  6. 弃用策略:为旧版本设置 EOL(End of Life)时间,并通过 Deprecation 响应头通知客户端。

五、总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文