Spring Boot API输入数据版本化实现方案咨询
基于Spring Boot的API输入版本化解决方案
针对你的需求,结合Accept请求头的版本化策略,可以通过版本化DTO+内部统一领域模型的方式实现新旧客户端的兼容,同时避免重复代码,还能方便后续版本废弃。以下是具体实现方案:
一、核心思路
- 用不同版本的DTO对应客户端的JSON结构,DTO仅负责与客户端的协议适配
- 所有版本的DTO都转换为内部统一的领域模型,业务逻辑和XML生成完全基于该模型,避免重复代码
- 通过Spring的内容协商机制,根据Accept头自动匹配对应版本的DTO处理逻辑
- 监控各版本请求量,安全识别并废弃无人使用的旧版本
二、具体实现步骤
1. 配置Spring内容协商(识别Accept头版本)
在application.yml中配置媒体类型与版本的映射,让Spring能识别Accept: application/vnd.yourcompany.v1+json和Accept: application/vnd.yourcompany.v2+json这类请求头:
spring: mvc: contentnegotiation: favor-path-extension: false favor-parameter: false media-types: v1: application/vnd.yourcompany.v1+json v2: application/vnd.yourcompany.v2+json
或者通过配置类自定义:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer .ignoreAcceptHeader(false) .defaultContentType(MediaType.APPLICATION_JSON) .mediaType("v1", MediaType.valueOf("application/vnd.yourcompany.v1+json")) .mediaType("v2", MediaType.valueOf("application/vnd.yourcompany.v2+json")); } }
2. 定义版本化DTO与统一领域模型
为避免重复代码,DTO仅保留与客户端交互的结构,核心字段复用内部模型:
内部统一领域模型(业务逻辑唯一依赖)
@Data @Builder(toBuilder = true) public class CustomerDomain { private PersonalDetails personalDetails; private ContractDetails contractDetails; } @Data @Builder public class PersonalDetails { private String dateOfBirth; private String occupation; } @Data @Builder public class ContractDetails { private String startDate; }
V1版本DTO(对应旧JSON结构)
@Data @AllArgsConstructor(access = AccessLevel.PACKAGE) @NoArgsConstructor(access = AccessLevel.PACKAGE) @Builder(toBuilder = true) public class CustomerV1DTO { @NotEmpty private String dateOfBirth; @NotNull @Pattern(regexp="^[A-Z]{3}", message=INVALID_CODE) private String occupation; @Valid @NotNull private ContractDetails contractDetails; // 转换为内部领域模型 public CustomerDomain toDomain() { return CustomerDomain.builder() .personalDetails(PersonalDetails.builder() .dateOfBirth(this.dateOfBirth) .occupation(this.occupation) .build()) .contractDetails(this.contractDetails) .build(); } }
V2版本DTO(对应新JSON结构)
@Data @AllArgsConstructor(access = AccessLevel.PACKAGE) @NoArgsConstructor(access = AccessLevel.PACKAGE) @Builder(toBuilder = true) public class CustomerV2DTO { @Valid @NotNull private PersonalDetails personalDetails; @Valid @NotNull private ContractDetails contractDetails; // 转换为内部领域模型 public CustomerDomain toDomain() { return CustomerDomain.builder() .personalDetails(this.personalDetails) .contractDetails(this.contractDetails) .build(); } }
3. Controller中处理不同版本请求
通过consumes属性指定对应版本的媒体类型,自动绑定到对应DTO:
@RestController @RequestMapping("/api/customers") public class CustomerController { @PostMapping(consumes = "application/vnd.yourcompany.v1+json") public ResponseEntity<Void> handleV1Request(@Valid @RequestBody CustomerV1DTO customerV1) { processCustomer(customerV1.toDomain()); return ResponseEntity.ok().build(); } @PostMapping(consumes = "application/vnd.yourcompany.v2+json") public ResponseEntity<Void> handleV2Request(@Valid @RequestBody CustomerV2DTO customerV2) { processCustomer(customerV2.toDomain()); return ResponseEntity.ok().build(); } // 统一业务逻辑:转换为XML并发送 private void processCustomer(CustomerDomain domain) { // 此处编写生成XML、调用外部应用的逻辑 } }
4. 版本废弃与监控
- 标记废弃:在旧版本Controller方法上添加
@Deprecated注解,同时在响应头中告知客户端:@PostMapping(consumes = "application/vnd.yourcompany.v1+json") @Deprecated public ResponseEntity<Void> handleV1Request(@Valid @RequestBody CustomerV1DTO customerV1) { HttpHeaders headers = new HttpHeaders(); headers.add("Deprecated", "true"); headers.add("Sunset", "2024-12-31T00:00:00Z"); // 废弃截止日期 processCustomer(customerV1.toDomain()); return ResponseEntity.ok().headers(headers).build(); } - 监控与删除:通过Spring Boot Actuator、Prometheus等工具统计各版本请求量,当某版本持续无调用时,即可安全删除对应的DTO和Controller方法。
三、替代方案:单模型兼容(小版本变更适用)
如果只是小结构变更,不想定义多DTO,可以用Jackson注解在单个模型中兼容新旧结构:
@Data @Builder(toBuilder = true) public class Customer { @Valid @NotNull private PersonalDetails personalDetails; // 旧版本字段仅用于反序列化V1请求 @JsonProperty(value = "dateOfBirth", access = JsonProperty.Access.WRITE_ONLY) private String dateOfBirth; @JsonProperty(value = "occupation", access = JsonProperty.Access.WRITE_ONLY) private String occupation; @Valid @NotNull private ContractDetails contractDetails; // 自动将旧字段映射到personalDetails @JsonSetter public void setDateOfBirth(String dateOfBirth) { if (personalDetails == null) personalDetails = new PersonalDetails(); personalDetails.setDateOfBirth(dateOfBirth); } @JsonSetter public void setOccupation(String occupation) { if (personalDetails == null) personalDetails = new PersonalDetails(); personalDetails.setOccupation(occupation); } }
这种方式适合小范围结构调整,大版本变更还是推荐多DTO+统一领域模型的方案,更易维护和扩展。
内容的提问来源于stack exchange,提问作者runnerpaul
相关产品推荐
相关产品推荐

