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 日期时间类(如YearMonthLocalDate)做了专门适配,无需额外配置即可通过2018-12格式反序列化为YearMonth实例;

  • FastJson:默认未适配YearMonth,因无法找到无参构造函数,导致反序列化失败。

三、解决方案:两种序列化工具的适配方案

根据项目是否能更换序列化工具,提供两种解决方案:

方案 1:使用默认 Jackson(推荐,零额外开发)

若项目无强制依赖 FastJson 的需求,直接使用 Spring MVC 默认的 Jackson 序列化工具即可,无需额外配置,YearMonth可自动适配2018-12格式。

关键验证:为何 Jackson 能支持?

Jackson 的jackson-datatype-jsr310模块(Java 8 日期时间支持模块)中,已内置YearMonthDeserializerYearMonthSerializer,专门处理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);

     }

}

四、总结与实践建议

  1. 优先选择 Jackson:Spring MVC 默认使用 Jackson,且其对 Java 8 日期时间类(YearMonthLocalDate等)有完善适配,可减少手动开发成本,避免兼容性问题;

  2. FastJson 场景注意适配:若必须使用 FastJson,建议通过 “全局配置” 方式注册YearMonth反序列化器,确保项目中所有YearMonth字段统一适配;

  3. GET 请求与 POST 请求的一致性:GET 请求(x-www-form-urlencoded)因参数解析逻辑不同(不依赖 JSON 序列化),通常可正常接收YearMonth,但需注意 POST 请求的 JSON 序列化适配,避免前后端参数格式不一致。

posted @ 2020-02-18 11:41  雨落寒沙  阅读(3286)  评论(0)    收藏  举报