如何用Event接口替代Object类型?Jackson反序列化异常问题
我想把EventDto类的eventData字段类型从Object改成Event接口(该接口已有PrintEvent实现类),但替换后抛出了序列化异常。我不想用Object类型,因为会降低代码可读性。
错误信息:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.app.dto.Event]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of
com.app.dto.Event(no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 5, column: 26] (through reference chain: com.app.dto.SubmitEventsRequest["events"]->java.util.ArrayList[0]->com.app.dto.EventDto["eventData"])
Jackson在反序列化时无法直接实例化抽象的Event接口,它不知道该把JSON数据转换成哪个具体的实现类(比如PrintEvent)。之前用Object类型时,Jackson会默认转成LinkedHashMap,所以不会报错,但换成接口类型后就需要明确指定类型映射规则。
方案1:使用Jackson类型注解(推荐)
在Event接口上添加@JsonTypeInfo和@JsonSubTypes注解,告诉Jackson如何根据JSON中的字段匹配具体实现类。这里我们可以利用EventDto里的eventType字段作为类型标识:
修改后的Event接口
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, // 从父对象EventDto获取类型标识 property = "eventType" // 指定类型判断字段为eventType ) @JsonSubTypes({ @JsonSubTypes.Type(value = PrintEvent.class, name = "PRINT_SUBMITTED"), @JsonSubTypes.Type(value = PrintEvent.class, name = "PRINT_FAILED") // 后续新增Event实现类时,在此添加对应映射 }) public interface Event { }
说明
@JsonTypeInfo配置类型信息读取规则:EXTERNAL_PROPERTY表示从当前对象的父对象(EventDto)中读取eventType字段值,以此判断具体实现类。@JsonSubTypes枚举所有事件类型与实现类的映射关系,比如PRINT_SUBMITTED和PRINT_FAILED都对应PrintEvent。
方案2:自定义反序列化器
如果不想依赖注解,可以自定义EventDto的反序列化器,根据eventType手动选择eventData的具体类型:
1. 实现自定义反序列化器
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; public class EventDtoDeserializer extends StdDeserializer<EventDto> { protected EventDtoDeserializer() { super(EventDto.class); } @Override public EventDto deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); IncomingEventsTypeEnum eventType = IncomingEventsTypeEnum.valueOf(node.get("eventType").asText()); EventDto.EventDtoBuilder builder = EventDto.builder(); builder.eventType(eventType); // 根据eventType匹配对应的Event实现类 JsonNode eventDataNode = node.get("eventData"); switch (eventType) { case PRINT_SUBMITTED: case PRINT_FAILED: PrintEvent printEvent = p.getCodec().treeToValue(eventDataNode, PrintEvent.class); builder.eventData(printEvent); break; // 新增事件类型时在这里扩展 default: throw new IllegalArgumentException("Unknown event type: " + eventType); } return builder.build(); } }
2. 注册反序列化器到ObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(EventDto.class, new EventDtoDeserializer()); mapper.registerModule(module); return mapper; } }
用原来的curl请求测试,现在eventData会被正确反序列化为PrintEvent对象,后续的EventHandler也能正常处理事件。
内容的提问来源于stack exchange,提问作者Taras

