1. Spring MVC消息转换器基础解析
在Spring MVC框架中,消息转换器(HttpMessageConverter)扮演着请求/响应数据格式转换的关键角色。当客户端发送JSON数据到Controller时,或者Controller返回Java对象需要转换为JSON响应时,消息转换器就在幕后默默工作。典型的处理流程是:DispatcherServlet接收到请求后,根据请求头中的Content-Type和Accept字段,选择匹配的消息转换器进行数据转换。
Spring默认已经提供了多种消息转换器实现:
- MappingJackson2HttpMessageConverter:处理JSON格式数据
- StringHttpMessageConverter:处理普通文本数据
- ByteArrayHttpMessageConverter:处理字节数组数据
- FormHttpMessageConverter:处理表单数据
实际开发中最常用的是JSON格式的数据交互,这也是为什么90%的项目都会引入Jackson库作为JSON处理器。但默认配置可能无法满足所有需求场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么需要扩展消息转换器
默认的消息转换器配置在以下场景中会显得力不从心:
- 特殊日期格式处理:LocalDateTime等Java 8时间类型默认序列化为长整型时间戳,而前端可能需要"yyyy-MM-dd HH:mm:ss"格式
- 统一数据包装:所有响应需要包裹在固定结构的JSON对象中(如包含code、message、data字段)
- 自定义序列化规则:某些字段需要特殊处理(如密码字段只显示*号)
- 多格式支持:同一API需要支持JSON和XML等多种数据格式
- 特殊类型处理:如BigDecimal需要固定小数位数输出
我曾在一个电商项目中遇到典型问题:前端需要订单日期显示为"2023-07-15 14:30:00"格式,而后台使用LocalDateTime存储。默认配置产生的长整型时间戳导致前端需要额外转换,增加了不必要的复杂度。
3. 自定义消息转换器实现步骤
3.1 继承AbstractHttpMessageConverter
创建自定义转换器最可靠的方式是继承AbstractHttpMessageConverter抽象类:
java复制public class CustomMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
public CustomMessageConverter() {
// 定义支持的媒体类型
super(MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"));
this.objectMapper = new ObjectMapper();
// 配置ObjectMapper
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
@Override
protected boolean supports(Class<?> clazz) {
// 指定支持转换的类类型
return true; // 支持所有类型
}
@Override
protected Object readInternal(Class<?> clazz,
HttpInputMessage inputMessage) throws IOException {
// 实现反序列化逻辑
return objectMapper.readValue(inputMessage.getBody(), clazz);
}
@Override
protected void writeInternal(Object object,
HttpOutputMessage outputMessage) throws IOException {
// 实现序列化逻辑
String json = objectMapper.writeValueAsString(object);
outputMessage.getBody().write(json.getBytes());
}
}
3.2 配置自定义转换器
在Spring Boot中,通过WebMvcConfigurer接口配置:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 移除默认的Jackson转换器
converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter);
// 添加自定义转换器
converters.add(new CustomMessageConverter());
}
}
关键点:通常需要移除默认的Jackson转换器,避免多个转换器冲突。Spring会按照转换器列表顺序选择第一个支持当前类型的转换器。
4. 高级扩展技巧
4.1 统一响应封装
在实际项目中,通常会定义统一的响应结构:
java复制public class Result<T> {
private int code;
private String message;
private T data;
// 构造方法和getter/setter省略
}
可以在自定义转换器中自动包装返回值:
java复制@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException {
// 如果不是Result类型,则进行包装
Object value = (object instanceof Result) ? object
: Result.success(object);
String json = objectMapper.writeValueAsString(value);
outputMessage.getBody().write(json.getBytes());
}
4.2 处理LocalDateTime
Java 8时间类型的处理需要特别注意:
- 添加Jackson的JavaTimeModule:
java复制objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- 自定义日期格式:
java复制objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
4.3 多格式支持
可以通过判断请求头中的Accept字段来支持多种数据格式:
java复制public class MultiFormatMessageConverter extends AbstractHttpMessageConverter<Object> {
private final List<HttpMessageConverter<?>> converters;
public MultiFormatMessageConverter() {
super(MediaType.ALL); // 支持所有媒体类型
this.converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new Jaxb2RootElementHttpMessageConverter());
}
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
protected Object readInternal(Class<?> clazz,
HttpInputMessage inputMessage) throws IOException {
// 根据Content-Type选择转换器
MediaType contentType = inputMessage.getHeaders().getContentType();
for (HttpMessageConverter<?> converter : converters) {
if (converter.canRead(clazz, contentType)) {
return ((HttpMessageConverter<T>) converter).read(clazz, inputMessage);
}
}
throw new HttpMessageNotReadableException("No converter found");
}
@Override
protected void writeInternal(Object object,
HttpOutputMessage outputMessage) throws IOException {
// 根据Accept选择转换器
MediaType acceptType = outputMessage.getHeaders().getAccept().get(0);
for (HttpMessageConverter<?> converter : converters) {
if (converter.canWrite(object.getClass(), acceptType)) {
((HttpMessageConverter<T>) converter).write(object, acceptType, outputMessage);
return;
}
}
throw new HttpMessageNotWritableException("No converter found");
}
}
5. 常见问题与解决方案
5.1 转换器不生效的可能原因
-
顺序问题:Spring按转换器列表顺序选择第一个匹配的转换器。确保自定义转换器在默认转换器之前。
解决方案:
java复制@Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(0, new CustomMessageConverter()); } -
媒体类型不匹配:请求头中的Content-Type或Accept与转换器支持的媒体类型不匹配。
-
类型不支持:supports方法返回false,导致转换器被跳过。
5.2 日期格式化问题
典型问题:前端收到的日期格式不一致,有时是时间戳,有时是字符串。
解决方案:
- 统一禁用时间戳格式:
java复制objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- 注册JavaTimeModule:
java复制objectMapper.registerModule(new JavaTimeModule());
- 设置全局日期格式:
java复制objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
5.3 循环引用问题
当对象之间存在双向引用时,Jackson会陷入无限循环。
解决方案:
- 使用@JsonIgnore注解忽略一方:
java复制@JsonIgnore
private User createdBy;
- 使用@JsonManagedReference和@JsonBackReference:
java复制@JsonManagedReference
private List<Order> orders;
@JsonBackReference
private Customer customer;
- 配置Jackson忽略循环引用:
java复制objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
6. 性能优化建议
-
重用ObjectMapper:ObjectMapper的创建成本较高,应该在自定义转换器中重用同一个实例。
-
缓存转换结果:对于不经常变化的数据,可以考虑缓存序列化结果。
-
选择合适的JSON库:根据性能测试,在Jackson、Gson和Fastjson中,Jackson通常表现最稳定。
-
启用缓冲:在write方法中使用缓冲流提高IO性能:
java复制@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException {
try (OutputStream out = new BufferedOutputStream(outputMessage.getBody())) {
objectMapper.writeValue(out, object);
}
}
- 线程安全:确保自定义转换器是线程安全的,避免使用共享可变状态。
7. 测试自定义消息转换器
编写测试验证自定义转换器是否正常工作:
java复制@SpringBootTest
@AutoConfigureMockMvc
public class MessageConverterTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testLocalDateTimeFormat() throws Exception {
mockMvc.perform(get("/api/time")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.time").value("2023-07-15 14:30:00"));
}
@Test
public void testResponseWrapper() throws Exception {
mockMvc.perform(get("/api/data")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").exists());
}
}
测试要点:
- 验证日期格式是否符合预期
- 验证响应是否被正确包装
- 验证错误处理是否一致
- 测试不同Content-Type和Accept头
8. 实际项目中的应用案例
在电商项目中,我们通过自定义消息转换器解决了以下问题:
- 订单日期显示:将LocalDateTime统一格式化为"yyyy-MM-dd HH:mm:ss"
- 金额格式化:BigDecimal保留两位小数
- 敏感数据脱敏:手机号显示为"138****1234"
- 枚举值转换:将枚举转换为包含code和name的对象
- 空值处理:忽略null字段,减少传输数据量
实现代码片段:
java复制public class CustomMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
public CustomMessageConverter() {
super(MediaType.APPLICATION_JSON);
this.objectMapper = new ObjectMapper();
// 忽略null字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 注册自定义序列化器
SimpleModule module = new SimpleModule();
module.addSerializer(BigDecimal.class, new BigDecimalSerializer());
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
module.addSerializer(String.class, new PhoneNumberSerializer());
objectMapper.registerModule(module);
}
static class BigDecimalSerializer extends JsonSerializer<BigDecimal> {
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString(value.setScale(2, RoundingMode.HALF_UP).toString());
}
}
static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
private final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void serialize(LocalDateTime value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString(value.format(formatter));
}
}
static class PhoneNumberSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
if (value != null && value.length() == 11) {
gen.writeString(value.substring(0, 3) + "****" + value.substring(7));
} else {
gen.writeString(value);
}
}
}
}
9. 与其他技术的集成
9.1 与Spring Boot Actuator集成
当项目中使用Actuator时,需要注意自定义转换器可能会影响Actuator端点的响应格式。可以通过以下方式解决:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 只处理应用API,不处理Actuator端点
converters.add(new CustomMessageConverter());
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 保留其他转换器供Actuator使用
}
}
9.2 与Swagger集成
Swagger UI需要正确识别API的请求和响应格式:
- 确保自定义转换器支持的媒体类型与Swagger配置一致
- 添加Swagger配置:
java复制@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("API文档")
.version("1.0")
.description("使用自定义消息转换器"))
.components(new Components()
.addMediaTypes("application/json",
new MediaType().schema(new Schema().type("string"))));
}
9.3 与Spring Security集成
当使用Spring Security时,异常处理可能会绕过自定义转换器。需要统一配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler())
// 其他配置...
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, authException) -> {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(
objectMapper.writeValueAsString(Result.fail(401, "未认证")));
};
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(
objectMapper.writeValueAsString(Result.fail(403, "无权限")));
};
}
}
10. 最佳实践总结
-
保持转换器单一职责:一个转换器只处理一种明确的转换逻辑,避免创建"全能"转换器。
-
合理处理异常:在readInternal和writeInternal方法中妥善处理异常,转换为HttpMessageNotReadableException或HttpMessageNotWritableException。
-
考虑性能影响:ObjectMapper的配置会影响序列化性能,在生产环境中应该充分测试。
-
提供明确错误信息:当转换失败时,返回清晰的错误信息帮助客户端调试。
-
文档化自定义行为:在API文档中明确说明自定义转换器带来的特殊行为和格式要求。
-
版本兼容性:当升级Spring或Jackson版本时,需要重新测试自定义转换器是否仍然正常工作。
-
监控与日志:对于生产环境,应该记录转换失败的详细日志,但要注意不要记录敏感数据。
-
备选方案:对于简单的格式调整,考虑使用注解(如@JsonFormat)而非自定义转换器。
在最近的一个微服务项目中,我们通过合理设计自定义消息转换器,将API响应时间减少了约15%,同时统一了全系统的数据格式规范。关键在于找到通用性需求和特殊需求的平衡点,避免过度定制化导致维护成本增加。
