4hutool源码分析:DateUtil(时间工具类)-格式化时间(万字长文源码分析,学大佬如何写代码)

/**

  • 格式化日期时间为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”;

然后会调用format(LocalDateTime time, DateTimeFormatter formatter),DateTimeFormatter 这个也是(Java8支持的日期格式化器类,是线程安全的)。

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

image-20210725113946673

然后执行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 {

}

image-20210725115905355

然后:调用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源码的日期和时间相关用法):

  1. 在正常配置按照标准格式的字符串日期,是能够正常转换的。如果月,日,时,分,秒在不足两位的情况需要补0,否则的话会转换失败,抛出异常。

  2. 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},注意此对象非线程安全!

  • 此对象默认为严格格式模式,即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不会帮忙计算,直接抛出异常。

image-20210725122714337

然后**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);

image-20210725195348793

到这里已经是jdk的源码了DateTimeFormatter.format

image-20210725195424950

image-20210725195522610

image-20210725195636339

从上面可知,会调用 NumberPrinterParser.format() NumberPrinterParser是在DateTimeFormatterBuilder类中的。

image-20210725195947802

到这一步会报错

image-20210725200153850

为什么会报错呢,我们来看下context.getValue(field)发生了什么:

image-20210725200349650

从上面代码可行,temporal实际上是Instant对象,Instant.getLong只支持四种字段类型。。

NANO_OF_SECOND

MICRO_OF_SECOND

MILLI_OF_SECOND

INSTANT_SECONDS

image-20210725200551164

如果不是上面这几种字段类型,则抛出异常

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);

image-20210725201444728

官方改法


修订版本 #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()));

}

image-2021072651290

方法名称:DateUtil.formatDateTime(java.util.Date)

================================================================================================================

方法描述


格式化日期时间

格式 yyyy-MM-dd HH:mm:ss

源码分析一


/**

  • 格式化日期时间

  • 格式 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©;

}

先把date转化为Calendar,方便获取日期和时间

private String applyRulesToString(final Calendar c) {

return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString();

}

private 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);

}

return buf;

}

核心的代码是这块

for (final Rule rule : this.rules) {

rule.appendTo(buf, calendar);

}

测试demo

String dateStr = “2017-03-01”;

Date date = DateUtil.parse(dateStr);

String formatDateTime = DateUtil.formatDateTime(date);

Assert.assertEquals(“2017-03-01 00:00:00”, formatDateTime);

断点跟进代码:

image-20210725211322291

往下跟代码

image-20210725203801875

//FastDatePrinter

private static class PaddedNumberField implements NumberRule {

@Override

public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {

appendTo(buffer, calendar.get(mField));

}

/**

  • {@inheritDoc}

*/

@Override

public final void appendTo(final Appendable buffer, final int value) throws IOException {

appendFullDigits(buffer, value, mSize);

}

…}

已经取到年份了:2017

image-20210725204215108

往下跟代码:

image-20210725211459199

//FastDatePrinter

private static class CharacterLiteral implements Rule {

@Override

public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {

buffer.append(mValue);

}

}

就是把’-'字符串直接拼接上去。

下一个获取月份:

image-20210725211534912

//FastDatePrinter

private static class TwoDigitMonthField implements NumberRule {

/**

  • {@inheritDoc}

*/

@Override

public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {

appendTo(buffer, calendar.get(Calendar.MONTH) + 1);

}

/**

  • {@inheritDoc}

*/

@Override

public final void appendTo(final Appendable buffer, final int value) throws IOException {

appendDigits(buffer, value);

}

}

获取了月份:3

image-20210725205900990

然后下一个:把’-'字符串直接拼接上去。

image-20210725211609400

总结

以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!

下面给大家分享下我的面试大全资料

  • 第一份是我的后端JAVA面试大全

image.png

后端JAVA面试大全

  • 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理

字节二面拜倒在“数据库”脚下,闭关修炼半个月,我还有机会吗?

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

  • 第三份是Spring全家桶资料

字节二面拜倒在“数据库”脚下,闭关修炼半个月,我还有机会吗?

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

Exception {

buffer.append(mValue);

}

}

就是把’-'字符串直接拼接上去。

下一个获取月份:

image-20210725211534912

//FastDatePrinter

private static class TwoDigitMonthField implements NumberRule {

/**

  • {@inheritDoc}

*/

@Override

public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {

appendTo(buffer, calendar.get(Calendar.MONTH) + 1);

}

/**

  • {@inheritDoc}

*/

@Override

public final void appendTo(final Appendable buffer, final int value) throws IOException {

appendDigits(buffer, value);

}

}

获取了月份:3

image-20210725205900990

然后下一个:把’-'字符串直接拼接上去。

image-20210725211609400

总结

以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!

下面给大家分享下我的面试大全资料

  • 第一份是我的后端JAVA面试大全

[外链图片转存中…(img-PUK1fvFQ-1714253281689)]

后端JAVA面试大全

  • 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理

[外链图片转存中…(img-15YfGwL6-1714253281690)]

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

  • 第三份是Spring全家桶资料

[外链图片转存中…(img-K09oqK4I-1714253281690)]

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 21
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值