概述
Jackson作为SpringBoot中默认的JSON mapping库,在java项目中应用十分广泛,你在项目实践中是不是遇到过这样的问题:
- 日期格式看上去没问题,但是序列化之后输出的字符串差了8小时
- 服务接口的日期格式不统一,你可能需要各个接口分别适配,不知道如何全局配置反序列化
Jackson简介
Jackson是一个简单基于Java应用库,Jackson可以轻松的将Java对象转换成json对象和xml文档,同样也可以将json、xml转换成Java对象。
ObjectMapper类
ObjectMapper是Jackson库的主要类。它提供一些功能将转换成Java对象匹配JSON结构,反之亦然。它使用JsonParser和JsonGenerator的实例实现JSON实际的读/写。
转换代码
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);
}
}
数据绑定
简单的数据绑定是指JSON映射到Java核心数据类型。下表列出了JSON类型和Java类型之间的关系。
序号 | 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应用中如何使用Jackson
Spring Boot支持与三种JSON mapping库集成:Gson、Jackson和JSON-B。Jackson是首选和默认的。
Jackson是spring-boot-starter-json依赖中的一部分,spring-boot-starter-web中包含spring-boot-starter-json。也就是说,当项目中引入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
方法会发现它使用DateTimeFormatter.ISO_INSTANT
标准时间格式输出,如下:
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);
}
}
}
当我们没有配置时区时,它会执行timeZone = (new ObjectMapper()).getSerializationConfig().getTimeZone();
,进一步查看timeZone属性,发现这其实是默认时区DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC")
看来问题的根源就在这儿,也就是说它其实用的是UTC时间,这就解释 了为什么会少了8小时(本地时间为GMT+8),我原先以为不设置时区会使用JVM时区或系统所在时区(中国时区),所以只要加个时区配置就行了,如下:
# Asia/Shanghai 等同于 GMT+8
spring:
jackson:
date-format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
time-zone: Asia/Shanghai
注意!注意!
我们虽然从表面解决了时间差8小时的问题,但这种方法并不优雅(或者说有些令人费解),上面提到了Instant
打印出来的结果,使用yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
格式就是应该返回标准时间(相对本地时间少8小时),时区本不需要配置或者你应该配置为UTC
,比如上述时间在js中执行
var date = new Date('2019-08-18T02:57:55.234Z');
// Sun Aug 18 2019 10:57:55 GMT+0800 (中国标准时间)
说明前端使用的时候其实是能正确识别的,如果配置加上GMT+8
返回的时间补上8小时,前端在解析的时候反而不正确了,但是这个时间不适合阅读(需要进行一个转换),或许我们该使用不带引号的大Z
(小z
输出的格式js转换会报Invalid Date),下面来说明
日期模式字符串说明
- 文本可以由单引号
'
引起来,这样就不需要解析,比如yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
中的T
和Z
(由此可见这种带引号的'Z'
只是作为占位符,没有实际意义,但是你也可以认为带'Z'的格式表示使用的是UTC标准时间) - 时区可以用小写
z
和大写Z
来表示,小z
表示世界标准时间,大Z
表示RFC 822 time zone
时区说明
- UTC时间 世界标准时间
- GMT时间 格林尼治平时,不再被作为标准时间使用,可以认为其等同于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)));
}
}
}
输出结果
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
从例子中可以得到如下结论
- SimpleDateFormat不设置时区默认使用本地时区
- 在使用UTC时间的情况下,要注意
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
返回的是标准时间(少了8小时) - 写成带时区的格式
yyyy-MM-dd'T'HH:mm:ss.SSSZ
,即使不配时区也能从字面意思中翻译出北京时间,如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 {
// 按照此优先级顺序尝试转换日期格式,建议从配置文件加载
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("日期格式非数字: %s, error=%s", dateAsString, e.getMessage()), e);
}
}
return parseDate;
}
}
mysql出现的时区问题
表现为入库时间,或者binlog日志中查询
或更新
的实际日期与传递参数相差N小时,一般解决方法是在jdbc连接url中添加时区serverTimezone
设置
serverTimezone=GMT%2B8
#或者
serverTimezone=Asia/Shanghai
例如:
spring:
datasource:
url: jdbc:mysql://host:3306/database?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8
网友评论