You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Spring+Jackson实现REST接口版本化动态属性命名策略

同一DTO适配不同版本接口的JSON字段命名方案(Spring + Jackson)

问题原因说明

你之前尝试通过反射修改运行时注解无效,是因为Jackson在类加载阶段就已解析并缓存了类上的@JsonNaming注解配置,运行时修改注解不会触发重新解析,因此无法改变序列化策略。下面提供两种可行的解决方案,无需重复创建DTO类。


方案一:基于ResponseBodyAdvice动态切换序列化策略

通过Spring的ResponseBodyAdvice拦截响应,根据请求的版本标识(如路径中的v1/v2)动态调整Jackson的命名策略,无需修改原有DTO注解。

实现代码

@RestControllerAdvice
public class VersionedNamingAdvice implements ResponseBodyAdvice<Object> {

    private final MappingJackson2HttpMessageConverter jacksonConverter;

    // 注入Spring默认的Jackson转换器,保留原有配置(如日期格式化、模块等)
    public VersionedNamingAdvice(MappingJackson2HttpMessageConverter jacksonConverter) {
        this.jacksonConverter = jacksonConverter;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅处理JSON响应和目标DTO类(可根据实际情况调整判断逻辑)
        return converterType.isAssignableFrom(MappingJackson2HttpMessageConverter.class)
                && returnType.getParameterType().isAssignableFrom(FooDTO.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        String requestPath = request.getURI().getPath();
        ObjectMapper originalMapper = jacksonConverter.getObjectMapper();
        // 克隆ObjectMapper,避免修改全局配置影响其他接口
        ObjectMapper customMapper = originalMapper.copy();

        // 根据路径版本切换命名策略
        if (requestPath.contains("/v2/")) {
            customMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        }
        // v1版本使用DTO类上的原有UpperCamelCase策略,无需额外配置

        try {
            // 处理String类型的返回值,避免重复序列化
            if (body instanceof String) {
                return body;
            }
            return customMapper.writeValueAsString(body);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON序列化失败", e);
        }
    }
}

方案二:基于媒体类型协商的多Converter配置

遵循RESTful版本控制规范,通过自定义媒体类型(如application/vnd.yourapp.v1+json)区分版本,配置不同的Jackson转换器对应不同的命名策略。

1. 配置ObjectMapper和消息转换器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 对应v1版本的UpperCamelCase策略
    @Bean("camelCaseMapper")
    public ObjectMapper camelCaseObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE);
        // 保留原有全局配置(如日期格式化、序列化模块等)
        return mapper;
    }

    // 对应v2版本的SnakeCase策略
    @Bean("snakeCaseMapper")
    public ObjectMapper snakeCaseObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        return mapper;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // v1版本转换器
        MappingJackson2HttpMessageConverter v1Converter = new MappingJackson2HttpMessageConverter(camelCaseObjectMapper());
        v1Converter.setSupportedMediaTypes(Collections.singletonList(MediaType.valueOf("application/vnd.yourapp.v1+json")));
        converters.add(v1Converter);

        // v2版本转换器
        MappingJackson2HttpMessageConverter v2Converter = new MappingJackson2HttpMessageConverter(snakeCaseObjectMapper());
        v2Converter.setSupportedMediaTypes(Collections.singletonList(MediaType.valueOf("application/vnd.yourapp.v2+json")));
        converters.add(v2Converter);

        // 保留默认转换器处理其他请求
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

2. Controller中指定版本媒体类型

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

    // v1接口,返回UpperCamelCase格式
    @GetMapping(value = "/foo", produces = "application/vnd.yourapp.v1+json")
    public FooDTO getFooV1() {
        return new FooDTO("demoValue", 123);
    }

    // v2接口,返回SnakeCase格式
    @GetMapping(value = "/foo", produces = "application/vnd.yourapp.v2+json")
    public FooDTO getFooV2() {
        return new FooDTO("demoValue", 123);
    }
}

客户端请求时,可通过Accept头指定版本(如Accept: application/vnd.yourapp.v2+json),或通过路径参数动态绑定媒体类型。


方案选择建议

  • 若接口版本通过路径区分(如/api/v1/foo),优先选择方案一,实现简单直接。
  • 若遵循RESTful规范,推荐方案二,通过媒体类型协商控制版本,扩展性更强。

内容的提问来源于stack exchange,提问作者João Castro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 17:55:30