PAYATHON 2026

如何向字段级 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 的时区。如果多个格式都能解析同一字符串,示例会按配置顺序采用第一个成功结果。

备注:内容仅供参考。