深度自定义jackson的JSON序列化日期时间类型

直接上代码

java 复制代码
import java.io.IOException;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.DateDeserializers.DateDeserializer;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.DateSerializer;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;

/**
 * 自定义JACKSON序列化
 */
public class JsonRefUtil {

    public static final DateTimeFormatter DATE_TIME_FORMATTER_WITHOUT_T   = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(DateTimeFormatter.ISO_LOCAL_DATE)
            .appendLiteral(' ')
            .append(DateTimeFormatter.ISO_LOCAL_TIME)
            .toFormatter()
            .withZone(ZoneId.systemDefault());
    public static       DateTimeFormatter DATE_TIME_FORMAT_WITHOUT_T_NANO = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(DateTimeFormatter.ISO_LOCAL_DATE)
            .appendLiteral(' ')
            .appendValue(HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(MINUTE_OF_HOUR, 2)
            .optionalStart()
            .appendLiteral(':')
            .appendValue(SECOND_OF_MINUTE, 2)
            .toFormatter()
            .withZone(ZoneId.systemDefault());

    /**
     * 自定义 LocalDateTime 序列化器
     */
    public static class MyLocalDateTimeSerializer extends LocalDateTimeSerializer {
        public static final MyLocalDateTimeSerializer INSTANCE = new MyLocalDateTimeSerializer();

        public MyLocalDateTimeSerializer() {
            super(LocalDateTimeSerializer.INSTANCE, false, false, DATE_TIME_FORMAT_WITHOUT_T_NANO);
        }
    }

    /**
     * 自定义 LocalDateTime 反序列化器,兼容带T和不带T的日期格式
     */
    public static class MyLocalDateTimeDeserializer extends LocalDateTimeDeserializer {
        public static final MyLocalDateTimeDeserializer INSTANCE = new MyLocalDateTimeDeserializer();

        public MyLocalDateTimeDeserializer() {
            super(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        }

        @Override
        protected LocalDateTime _fromString(JsonParser p, DeserializationContext ctxt,
                                            String string0) throws IOException {
            String string = string0.trim();
            if (string.isEmpty()) {
                return _fromEmptyString(p, ctxt, string);
            }
            try {
                if (string.contains("T")) {
                    return LocalDateTime.parse(string, _formatter);
                } else {
                    return LocalDateTime.parse(string, DATE_TIME_FORMATTER_WITHOUT_T);
                }
            } catch (DateTimeException e) {
                return _handleDateTimeException(ctxt, e, string);
            }
        }
    }

    /**
     * 自定义 Date 反序列化器,兼容带T和不带T的日期格式
     */
    public static class MyStdDateFormat extends StdDateFormat {
        public static final    MyStdDateFormat INSTANCE = new MyStdDateFormat();
        protected final static Pattern         PATTERN;

        static {
            Pattern p = null;
            try {
                // 替换 DATE_FORMAT_STR_ISO8601 兼容空格分隔日起时间
                p = Pattern.compile(PATTERN_PLAIN_STR
                        + "[T ]\\d\\d[:]\\d\\d(?:[:]\\d\\d)?" // hours, minutes, optional seconds
                        + "(\\.\\d+)?" // optional second fractions
                        + "(Z|[+-]\\d\\d(?:[:]?\\d\\d)?)?" // optional timeoffset/Z
                );
            } catch (Exception t) {
                throw new RuntimeException(t);
            }
            PATTERN = p;
        }

        public MyStdDateFormat() {
            super(TimeZone.getDefault(), Locale.CHINA, true);
        }

        @Override
        public MyStdDateFormat clone() {
            return new MyStdDateFormat();
        }

        @Override
        protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
                throws IllegalArgumentException, ParseException {
            dateStr = dateStr.trim();
            final int totalLen = dateStr.length();
            // actually, one short-cut: if we end with "Z", must be UTC
            TimeZone tz = TimeZone.getDefault();
            if ((_timezone != null) && ('Z' != dateStr.charAt(totalLen - 1))) {
                tz = _timezone;
            }
            Calendar cal = _getCalendar(tz);
            cal.clear();
            String formatStr;
            if (totalLen <= 10) {
                Matcher m = PATTERN_PLAIN.matcher(dateStr);
                if (m.matches()) {
                    int year = _parse4D(dateStr, 0);
                    int month = _parse2D(dateStr, 5) - 1;
                    int day = _parse2D(dateStr, 8);

                    cal.set(year, month, day, 0, 0, 0);
                    cal.set(Calendar.MILLISECOND, 0);
                    return cal.getTime();
                }
                formatStr = DATE_FORMAT_STR_PLAIN;
            } else {
                if (dateStr.contains(" ")) {

                }
                Matcher m = PATTERN.matcher(dateStr);
                if (m.matches()) {
                    // Important! START with optional time zone; otherwise Calendar will explode

                    int start = m.start(2);
                    int end = m.end(2);
                    int len = end - start;
                    if (len > 1) { // 0 -> none, 1 -> 'Z'
                        // NOTE: first char is sign; then 2 digits, then optional colon, optional 2 digits
                        int offsetSecs = _parse2D(dateStr, start + 1) * 3600; // hours
                        if (len >= 5) {
                            offsetSecs += _parse2D(dateStr, end - 2) * 60; // minutes
                        }
                        if (dateStr.charAt(start) == '-') {
                            offsetSecs *= -1000;
                        } else {
                            offsetSecs *= 1000;
                        }
                        cal.set(Calendar.ZONE_OFFSET, offsetSecs);
                        // 23-Jun-2017, tatu: Not sure why, but this appears to be needed too:
                        cal.set(Calendar.DST_OFFSET, 0);
                    }

                    int year = _parse4D(dateStr, 0);
                    int month = _parse2D(dateStr, 5) - 1;
                    int day = _parse2D(dateStr, 8);

                    // So: 10 chars for date, then `T`, so starts at 11
                    int hour = _parse2D(dateStr, 11);
                    int minute = _parse2D(dateStr, 14);

                    // Seconds are actually optional... so
                    int seconds;
                    if ((totalLen > 16) && dateStr.charAt(16) == ':') {
                        seconds = _parse2D(dateStr, 17);
                    } else {
                        seconds = 0;
                    }
                    cal.set(year, month, day, hour, minute, seconds);

                    // Optional milliseconds
                    start = m.start(1) + 1;
                    end = m.end(1);
                    int msecs = 0;
                    if (start >= end) { // no fractional
                        cal.set(Calendar.MILLISECOND, 0);
                    } else {
                        // first char is '.', but rest....
                        msecs = 0;
                        final int fractLen = end - start;
                        switch (fractLen) {
                            default: // [databind#1745] Allow longer fractions... for now, cap at nanoseconds tho

                                if (fractLen > 9) { // only allow up to nanos
                                    throw new ParseException(String.format(
                                            "Cannot parse date \"%s\": invalid fractional seconds '%s'; can use at most 9 digits",
                                            dateStr, m.group(1).substring(1)
                                    ), start);
                                }
                                // fall through
                            case 3:
                                msecs += (dateStr.charAt(start + 2) - '0');
                            case 2:
                                msecs += 10 * (dateStr.charAt(start + 1) - '0');
                            case 1:
                                msecs += 100 * (dateStr.charAt(start) - '0');
                                break;
                            case 0:
                                break;
                        }
                        cal.set(Calendar.MILLISECOND, msecs);
                    }
                    return cal.getTime();
                }
                formatStr = DATE_FORMAT_STR_ISO8601;
            }

            throw new ParseException
                    (String.format("Cannot parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)",
                            dateStr, formatStr, _lenient),
                            // [databind#1742]: Might be able to give actual location, some day, but for now
                            //  we can't give anything more indicative
                            0);
        }

        private static int _parse4D(String str, int index) {
            return (1000 * (str.charAt(index) - '0'))
                    + (100 * (str.charAt(index + 1) - '0'))
                    + (10 * (str.charAt(index + 2) - '0'))
                    + (str.charAt(index + 3) - '0');
        }

        private static int _parse2D(String str, int index) {
            return (10 * (str.charAt(index) - '0'))
                    + (str.charAt(index + 1) - '0');
        }

        @Override
        protected void _format(TimeZone tz, Locale loc, Date date,
                               StringBuffer buffer) {
            Calendar cal = _getCalendar(tz);
            cal.setTime(date);
            // [databind#2167]: handle range beyond [1, 9999]
            final int year = cal.get(Calendar.YEAR);

            // Assuming GregorianCalendar, special handling needed for BCE (aka BC)
            if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
                _formatBCEYear(buffer, year);
            } else {
                if (year > 9999) {
                    buffer.append('+');
                }
                pad4(buffer, year);
            }
            buffer.append('-');
            pad2(buffer, cal.get(Calendar.MONTH) + 1);
            buffer.append('-');
            pad2(buffer, cal.get(Calendar.DAY_OF_MONTH));
            buffer.append(' ');
            pad2(buffer, cal.get(Calendar.HOUR_OF_DAY));
            buffer.append(':');
            pad2(buffer, cal.get(Calendar.MINUTE));
            buffer.append(':');
            pad2(buffer, cal.get(Calendar.SECOND));
            buffer.append('.');
            pad3(buffer, cal.get(Calendar.MILLISECOND));
        }

        private static void pad2(StringBuffer buffer, int value) {
            int tens = value / 10;
            if (tens == 0) {
                buffer.append('0');
            } else {
                buffer.append((char) ('0' + tens));
                value -= 10 * tens;
            }
            buffer.append((char) ('0' + value));
        }

        private static void pad3(StringBuffer buffer, int value) {
            int h = value / 100;
            if (h == 0) {
                buffer.append('0');
            } else {
                buffer.append((char) ('0' + h));
                value -= (h * 100);
            }
            pad2(buffer, value);
        }

        private static void pad4(StringBuffer buffer, int value) {
            int h = value / 100;
            if (h == 0) {
                buffer.append('0').append('0');
            } else {
                if (h > 99) { // [databind#2167]: handle above 9999 correctly
                    buffer.append(h);
                } else {
                    pad2(buffer, h);
                }
                value -= (100 * h);
            }
            pad2(buffer, value);
        }

    }

    public static final DateDeserializer MY_DATE_DESERIALIZER = new DateDeserializer(DateDeserializer.instance, MyStdDateFormat.INSTANCE,
            null);
    public static final DateSerializer   MY_DATE_SERIALIZER   = new DateSerializer(false, MyStdDateFormat.INSTANCE);

    /**
     * 创建自定义的 {@link SimpleModule},
     */
    public static SimpleModule myModule() {
        SimpleModule module = new SimpleModule();
        module.addSerializer(LocalDateTime.class, MyLocalDateTimeSerializer.INSTANCE);
        module.addSerializer(Date.class, MY_DATE_SERIALIZER);
        module.addDeserializer(LocalDateTime.class, MyLocalDateTimeDeserializer.INSTANCE);
        module.addDeserializer(Date.class, MY_DATE_DESERIALIZER);
        return module;
    }

    /**
     * 标准 json mapper
     */
    public static final JsonMapper jsonMapper = JsonMapper.builder()
            .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
            .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES)
            //.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .defaultDateFormat(MyStdDateFormat.INSTANCE)
            .addModule(new JavaTimeModule())
            .addModule(myModule())
            .build();
}

测试它

java 复制代码
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Date;
import java.util.List;

import com.alibaba.fastjson.JSON;

public class XxTest {


    public Date a;
    public LocalDate b;
    public LocalDateTime c;
    public LocalTime d;


    public static void main(String[] args) throws Exception{
        var x= new XxTest();
        x.a=new Date();
        x.b=LocalDate.now();
        x.c=LocalDateTime.now();
        x.d=LocalTime.now();

        System.out.println("直接序列");
        var json = JsonRefUtil.jsonMapper.writeValueAsString(x);
        System.out.println(json);

        for (String s : List.of("2026-02-11","2026-02-11 12:15:29","2026-02-11 12:15:29.123"
                ,"2026-02-11 12:15:29Z","2026-02-11 12:15:29+08:00")) {
            test(s);
        }

        var s = """
                {"a":'2026-02-11 12:15:29',"b":"2026-02-11","c":"2026-02-11 12:15:29.1","d":"12:15:29.1",f:1}
                """;
        var y = BeanRefUtil.jsonToBean(s, XxTest.class,true);
        System.out.println(JSON.toJSONString(y));
        var s1 = """
                {"a":'2026-02-11T12:15:29',"b":"2026-02-11","c":"2026-02-11 12:15:29","d":"12:15:29"}
                """;
        var y1 = BeanRefUtil.jsonToBean(s1, XxTest.class,true);
        System.out.println(JSON.toJSONString(y1));
        var s2 = """
                {"a":'2026-02-11 12:15:29Z',"b":"2026-02-11","c":"2026-02-11 12:15:29","d":"12:15:29"}
                """;
        var y2 = BeanRefUtil.jsonToBean(s2, XxTest.class,true);
        System.out.println(JSON.toJSONString(y2));
        var s3 = """
                {"a":1770783329000,"b":"2026-02-11","c":"2026-02-11 12:15:29","d":"12:15:29"}
                """;
        var y3 = BeanRefUtil.jsonToBean(s3, XxTest.class,true);
        System.out.println(JSON.toJSONString(y3));
    }

    private static void test(String a){
        var s = "{\"a\":\""+a+"\"}";
        System.out.println("序列"+a);
        var y = BeanRefUtil.jsonToBean(s, XxTest.class,true);
        System.out.println(JSON.toJSONString(y));
        System.out.println(JSON.toJSONString(JSON.parseObject(s,XxTest.class)));
    }
}

控制台输出符合预期

plian 复制代码
直接序列
{"a":"2026-02-13 14:38:58.732","b":"2026-02-13","c":"2026-02-13 14:38:58","d":"14:38:58.743665534"}
序列2026-02-11
{"a":1770739200000}
{"a":1770739200000}
序列2026-02-11 12:15:29
{"a":1770783329000}
{"a":1770783329000}
序列2026-02-11 12:15:29.123
{"a":1770783329123}
{"a":1770783329123}
序列2026-02-11 12:15:29Z
{"a":1770783329000}
{"a":1770812129000}
序列2026-02-11 12:15:29+08:00
{"a":1770783329000}
{"a":1770783329000}
{"a":1770783329000,"b":"2026-02-11","c":"2026-02-11T12:15:29.100","d":"12:15:29.100"}
{"a":1770783329000,"b":"2026-02-11","c":"2026-02-11T12:15:29","d":"12:15:29"}
{"a":1770783329000,"b":"2026-02-11","c":"2026-02-11T12:15:29","d":"12:15:29"}
{"a":1770783329000,"b":"2026-02-11","c":"2026-02-11T12:15:29","d":"12:15:29"}
相关推荐
廋到被风吹走2 小时前
DDD领域驱动设计深度解析:从理论到代码实践
java·大数据·linux
我命由我123452 小时前
Java 开发 - 如何让一个类拥有两个父类
java·服务器·开发语言·后端·java-ee·intellij-idea·intellij idea
范什么特西2 小时前
狂神--守护线程
java·linux·服务器
何中应2 小时前
CentOS7安装Maven
java·运维·后端·maven
大鹏说大话2 小时前
Windows 下将 Java 项目打包为 Docker 容器并部署的完整指南
java·windows·docker
Zachery Pole2 小时前
JAVA_03_运算符
java·开发语言·前端
张万森爱喝可乐2 小时前
Java 8 新特性探秘:开启现代Java开发之旅
java
毕设源码-邱学长3 小时前
【开题答辩全过程】以 基于java的网上书店管理系统为例,包含答辩的问题和答案
java·开发语言
消失的旧时光-19433 小时前
第二十二课:领域建模入门——从业务中“提炼结构”(认知篇)
java·spring boot·后端·domain