概述
Jackson作為SpringBoot中默認(rèn)的JSON mapping庫,在java項目中應(yīng)用十分廣泛扣甲,你在項目實踐中是不是遇到過這樣的問題:
- 日期格式看上去沒問題篮赢,但是序列化之后輸出的字符串差了8小時
- 服務(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'
中的T
和Z
(由此可見這種帶引號的'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é)論
- SimpleDateFormat不設(shè)置時區(qū)默認(rèn)使用本地時區(qū)
- 在使用UTC時間的情況下,要注意
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
返回的是標(biāo)準(zhǔn)時間(少了8小時) - 寫成帶時區(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