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

Spring动态ObjectMapper实现 AOP设置命名策略仅首次生效问题

问题产生原因
  • Spring默认注入的ObjectMapper是单例Bean,全局所有请求共用同一个实例。在AOP中直接修改这个单例的配置,并发场景下不同请求的配置会互相覆盖,根本无法保证当前请求能拿到预期的命名策略。
  • Jackson的ObjectMapper内置了序列化/反序列化器缓存:第一次对某个Java类型执行序列化时,会根据当时生效的命名策略生成专属的序列化器,把属性名转换结果全部提前计算后存入缓存。后续就算你修改了setPropertyNamingStrategy的值,只要缓存没有被清空,还是会复用之前生成的序列化器,这就是为什么只有第一次设置的策略会生效。
  • 原有代码还有个隐藏bug:如果请求中没有传namingJson参数,namingJson.equals("SNAKE_CASE")会直接抛出空指针异常。
正确实现方案

不要修改全局单例ObjectMapper的配置,借助Spring提供的ResponseBodyAdvice扩展点+MappingJacksonValue动态指定单次请求的序列化规则即可,无侵入、没有并发和缓存问题。
实现逻辑如下:

  • 拦截所有Controller的返回值,在返回值被Jackson序列化之前,从请求参数中读取命名策略配置。
  • 对需要自定义命名策略的请求,拷贝全局ObjectMapper的配置生成一个全新的、请求级别的ObjectMapper实例,设置对应的命名策略,完全不影响全局单例的配置和缓存。
  • 用MappingJacksonValue包装原返回值,绑定自定义的ObjectMapper,后续Spring序列化时就会用这个专属实例生成JSON。

完整实现代码:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice
public class DynamicNamingAdvice implements ResponseBodyAdvice<Object> {

    private final ObjectMapper globalObjectMapper;

    // 构造注入全局ObjectMapper
    public DynamicNamingAdvice(ObjectMapper globalObjectMapper) {
        this.globalObjectMapper = globalObjectMapper;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 只拦截走Jackson JSON序列化的请求
        return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        String namingConfig = request.getServletRequest().getParameter("namingJson");
        // 没传配置参数 或者 返回体为空,直接走默认逻辑
        if (body == null || StringUtils.isBlank(namingConfig)) {
            return body;
        }

        // 拷贝全局ObjectMapper配置,生成请求专属的实例,不会影响全局实例
        ObjectMapper requestMapper = globalObjectMapper.copy();
        if ("SNAKE_CASE".equals(namingConfig)) {
            requestMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        } else if ("CAMEL_CASE".equals(namingConfig)) {
            requestMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
        } else {
            // 参数非法直接走默认逻辑
            return body;
        }

        // 包装返回值,绑定自定义序列化器
        MappingJacksonValue jacksonValue = new MappingJacksonValue(body);
        jacksonValue.setObjectMapper(requestMapper);
        return jacksonValue;
    }
}

方案说明

  • 用ObjectMapper.copy()生成的新实例会继承全局ObjectMapper的所有配置(比如时间格式、枚举序列化规则、空值处理策略等),不需要重复编写配置。
  • 每个请求使用独立的ObjectMapper实例,不存在并发安全问题,也不会触碰全局实例的序列化缓存,配置修改实时生效。
  • 如果需要支持String类型的返回值,额外判断返回值类型后用自定义的requestMapper手动序列化写出即可,常规JSON接口不需要额外处理。

内容的提问来源于stack exchange,提问作者Sakura

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.12 16:15:51