JSON parse error: default constructor not found. class java.time.YearMonth; nested exception is com.alibaba.fastjson.JSONException: default constructor not found. class java.time.YearMonth
Java 8 YearMonth 在 Spring MVC 中的序列化问题与解决方案
Java 8 引入的YearMonth类(java.time.YearMonth)可便捷表示 “年月” 维度(如2018-12),但在 Spring MVC 项目中接收该类型参数时,可能因序列化工具差异出现兼容性问题。本文将详细分析问题场景、根因,并提供针对性解决方案。
一、问题场景:不同请求格式下的接收差异
在 Spring MVC 项目中使用YearMonth接收参数时,出现 “请求格式不同,结果不同” 的现象:
| 请求类型 | 数据格式 | 序列化工具 | 接收结果 | 
|---|---|---|---|
| GET 请求(x-www-form-urlencoded) | yearMonth=2018-12 | 
- | 正常接收 | 
| POST 请求(application/json) | {"yearMonth":"2018-12"} | 
FastJson | 抛出反序列化异常 | 
| POST 请求(application/json) | {"yearMonth":"2018-12"} | 
Jackson | 正常接收 | 
关键异常信息(FastJson 场景)
当使用 FastJson 作为 Spring MVC 的 JSON 序列化工具时,POST 请求会抛出如下异常,核心原因是YearMonth无公有无参构造函数:
2020-02-18 11:18:25.284 WARN 16212 --- \[nio-8090-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver :  
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException:  
JSON parse error: default constructor not found. class java.time.YearMonth;  
nested exception is com.alibaba.fastjson.JSONException: default constructor not found. class java.time.YearMonth
二、根因分析:YearMonth 结构与序列化工具特性
1. YearMonth 的构造函数特性
查看YearMonth源码可知,其无公有无参构造函数,仅提供静态工厂方法of()创建实例,这是 FastJson 反序列化失败的核心原因(FastJson 默认依赖无参构造函数实例化对象):
// YearMonth的构造函数为private,外部无法直接调用
private YearMonth(int year, int month) {
     this.year = year;
     this.month = month;
}
// 仅提供静态工厂方法创建实例
public static YearMonth of(int year, int month) {
     YEAR.checkValidValue(year);
     MONTH_OF_YEAR.checkValidValue(month);
     return new YearMonth(year, month);
}
2. 不同序列化工具的兼容性差异
- 
Jackson(Spring MVC 默认):Jackson 对 Java 8 日期时间类(如
YearMonth、LocalDate)做了专门适配,无需额外配置即可通过2018-12格式反序列化为YearMonth实例; - 
FastJson:默认未适配
YearMonth,因无法找到无参构造函数,导致反序列化失败。 
三、解决方案:两种序列化工具的适配方案
根据项目是否能更换序列化工具,提供两种解决方案:
方案 1:使用默认 Jackson(推荐,零额外开发)
若项目无强制依赖 FastJson 的需求,直接使用 Spring MVC 默认的 Jackson 序列化工具即可,无需额外配置,YearMonth可自动适配2018-12格式。
关键验证:为何 Jackson 能支持?
Jackson 的jackson-datatype-jsr310模块(Java 8 日期时间支持模块)中,已内置YearMonthDeserializer和YearMonthSerializer,专门处理YearMonth的序列化与反序列化,因此无需手动开发适配逻辑。
方案 2:保留 FastJson,手动适配 YearMonth
若项目必须使用 FastJson,需为YearMonth定制反序列化工具类,并通过 “局部字段标注” 或 “全局配置” 生效。
步骤 1:开发 YearMonth 反序列化工具类
实现 FastJson 的ObjectDeserializer接口,利用YearMonth.parse()方法(支持2018-12格式)完成反序列化:
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
import java.lang.reflect.Type;
import java.time.YearMonth;
public class YearMonthDeserializer implements ObjectDeserializer {
     @Override
     public YearMonth deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
         // 1. 从JSON解析器中获取字符串格式的年月(如"2018-12")
         String yearMonthStr = parser.getLexer().stringVal();
         // 2. 推进JSON解析器游标,避免后续解析异常
         parser.getLexer().nextToken();
         // 3. 调用YearMonth的parse方法转换为实例
         return YearMonth.parse(yearMonthStr);
     }
     @Override
     public int getFastMatchToken() {
         return 0; // 无需快速匹配,直接走完整解析逻辑
     }
}
步骤 2:两种生效方式(二选一)
方式 A:局部生效(指定字段使用)
在 VO 类的YearMonth字段上,通过@JSONField(deserializeUsing = ...)标注指定反序列化工具:
public class AddVO {
     // 仅该字段使用自定义反序列化器
     @JSONField(deserializeUsing = YearMonthDeserializer.class)
     private YearMonth yearMonth;
     // getter/setter
     public YearMonth getYearMonth() {
         return yearMonth;
     }
     public void setYearMonth(YearMonth yearMonth) {
         this.yearMonth = yearMonth;
     }
}
方式 B:全局生效(所有 YearMonth 字段)
在 Spring MVC 配置 FastJson 的HttpMessageConverter时,全局注册YearMonth的反序列化器,避免每个字段单独标注:
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.time.YearMonth;
import java.util.List;
@Configuration
public class FastJsonConfig implements WebMvcConfigurer {
     @Override
     public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
         // 1. 创建FastJson的HTTP消息转换器
         FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
         // 2. 获取FastJson配置对象
         com.alibaba.fastjson.support.config.FastJsonConfig fastJsonConfig = fastJsonConverter.getFastJsonConfig();
         // 3. 全局注册YearMonth的反序列化器
         fastJsonConfig.getParserConfig().putDeserializer(YearMonth.class, new YearMonthDeserializer());
         // 4. 将转换器加入Spring MVC的转换器列表(放在首位,优先使用)
         converters.add(0, fastJsonConverter);
     }
}
四、总结与实践建议
- 
优先选择 Jackson:Spring MVC 默认使用 Jackson,且其对 Java 8 日期时间类(
YearMonth、LocalDate等)有完善适配,可减少手动开发成本,避免兼容性问题; - 
FastJson 场景注意适配:若必须使用 FastJson,建议通过 “全局配置” 方式注册
YearMonth反序列化器,确保项目中所有YearMonth字段统一适配; - 
GET 请求与 POST 请求的一致性:GET 请求(x-www-form-urlencoded)因参数解析逻辑不同(不依赖 JSON 序列化),通常可正常接收
YearMonth,但需注意 POST 请求的 JSON 序列化适配,避免前后端参数格式不一致。 
                    
                
                
            
        
浙公网安备 33010602011771号