接口返回時間為什么差8小時莹捡?論SpringBoot中Jackson配置

概述

Jackson作為SpringBoot中默認(rèn)的JSON mapping庫,在java項目中應(yīng)用十分廣泛扣甲,你在項目實踐中是不是遇到過這樣的問題:

  1. 日期格式看上去沒問題篮赢,但是序列化之后輸出的字符串差了8小時
  2. 服務(wù)接口的日期格式不統(tǒng)一,你可能需要各個接口分別適配琉挖,不知道如何全局配置反序列化

Jackson簡介

Jackson是一個簡單基于Java應(yīng)用庫启泣,Jackson可以輕松的將Java對象轉(zhuǎn)換成json對象和xml文檔,同樣也可以將json示辈、xml轉(zhuǎn)換成Java對象寥茫。

ObjectMapper類

ObjectMapper是Jackson庫的主要類。它提供一些功能將轉(zhuǎn)換成Java對象匹配JSON結(jié)構(gòu)矾麻,反之亦然纱耻。它使用JsonParser和JsonGenerator的實例實現(xiàn)JSON實際的讀/寫。

轉(zhuǎn)換代碼

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class JacksonUtil {
    private static ObjectMapper mapper = new ObjectMapper();

    private JacksonUtil() {
    }

    /**
     * 序列化對象到Json字符
     */
    public static String generate(Object object) throws JsonProcessingException {
        return mapper.writeValueAsString(object);
    }

    /**
     * 反序列化Json字符到對象
     */
    public static <T> T parse(String content, Class<T> valueType) throws IOException {
        return mapper.readValue(content, valueType);
    }
}

數(shù)據(jù)綁定

簡單的數(shù)據(jù)綁定是指JSON映射到Java核心數(shù)據(jù)類型险耀。下表列出了JSON類型和Java類型之間的關(guān)系弄喘。

序號 JSON 類型 Java 類型
1 object LinkedHashMap<String,Object>
2 array ArrayList<Object>
3 string String
4 complete number Integer, Long or BigInteger
5 fractional number Double / BigDecimal
6 true | false Boolean
7 null null

Spring應(yīng)用中如何使用Jackson

Spring Boot支持與三種JSON mapping庫集成:Gson、Jackson和JSON-B甩牺。Jackson是首選和默認(rèn)的蘑志。

Jackson是spring-boot-starter-json依賴中的一部分,spring-boot-starter-web中包含spring-boot-starter-json贬派。也就是說急但,當(dāng)項目中引入spring-boot-starter-web后會自動引入spring-boot-starter-json。

pom.xml依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Restful接口中返回對象的Date類型為什么少了8小時赠群?

本地jackson配置

spring:
    jackson:
        date-format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'

如果你用過java8新的時間類下的Instant羊始,查看toString方法會發(fā)現(xiàn)它使用DateTimeFormatter.ISO_INSTANT標(biāo)準(zhǔn)時間格式輸出,如下:

Instant now = Instant.now();
// 2019-08-18T02:57:55.234Z

這個輸出格式與我們上面的配置一致查描,所以就日期格式配置而言這本身并沒有問題突委,下面我們再查看JacksonAutoConfiguration源碼中configureDateFormat方法

private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {
    String dateFormat = this.jacksonProperties.getDateFormat();
    if(dateFormat != null) {
        try {
            Class ex = ClassUtils.forName(dateFormat, (ClassLoader)null);
            builder.dateFormat((DateFormat)BeanUtils.instantiateClass(ex));
        } catch (ClassNotFoundException var6) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
            TimeZone timeZone = this.jacksonProperties.getTimeZone();
            if(timeZone == null) {
                timeZone = (new ObjectMapper()).getSerializationConfig().getTimeZone();
            }

            simpleDateFormat.setTimeZone(timeZone);
            builder.dateFormat(simpleDateFormat);
        }
    }
}

當(dāng)我們沒有配置時區(qū)時柏卤,它會執(zhí)行timeZone = (new ObjectMapper()).getSerializationConfig().getTimeZone();,進(jìn)一步查看timeZone屬性匀油,發(fā)現(xiàn)這其實是默認(rèn)時區(qū)DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC")

看來問題的根源就在這兒缘缚,也就是說它其實用的是UTC時間,這就解釋 了為什么會少了8小時(本地時間為GMT+8)敌蚜,我原先以為不設(shè)置時區(qū)會使用JVM時區(qū)或系統(tǒng)所在時區(qū)(中國時區(qū))桥滨,所以只要加個時區(qū)配置就行了,如下:

# Asia/Shanghai 等同于 GMT+8
spring:
    jackson:
        date-format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
        time-zone: Asia/Shanghai

注意弛车!注意齐媒!

我們雖然從表面解決了時間差8小時的問題,但這種方法并不優(yōu)雅(或者說有些令人費解)纷跛,上面提到了Instant打印出來的結(jié)果喻括,使用yyyy-MM-dd'T'HH:mm:ss.SSS'Z'格式就是應(yīng)該返回標(biāo)準(zhǔn)時間(相對本地時間少8小時),時區(qū)本不需要配置或者你應(yīng)該配置為UTC贫奠,比如上述時間在js中執(zhí)行

var date = new Date('2019-08-18T02:57:55.234Z');
// Sun Aug 18 2019 10:57:55 GMT+0800 (中國標(biāo)準(zhǔn)時間)

說明前端使用的時候其實是能正確識別的唬血,如果配置加上GMT+8返回的時間補(bǔ)上8小時,前端在解析的時候反而不正確了唤崭,但是這個時間不適合閱讀(需要進(jìn)行一個轉(zhuǎn)換)拷恨,或許我們該使用不帶引號的大Z(小z輸出的格式j(luò)s轉(zhuǎn)換會報Invalid Date),下面來說明

日期模式字符串說明

  • 文本可以由單引號'引起來谢肾,這樣就不需要解析腕侄,比如yyyy-MM-dd'T'HH:mm:ss.SSS'Z'中的TZ(由此可見這種帶引號的'Z'只是作為占位符,沒有實際意義芦疏,但是你也可以認(rèn)為帶'Z'的格式表示使用的是UTC標(biāo)準(zhǔn)時間)
  • 時區(qū)可以用小寫z和大寫Z來表示兜挨,小z表示世界標(biāo)準(zhǔn)時間,大Z表示RFC 822 time zone

時區(qū)說明

  • UTC時間 世界標(biāo)準(zhǔn)時間
  • GMT時間 格林尼治平時眯分,不再被作為標(biāo)準(zhǔn)時間使用,可以認(rèn)為其等同于UTC時間
  • CST時間 北京時間柒桑,記為UTC+8弊决,不過這個縮寫它可以同時代表四個不同的時間(所以不建議使用該格式)
    • Central Standard Time (USA) UT-6:00
    • Central Standard Time (Australia) UT+9:30
    • China Standard Time UT+8:00
    • Cuba Standard Time UT-4:00

舉個例子

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class DateFormatTest {
    public static void main(String[] args) {
        Date date = new Date();
        String[][] formatArray = {
                {"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", null},
                {"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "GMT+8"},
                {"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "UTC"},
                {"yyyy-MM-dd'T'HH:mm:ss.SSSz", "GMT"},
                {"yyyy-MM-dd'T'HH:mm:ss.SSSz", "GMT+8"},
                {"yyyy-MM-dd'T'HH:mm:ss.SSSZ", "GMT+8"},
                {"yyyy-MM-dd'T'HH:mm:ss.SSSZ", "GMT"},
                {"yyyy-MM-dd'T'HH:mm:ss.SSSZ", "UTC"},
        };
        for (String[] item : formatArray) {
            SimpleDateFormat sdf = new SimpleDateFormat(item[0]);
            if (item[1] != null) {
                sdf.setTimeZone(TimeZone.getTimeZone(item[1]));
            }
            System.out.println(String.format("format=%s, timeZone=%s, print=%s", item[0], item[1], sdf.format(date)));
        }
    }
}

輸出結(jié)果

format=yyyy-MM-dd'T'HH:mm:ss.SSS'Z', timeZone=null, print=2019-08-18T10:57:55.333Z
format=yyyy-MM-dd'T'HH:mm:ss.SSS'Z', timeZone=GMT+8, print=2019-08-18T10:57:55.333Z
format=yyyy-MM-dd'T'HH:mm:ss.SSS'Z', timeZone=UTC, print=2019-08-18T02:57:55.333Z
format=yyyy-MM-dd'T'HH:mm:ss.SSSz, timeZone=GMT, print=2019-08-18T02:57:55.333GMT
format=yyyy-MM-dd'T'HH:mm:ss.SSSz, timeZone=GMT+8, print=2019-08-18T10:57:55.333GMT+08:00
format=yyyy-MM-dd'T'HH:mm:ss.SSSZ, timeZone=GMT+8, print=2019-08-18T10:57:55.333+0800
format=yyyy-MM-dd'T'HH:mm:ss.SSSZ, timeZone=GMT, print=2019-08-18T02:57:55.333+0000
format=yyyy-MM-dd'T'HH:mm:ss.SSSZ, timeZone=UTC, print=2019-08-18T02:57:55.333+0000

從例子中可以得到如下結(jié)論

  1. SimpleDateFormat不設(shè)置時區(qū)默認(rèn)使用本地時區(qū)
  2. 在使用UTC時間的情況下,要注意yyyy-MM-dd'T'HH:mm:ss.SSS'Z'返回的是標(biāo)準(zhǔn)時間(少了8小時)
  3. 寫成帶時區(qū)的格式yyyy-MM-dd'T'HH:mm:ss.SSSZ魁淳,即使不配時區(qū)也能從字面意思中翻譯出北京時間飘诗,如2019-08-18T02:57:55.333+0000

日期格式反序列化全局配置

直接上代碼,這里偷懶用到了apache common工具類中的DateUtils

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.text.ParseException;
import java.util.Date;

@Configuration
public class JacksonConfiguration {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper om = new ObjectMapper();
        om.setSerializationInclusion(Include.NON_NULL);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Date.class, new JsonDateDeserialize());
        om.registerModule(module);
        return om;
    }
}

@Slf4j
class JsonDateDeserialize extends JsonDeserializer {
    // 按照此優(yōu)先級順序嘗試轉(zhuǎn)換日期格式界逛,建議從配置文件加載
    private static final String[] patterns = {"yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm:ss"};

    public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        String dateAsString = jp.getText();
        Date parseDate = null;
        if (dateAsString.contains("-")) {
            // 日期類型
            try {
                parseDate = DateUtils.parseDate(dateAsString, patterns);
            } catch (ParseException e) {
                log.error(String.format("不支持的日期格式: %s, error=%s", dateAsString, e.getMessage()), e);
            }
        } else {
            // long毫秒
            try {
                long time = Long.valueOf(dateAsString);
                parseDate = new Date(time);
            } catch (NumberFormatException e) {
                log.error(String.format("日期格式非數(shù)字: %s, error=%s", dateAsString, e.getMessage()), e);
            }
        }
        return parseDate;
    }
}

mysql出現(xiàn)的時區(qū)問題

表現(xiàn)為入庫時間昆稿,或者binlog日志中查詢更新的實際日期與傳遞參數(shù)相差N小時,一般解決方法是在jdbc連接url中添加時區(qū)serverTimezone設(shè)置

serverTimezone=GMT%2B8
#或者
serverTimezone=Asia/Shanghai

例如:

spring:
    datasource:
        url: jdbc:mysql://host:3306/database?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8

參考資料

Jackson教程
Spring Boot中Jackson應(yīng)用詳解
Jackson Annotations

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末息拜,一起剝皮案震驚了整個濱河市溉潭,隨后出現(xiàn)的幾起案子净响,更是在濱河造成了極大的恐慌,老刑警劉巖喳瓣,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馋贤,死亡現(xiàn)場離奇詭異,居然都是意外死亡畏陕,警方通過查閱死者的電腦和手機(jī)配乓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惠毁,“玉大人犹芹,你說我怎么就攤上這事【洗拢” “怎么了腰埂?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長洞豁。 經(jīng)常有香客問我盐固,道長,這世上最難降的妖魔是什么丈挟? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任刁卜,我火速辦了婚禮,結(jié)果婚禮上曙咽,老公的妹妹穿的比我還像新娘蛔趴。我一直安慰自己,他們只是感情好例朱,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布孝情。 她就那樣靜靜地躺著,像睡著了一般洒嗤。 火紅的嫁衣襯著肌膚如雪箫荡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天渔隶,我揣著相機(jī)與錄音羔挡,去河邊找鬼。 笑死间唉,一個胖子當(dāng)著我的面吹牛绞灼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播呈野,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼低矮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了被冒?” 一聲冷哼從身側(cè)響起军掂,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤轮蜕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后良姆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肠虽,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年玛追,在試婚紗的時候發(fā)現(xiàn)自己被綠了税课。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡痊剖,死狀恐怖韩玩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陆馁,我是刑警寧澤找颓,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站叮贩,受9級特大地震影響击狮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜益老,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一彪蓬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捺萌,春花似錦档冬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至态坦,卻和暖如春盐数,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伞梯。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工娘扩, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人壮锻。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像涮阔,于是被迫代替她去往敵國和親猜绣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349