如何向字段级 Jackson 自定义反序列化器注入 Spring 依赖?
可以在字段上保留 @JsonDeserialize,同时通过构造器注入依赖。需要为实际执行反序列化的 ObjectMapper 配置 SpringHandlerInstantiator,由 Spring 创建反序列化器并解析依赖。
无需通过 deserializerByType(Date.class, ...) 全局注册。该方法按类型选择反序列化器,而这里的目标是只对指定字段启用。
为什么带参构造器可以工作?
Jackson 2.13 创建实例时,会先询问配置的 HandlerInstantiator。未配置该机制,或它没有返回实例时,Jackson 才会回退到默认的反射创建方式。因此,即使默认路径需要无参构造器,自定义反序列化器仍可使用依赖注入。参见 Jackson 2.13 的实例化实现。
Spring 的 SpringHandlerInstantiator 可以创建并自动装配 Jackson 的 serializer、deserializer 等处理器。参见 SpringHandlerInstantiator API。
配置实际使用的 ObjectMapper
在 Spring 5.3.18 中,给 Jackson2ObjectMapperBuilder 设置 applicationContext 后,如果没有显式指定其他 HandlerInstantiator,构建时会安装 SpringHandlerInstantiator。具体可见 Spring 5.3.18 的 builder 实现。
下面是普通 Spring 配置示例,日期格式从外部 properties 文件读取:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
@PropertySource("file:${date.config.path}")
public class JacksonConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public ObjectMapper objectMapper(ApplicationContext context) {
return Jackson2ObjectMapperBuilder.json()
.applicationContext(context)
.build();
}
}
启动时通过 JVM 系统属性指定文件位置,例如 -Ddate.config.path=/etc/myapp/date-formats.properties。文件内容可以是:
app.date-formats=yyyy-MM-dd,yyyy/MM/dd,yyyy-MM-dd HH:mm:ss
如果项目已经配置了属性文件加载和占位符解析,沿用已有配置即可。如果已经有 Jackson2ObjectMapperBuilder,也可以直接在原 builder 上增加 .applicationContext(context)。
还需要确认 HTTP 请求转换器或业务代码使用的是这个 ObjectMapper。声明一个 mapper Bean,并不代表所有自行创建的 mapper 或框架集成都使用它;在 Spring MVC 中,可以检查 MappingJackson2HttpMessageConverter 持有的实例。
在构造器中注入日期格式
下面的实现通过构造器接收格式字符串:Spring 使用 @Value 提供配置,普通单元测试可以直接传入字符串。
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.io.IOException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
public class DateTimeFormatDeserializer extends JsonDeserializer<Date> {
private final String[] patterns;
@Autowired
public DateTimeFormatDeserializer(
@Value("${app.date-formats}") String formats) {
this.patterns = Arrays.stream(formats.split(","))
.map(String::trim)
.filter(pattern -> !pattern.isEmpty())
.toArray(String[]::new);
if (patterns.length == 0) {
throw new IllegalArgumentException(
"app.date-formats must not be empty");
}
// 提前检查配置中的 pattern 是否有效。
for (String pattern : patterns) {
new SimpleDateFormat(pattern, Locale.ROOT);
}
}
@Override
public Date deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
if (!jp.hasToken(JsonToken.VALUE_STRING)) {
return (Date) ctxt.handleUnexpectedToken(Date.class, jp);
}
String text = jp.getText();
for (String pattern : patterns) {
SimpleDateFormat format =
new SimpleDateFormat(pattern, Locale.ROOT);
format.setLenient(false);
format.setTimeZone(ctxt.getTimeZone());
ParsePosition position = new ParsePosition(0);
Date parsed = format.parse(text, position);
if (parsed != null && position.getIndex() == text.length()) {
return parsed;
}
}
throw ctxt.weirdStringException(
text, Date.class, "Unsupported date format");
}
}
该示例使用 SimpleDateFormat 的 pattern 语法,格式之间以逗号分隔。如果 pattern 本身需要包含逗号,应改用不会产生歧义的配置结构。
如果要注入 myService,可以将构造器参数换成相应的服务类型,并确保该依赖已注册到 Spring 容器。
字段注解继续保留
原来的字段声明无需改为按类型注册:
@JsonProperty("Evn_setDT")
@JsonSerialize(using = DateTimeFormatSerializer.class)
@JsonDeserialize(using = DateTimeFormatDeserializer.class)
private Date evnSetDT;
字段注解决定反序列化器用于哪些字段;HandlerInstantiator 决定如何创建它。仅配置实例化机制不会让所有 Date 字段都使用 DateTimeFormatDeserializer。
无需仅为这条创建路径给反序列化器添加 @Component。如果 Jackson 没有使用 Spring 的实例化机制,单独添加 @Component 或 @Autowired 也无法解决默认反射路径下的构造器问题。
配置修改和单元测试需要注意什么?
外部 properties 文件可以让日期格式独立于应用构建产物维护。上面的实现会在实例创建时读取配置,没有实现运行时自动刷新,因此修改文件后通常需要重启应用。若需要动态更新,应注入可刷新、线程安全的配置服务,并在反序列化时读取当前格式。
单元测试可以直接创建实例:
DateTimeFormatDeserializer deserializer =
new DateTimeFormatDeserializer("yyyy-MM-dd,yyyy/MM/dd");
这可以验证解析逻辑。要验证字段注解和自动装配的完整链路,需要使用配置过 SpringHandlerInstantiator 的 mapper,反序列化包含该字段的对象。直接使用未经配置的 new ObjectMapper(),仍会遇到带参构造器无法通过默认路径实例化的问题。
反序列化器可能被复用,不要把可变的 SimpleDateFormat 保存为共享字段。示例每次调用时都会创建局部实例,并检查输入是否被完整消费。对于不带时区的字符串,还应按业务要求明确配置 mapper 的时区。如果多个格式都能解析同一字符串,示例会按配置顺序采用第一个成功结果。
备注:内容仅供参考。
