4hutool源码分析:DateUtil(时间工具类)-格式化时间(万字长文源码分析,学大佬如何写代码)
Posted 小虚竹
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4hutool源码分析:DateUtil(时间工具类)-格式化时间(万字长文源码分析,学大佬如何写代码)相关的知识,希望对你有一定的参考价值。
技术活,该赏
点赞再看,养成习惯
看本篇文章前,建议先对java源码的日期和时间有一定的了解,如果不了解的话,可以先看这篇文章:
关联文章:
源码分析目的
知其然,知其所以然
项目引用
此博文的依据:hutool-5.6.5版本源码
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.6.5</version>
</dependency>
方法名称:DateUtil.formatLocalDateTime(java.time.LocalDateTime)
方法描述
格式化日期时间<br>
格式 yyyy-MM-dd HH:mm:ss
源码分析一
/**
* 格式化日期时间<br>
* 格式 yyyy-MM-dd HH:mm:ss
*
* @param localDateTime 被格式化的日期
* @return 格式化后的字符串
*/
public static String formatLocalDateTime(LocalDateTime localDateTime) {
return LocalDateTimeUtil.formatNormal(localDateTime);
}
首先formatLocalDateTime方法的入参是LocalDateTime(Java8支持的日期时间类,是线程安全的)
然后调用LocalDateTimeUtil.formatNormal(localDateTime)
//LocalDateTimeUtil
/**
* 格式化日期时间为yyyy-MM-dd HH:mm:ss格式
*
* @param time {@link LocalDateTime}
* @return 格式化后的字符串
* @since 5.3.11
*/
public static String formatNormal(LocalDateTime time) {
return format(time, DatePattern.NORM_DATETIME_FORMATTER);
}
/**
* 格式化日期时间为指定格式
*
* @param time {@link LocalDateTime}
* @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter}
* @return 格式化后的字符串
*/
public static String format(LocalDateTime time, DateTimeFormatter formatter) {
return TemporalAccessorUtil.format(time, formatter);
}
跟代码,发现DatePattern.NORM_DATETIME_FORMATTER的日期时间格式为:
/**
* 标准日期时间格式,精确到秒:yyyy-MM-dd HH:mm:ss
*/
public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
hutool这里做了很好的示范,使用DateTimeFormatter替换了SimpleDateFormat(线程不安全的)。
为什么SimpleDateFormat是线程不安全的,请看万字博文教你搞懂java源码的日期和时间相关用法
然后我们继续往下深挖TemporalAccessorUtil.format(time, formatter)
/**
* 格式化日期时间为指定格式
*
* @param time {@link TemporalAccessor}
* @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter}
* @return 格式化后的字符串
* @since 5.3.10
*/
public static String format(TemporalAccessor time, DateTimeFormatter formatter) {
if (null == time) {
return null;
}
if(null == formatter){
formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
}
try {
return formatter.format(time);
} catch (UnsupportedTemporalTypeException e){
if(time instanceof LocalDate && e.getMessage().contains("HourOfDay")){
// 用户传入LocalDate,但是要求格式化带有时间部分,转换为LocalDateTime重试
return formatter.format(((LocalDate) time).atStartOfDay());
}else if(time instanceof LocalTime && e.getMessage().contains("YearOfEra")){
// 用户传入LocalTime,但是要求格式化带有日期部分,转换为LocalDateTime重试
return formatter.format(((LocalTime) time).atDate(LocalDate.now()));
}
throw e;
}
}
最前面加了两个入参判空处理,time为null时,返回null;formatter为null时,给格式默认值,eg:2011-12-03T10:15:30
然后执行formatter.format(time)相当于是DateTimeFormatter.format(LocalDateTime)。这样就格式化成功了。
值得一说的是**TemporalAccessorUtil.format(TemporalAccessor time, DateTimeFormatter formatter)**里面有加异常处理机制
try {
return formatter.format(time);
} catch (UnsupportedTemporalTypeException e){
if(time instanceof LocalDate && e.getMessage().contains("HourOfDay")){
// 用户传入LocalDate,但是要求格式化带有时间部分,转换为LocalDateTime重试
return formatter.format(((LocalDate) time).atStartOfDay());
}else if(time instanceof LocalTime && e.getMessage().contains("YearOfEra")){
// 用户传入LocalTime,但是要求格式化带有日期部分,转换为LocalDateTime重试
return formatter.format(((LocalTime) time).atDate(LocalDate.now()));
}
throw e;
}
因为入参TemporalAccessor time的实现类常用的有如下几个(java8提供的):
- LocalDateTime
- LocalDate
- LocalTime
在进行日期时间转化时,日期时间和要转化的格式化字符串要对应上,不然会抛出异常,所以做了如上的补救措施。
方法名称:DateUtil.format(java.time.LocalDateTime, java.lang.String)
方法描述
根据特定格式格式化日期
源码分析一
/**
* 根据特定格式格式化日期
*
* @param localDateTime 被格式化的日期
* @param format 日期格式,常用格式见: {@link DatePattern}
* @return 格式化后的字符串
*/
public static String format(LocalDateTime localDateTime, String format) {
return LocalDateTimeUtil.format(localDateTime, format);
}
首先:hutool提供了常用的日期时间格式
/**
* 日期格式化类,提供常用的日期格式化对象
*
*/
public class DatePattern {
...
}
然后:调用LocalDateTimeUtil.format(localDateTime, format)
/**
* 格式化日期时间为指定格式
*
* @param time {@link LocalDateTime}
* @param format 日期格式,类似于yyyy-MM-dd HH:mm:ss,SSS
* @return 格式化后的字符串
*/
public static String format(LocalDateTime time, String format) {
if (null == time) {
return null;
}
return format(time, DateTimeFormatter.ofPattern(format));
}
源码**format(time, DateTimeFormatter.ofPattern(format))**可以拆解成两部分:
-
DateTimeFormatter.ofPattern(format)
-
format(LocalDateTime time, DateTimeFormatter formatter)
第一部分:**DateTimeFormatter.ofPattern(format)**是把字符串日期时间格式转化为日期时间格式化对象DateTimeFormatter ;
注意DateTimeFormatter.ofPattern(format)的用法是有坑的(代码详解–>万字博文教你搞懂java源码的日期和时间相关用法):
- 在正常配置按照标准格式的字符串日期,是能够正常转换的。如果月,日,时,分,秒在不足两位的情况需要补0,否则的话会转换失败,抛出异常。
- YYYY和DD谨慎使用
第二部分,format(LocalDateTime time, DateTimeFormatter formatter)上面有介绍了,这里就不水字了。
方法名称:DateUtil.format(java.util.Date, java.text.DateFormat)
方法描述
根据特定格式格式化日期
源码分析一
/**
* 根据特定格式格式化日期
*
* @param date 被格式化的日期
* @param format 日期格式,常用格式见: {@link DatePattern}
* @return 格式化后的字符串
*/
public static String format(Date date, String format) {
if (null == date || StrUtil.isBlank(format)) {
return null;
}
TimeZone timeZone = null;
if (date instanceof DateTime) {
timeZone = ((DateTime) date).getTimeZone();
}
return format(date, newSimpleFormat(format, null, timeZone));
}
从代码中**format(Date date, String format)**方法提供了两个入参,一个是Date 类型的 被格式化的日期和要日期格式的字符串。这是为了兼容java8之前的旧日期时间API提供的方法。
方法内首先对两个参数加了判空处理。
然后判断时间是否是hutool的DateTime对象,如果是,则获取时区TimeZone
接着调用format(date, newSimpleFormat(format, null, timeZone)),可拆解成两部分:
-
newSimpleFormat(format, null, timeZone),获取SimpleDateFormat对象(注:此方法是非线程安全的)
-
format(Date date, DateFormat format) 根据特定格式格式化日期
首先:**newSimpleFormat(format, null, timeZone)**代码详解:
/**
* 创建{@link SimpleDateFormat},注意此对象非线程安全!<br>
* 此对象默认为严格格式模式,即parse时如果格式不正确会报错。
*
* @param pattern 表达式
* @param locale {@link Locale},{@code null}表示默认
* @param timeZone {@link TimeZone},{@code null}表示默认
* @return {@link SimpleDateFormat}
* @since 5.5.5
*/
public static SimpleDateFormat newSimpleFormat(String pattern, Locale locale, TimeZone timeZone) {
if (null == locale) {
locale = Locale.getDefault(Locale.Category.FORMAT);
}
final SimpleDateFormat format = new SimpleDateFormat(pattern, locale);
if (null != timeZone) {
format.setTimeZone(timeZone);
}
format.setLenient(false);
return format;
}
如果**format(Date date, String format)输入的是Date对象的时间,那format(date, newSimpleFormat(format, null, timeZone))**具象化后,是这样的:format(date, newSimpleFormat(format, null, null))。
//获取当前的语言环境
locale = Locale.getDefault(Locale.Category.FORMAT);
然后new了一个SimpleDateFormat对象。并设置了时区和设置了setLenient,这个方法的含义是是否严格解析日期。setLenient设置为false时,就是严格解析日期:会严格按照日期时间格式,java不会帮忙计算,直接抛出异常。
然后**format(Date date, DateFormat format) **代码分析:
/**
* 根据特定格式格式化日期
*
* @param date 被格式化的日期
* @param format {@link SimpleDateFormat}
* @return 格式化后的字符串
*/
public static String format(Date date, DateFormat format) {
if (null == format || null == date) {
return null;
}
return format.format(date);
}
对两个入参进行了判空处理。然后调用SimpleDateFormat.format(date),这是java8之前就有提供的方法。
方法名称:DateUtil.format(java.util.Date, java.time.format.DateTimeFormatter)(方法有问题,已反馈,官方已修正)
方法描述
根据特定格式格式化日期
源码分析一
/**
* 根据特定格式格式化日期
*
* @param date 被格式化的日期
* @param format {@link DateTimeFormatter}
* @return 格式化后的字符串
* @since 5.0.0
*/
public static String format(Date date, DateTimeFormatter format) {
if (null == format || null == date) {
return null;
}
return format.format(date.toInstant());
}
首先对两个入参做了判空处理。
然后,执行了format.format(date.toInstant()),代码可拆解成两部分:
- date.toInstant():返回Instant对象
- DateTimeFormatter.format(Instant):java8提供的格式化日期时间的方法
代码**DateTimeFormatter.format(Instant)**是怎么处理的呢?
public String format(TemporalAccessor temporal) {
StringBuilder buf = new StringBuilder(32);
formatTo(temporal, buf);
return buf.toString();
}
首先new了个StringBuilder对象,用来拼接字符串;
然后调用**formatTo(temporal, buf)**方法
public void formatTo(TemporalAccessor temporal, Appendable appendable) {
Objects.requireNonNull(temporal, "temporal");
Objects.requireNonNull(appendable, "appendable");
try {
DateTimePrintContext context = new DateTimePrintContext(temporal, this);
if (appendable instanceof StringBuilder) {
printerParser.format(context, (StringBuilder) appendable);
} else {
// buffer output to avoid writing to appendable in case of error
StringBuilder buf = new StringBuilder(32);
printerParser.format(context, buf);
appendable.append(buf);
}
} catch (IOException ex) {
throw new DateTimeException(ex.getMessage(), ex);
}
}
**formatTo(temporal, buf)**方法也是先判断两个入参空处理。
然后,Instant对象被封装在一个新new的DateTimePrintContext对象
运行demo有问题,进行排查
//根据特定格式格式化日期
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String dateStr = DateUtil.format(new Date(),dtf);
System.out.println(dateStr);
到这里已经是jdk的源码了DateTimeFormatter.format
从上面可知,会调用 NumberPrinterParser.format() NumberPrinterParser是在DateTimeFormatterBuilder类中的。
到这一步会报错
为什么会报错呢,我们来看下context.getValue(field)发生了什么:
从上面代码可行,temporal实际上是Instant对象,Instant.getLong只支持四种字段类型。。
NANO_OF_SECOND
MICRO_OF_SECOND
MILLI_OF_SECOND
INSTANT_SECONDS
如果不是上面这几种字段类型,则抛出异常
DateUtil.format当遇到DateTimeFormatter会将Date对象首先转换为Instant,因为缺少时区,导致报错。
建议改法
/**
* 根据特定格式格式化日期
*
* @param date 被格式化的日期
* @param format {@link SimpleDateFormat} todo-zhw DateTimeFormatter
* @return 格式化后的字符串
* @since 5.0.0
*/
public static String format(Date date, DateTimeFormatter format) {
if (null == format || null == date) {
return null;
}
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
return format.format(localDateTime);
}
先把date类型转化为LocalDateTime类型,然后再进行DateTimeFormatter.format(LocalDateTime)的格式化
测试demo
//根据特定格式格式化日期
String str = "2021-07-25 20:11:25";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd");
Date date = DateUtil.parse(str);
String dateStr = DateUtil.format(date,dtf);
System.out.println(dateStr);
Assert.assertEquals(str, dateStr);
官方改法
修订版本 #5.7.5
/**
* 根据特定格式格式化日期
*
* @param date 被格式化的日期
* @param format {@link SimpleDateFormat} {@link DatePattern#NORM_DATETIME_FORMATTER}
* @return 格式化后的字符串
* @since 5.0.0
*/
public static String format(Date date, DateTimeFormatter format) {
if (null == format || null == date) {
return null;
}
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra
// 出现以上报错时,表示Instant时间戳没有时区信息,赋予默认时区
return TemporalAccessorUtil.format(date.toInstant(), format);
}
更换了新的调用方法TemporalAccessorUtil.format(date.toInstant(), format),date.toInstant()返回Instant对象,则变成了TemporalAccessorUtil.format(Instant, format)
//TemporalAccessorUtil
/**
* 格式化日期时间为指定格式
*
* @param time {@link TemporalAccessor}
* @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter}
* @return 格式化后的字符串
* @since 5.3.10
*/
public static String format(TemporalAccessor time, DateTimeFormatter formatter) {
if (null == time) {
return null;
}
if(null == formatter){
formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
}
try {
return formatter.format(time);
} catch (UnsupportedTemporalTypeException e){
if(time instanceof LocalDate && e.getMessage().contains("HourOfDay")){
// 用户传入LocalDate,但是要求格式化带有时间部分,转换为LocalDateTime重试
return formatter.format(((LocalDate) time).atStartOfDay());
}else if(time instanceof LocalTime && e.getMessage().contains("YearOfEra")){
// 用户传入LocalTime,但是要求格式化带有日期部分,转换为LocalDateTime重试
return formatter.format(((LocalTime) time).atDate(LocalDate.now()));
} else if(time instanceof Instant){
// 时间戳没有时区信息,赋予默认时区
return formatter.format(((Instant) time).atZone(ZoneId.systemDefault()));
}
throw e;
}
}
对比了下跟5.6.5版本的差异,新增了当time是Instant时,给一个默认的时区
else if(time instanceof Instant){
// 时间戳没有时区信息,赋予默认时区
return formatter.format(((Instant) time).atZone(ZoneId.systemDefault()));
}
方法名称:DateUtil.formatDateTime(java.util.Date)
方法描述
格式化日期时间<br>
格式 yyyy-MM-dd HH:mm:ss
源码分析一
/**
* 格式化日期时间<br>
* 格式 yyyy-MM-dd HH:mm:ss
*
* @param date 被格式化的日期
* @return 格式化后的日期
*/
public static String formatDateTime(Date date) {
if (null == date) {
return null;
}
return DatePattern.NORM_DATETIME_FORMAT.format(date);
}
首先好习惯,先对入参进行判空处理
然后调用DatePattern.NORM_DATETIME_FORMAT.format(date)返回FastDateFormat对象
针对只支持java8之前的程序,可以使用FastDateFormat线程安全替换SimpleDateFormat线程不安全–》源码分析
FastDateFormat 是一个线程安全的实现
来源 Apache Commons Lang 3.5
DatePattern.NORM_DATETIME_FORMAT-->
/**
* 标准日期时间格式,精确到秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss
*/
public static final FastDateFormat NORM_DATETIME_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_PATTERN);
则转化为了FastDateFormat.format(date)
//FastDateFormat
@Override
public String format(final Date date) {
return printer.format(date);
}
//FastDatePrinter
@Override
public String format(final Date date) {
final Calendar c = Calendar.getInstance(timeZone, locale);
c.setTime(date);
return applyRulesToString(c);
}
先把date转化为Calendar,方便获取日期和时间
private String applyRulesToString(final Calendar c) {
return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString();
}
private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) {
try {
for (final Rule rule : this.rules) {
rule.appendTo(buf, calendar);
}
} catch (final IOException e) {
throw new DateException(e<以上是关于4hutool源码分析:DateUtil(时间工具类)-格式化时间(万字长文源码分析,学大佬如何写代码)的主要内容,如果未能解决你的问题,请参考以下文章
1hutool源码分析:DateUtil(时间工具类)-当前时间和当前时间戳
1hutool源码分析:DateUtil(时间工具类)-当前时间和当前时间戳