• 记一次Gson在不同环境解析时间结果不同的BUG定位


    1、前因

    由于领导要求,将生产服务由本地机房搬迁到了华为云,结果搬迁后发现Gson再解析报文的时候抛了异常:
    日期异常

    2、BUG定位

    这个异常指的是:Gson无法解析格式为yyyy-MM-dd HH:mm:ss的时间。由于之前在测试环境测过,并且时间解析是成功的,所以初步怀疑是生产环境与测试环境不一致导致的问题。

    根据报错堆栈信息,进入到DateTypeAdapter类中,查看deserializeToDate方法:

    private synchronized Date deserializeToDate(String json) {
        try {
            return localFormat.parse(json);
        } catch (ParseException ignored) {
        }
        try {
            return enUsFormat.parse(json);
        } catch (ParseException ignored) {
        }
        try {
            return ISO8601Utils.parse(json, new ParsePosition(0));
        } catch (ParseException e) {
            throw new JsonSyntaxException(json, e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到Gson解析时间用了三种方式,并且是依次执行,这里抛异常肯定是三种方式都解析失败导致的,先看第一种方式localFormat的源码定义:

    private final DateFormat localFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT);
    
    • 1

    进入到getDateTimeInstance方法内部:

    public final static DateFormat getDateTimeInstance(int dateStyle, int timeStyle) {
        return get(timeStyle, dateStyle, 3, Locale.getDefault(Locale.Category.FORMAT));
    }
    
    • 1
    • 2
    • 3

    可以看到这里传入了个参数:Locale.getDefault(Locale.Category.FORMAT),locale是Linux的一个命令,代表了多语言环境的相关操作,所以localFormat对象实际上是跟环境有关的,它的变量名也体现了这一点。看到这里我在测试环境和生产环境分别执行了locale命令,得到了下列结果:
    测试环境
    测试环境locale
    生产环境
    生产环境locale
    可以看到两个环境的语言确实是不同的,测试环境是中文环境,localFormat能够解析yyyy-MM-dd HH:mm:ss格式的时间,而生产环境是英文环境,localFormat无法解析yyyy-MM-dd HH:mm:ss格式的时间。

    3、BUG修复

    先说方法,可以在虚拟机参数中加入这两个参数:-Duser.language=zh -Duser.region=CN
    也可以只加这个参数:-Duser.language.format=zh

    为啥?继续上面的源码阅读,点进Locale.getDefault方法进行查看:

    public static Locale getDefault(Locale.Category category) {
        switch (category) {
            // 省略
            case FORMAT:
                if (defaultFormatLocale == null) {
                    synchronized(Locale.class) {
                        if (defaultFormatLocale == null) {
                            defaultFormatLocale = initDefault(category);
                        }
                    }
                }
                return defaultFormatLocale;
            // 省略
        }
        // 省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    因为传入的category是FORMAT,所以只需点进对应的initDefault方法查看:

    private static Locale initDefault(Locale.Category category) {
        return getInstance(
            AccessController.doPrivileged(new GetPropertyAction(category.languageKey, defaultLocale.getLanguage())),
            // 省略
            );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    doPrivileged是个native方法,可以猜测,这个方法是用来获取配置的参数的,而传入的两个参数,按照写代码的惯例,前一个应该是开发自己配置的参数,而后一个应该是默认的系统配置,看到这里就应该能明白为啥上面给出的方法有两种虚拟机参数修改方式了,我们先看defaultLocale的源码定义:

    private volatile static Locale defaultLocale = initDefault();
    
    • 1

    点进initDefault查看:

    private static Locale initDefault() {
        String language, region, script, country, variant;
        language = AccessController.doPrivileged(
            new GetPropertyAction("user.language", "en"));
        region = AccessController.doPrivileged(
            new GetPropertyAction("user.region"));
        // 省略
        return getInstance(language, script, country, variant, null);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里出现了user.language和user.region两个与语言有关的参数,第一个是语言,第二个是区域,如果要改为中文环境,那么语言就是zh,区域就是CN,而英文环境下语言是en,区域是US,这是方法一。


    再回到上面的代码:

    AccessController.doPrivileged(new GetPropertyAction(category.languageKey, defaultLocale.getLanguage()))
    
    • 1

    除了默认参数可以修改外,我们还可以修改前面的自定义参数,点进category.languageKey查看:

    public enum Category {
        FORMAT("user.language.format",
               // 省略
               );
    
        Category(String languageKey, String scriptKey, String countryKey, String variantKey) {
            this.languageKey = languageKey;
            //省略
        }
    
        final String languageKey;
        // 省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这是个枚举类,其中languageKey的值是user.language.format,把这个参数值配置为zh,就可以设置为中文环境,这是方法二。


    除了上面两种修复方法,其实还有其它方法,比如:

    • 修改Linux系统配置
    • 使用GsonBuilder来创建Gson对象,可以直接指定时间转换格式
    • 使用Jackson等其它json工具来替换Gson
  • 相关阅读:
    创建husky规范前端项目
    飞机电子式模拟空速表的设计与制作
    Go语言学习笔记—golang函数
    山东省专精特新申报条件是什么?各地市分别补贴多少钱?
    显示今天的年、月、日日期、时间的数据处理timetuple()
    牛客出bug(华为机试HJ71)
    WEB自动化_强制等待与智能等待(显示等待、隐式等待)
    matlab读取hdf5格式的全球火灾排放数据库Global Fire Emissions Database(GFED)数据
    消息队列-Kafka-消费方如何分区与分区重平衡
    20240620日志:TAS-MRAM的电阻开放分析
  • 原文地址:https://blog.csdn.net/Stone__Fly/article/details/126715090