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
相关产品推荐
相关产品推荐

