1hutool源码分析:DateUtil(时间工具类)-当前时间和当前时间戳

1,576

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

❤️作者简介:Java领域优质创作者🏆,CSDN博客专家认证🏆,华为云享专家认证🏆

❤️技术活,该赏

❤️点赞 👍 收藏 ⭐再看,养成习惯

看本篇文章前,建议先对java源码的日期和时间有一定的了解,如果不了解的话,可以先看这篇文章:

万字博文教你搞懂java源码的日期和时间相关用法

关联文章:

hutool实战(带你掌握里面的各种工具)目录

1hutool实战:DateUtil(时间工具类)-当前时间


源码分析目的

知其然,知其所以然

项目引用

此博文的依据:hutool-5.6.5版本源码

        <dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-core</artifactId>
			<version>5.6.5</version>
		</dependency>

方法名称:DateUtil.date()

方法描述

当前时间,转换为{@link DateTime}对象

源码分析一

源码:新new了一个DateTime对象,此对象是hutool定义的时间对象,DateTime对象继承了Date对象

/**
	 * 当前时间,转换为{@link DateTime}对象
	 *
	 * @return 当前时间
	 */
	public static DateTime date() {
		return new DateTime();
	}

----------------------------------------
    public class DateTime extends Date {
  	...
    }

断点看DateTime的对象数据,可以直接看到返回了:

  1. 当前时间
  2. 获得一周的第一天,默认为周一
  3. 包含时区

image-20210706204814561

		// 当前时间
		DateTime date = DateUtil.date();
		System.out.println(date.toString());
		
		----------------------------
         
		public String toString() {
		return toString(this.timeZone);
		}
	
	-----------------------------------
        /**
	 * 转为"yyyy-MM-dd HH:mm:ss" 格式字符串<br>
	 * 如果时区不为{@code null},会转换为其时区对应的时间,否则转换为当前时间对应的时区
	 *
	 * @param timeZone 时区
	 * @return "yyyy-MM-dd HH:mm:ss" 格式字符串
	 * @since 4.1.14
	 */
	public String toString(TimeZone timeZone) {
		if (null != timeZone) {
			return toString(DateUtil.newSimpleFormat(DatePattern.NORM_DATETIME_PATTERN, null, timeZone));
		}
		return toString(DatePattern.NORM_DATETIME_FORMAT);
	}

DateTime重写了toString() 方法,格式化了时间,返回"yyyy-MM-dd HH:mm:ss" 格式字符串

image-20210706210932679

从**DateTime(long timeMillis, TimeZone timeZone)中源码中,可看出,在Date(long date)**基础上,多加了timeZone的赋值。

image-20210706213821993

传统写法一

获取当前时间

		//获取当前时间
		Date date = new Date();
		System.out.println(date.toString());
		//格式化时间
		SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss");
		System.out.println("当前时间为: " + ft.format(date));

对比

hutool的写法对比传统写法会更加简洁一些,而且定义了DateTime对象,这个是hutool的DateUtil(时间工具类)的基础。

源码分析二

	/**
	 * 当前时间
	 */
	public DateTime() {
		this(TimeZone.getDefault());
	}
 

**TimeZone.getDefault()**是JDK自带的方法,所属包:java.util;方法返回此主机(程序运行的主机)的默认时区

使用默认时区是否有风险?

我们来看下默认时区是怎么取值的

image-202107085554049

1、java.util.TimeZone类中getDefault方法的源代码显示,它最终是会调用sun.util.calendar.ZoneInfo类的getTimeZone 方法。这个方法为需要的时间区域返回一个作为ID的String参数。

2、时间ID zoneID的获取方式:先从JVM中的user.timezone变量中读取,如果读不到,再读取系统的默认时区

zoneID = getSystemTimeZoneID(javaHome);

   /**
     * Gets the platform defined TimeZone ID.
     **/
    private static native String getSystemTimeZoneID(String javaHome);

看到这个native ,说明已经挖到核心了,到了这一步,还是不清楚是怎么获取系统的默认时区的,那怎么办,JDK代码只能跟到这里。

转战OpenJDK,源码下载方式:gitee.com/mirrors/ope…

3、如果再读不到,就用默认的 GMT_ID = "GMT"

避免风险最佳实践

JVM中的user.timezone变量中设置时区

什么是native

native是一个计算机函数,一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。

native的源码怎么看呢

以**private static native String getSystemTimeZoneID(String javaHome)**为例

getSystemTimeZoneID方法所在的package java.util.TimeZone;

如图所示,找到TimeZone.c下的getSystemTimeZoneID方法

image-20210706233905997

image-20210706234052425

/*
 * Gets the platform defined TimeZone ID
 */
JNIEXPORT jstring JNICALL
Java_java_util_TimeZone_getSystemTimeZoneID(JNIEnv *env, jclass ign,
                                            jstring java_home, jstring country)
{
    const char *cname;
    const char *java_home_dir;
    char *javaTZ;

    if (java_home == NULL)
        return NULL;

    java_home_dir = JNU_GetStringPlatformChars(env, java_home, 0);
    if (java_home_dir == NULL)
        return NULL;

    if (country != NULL) {
        cname = JNU_GetStringPlatformChars(env, country, 0);
        /* ignore error cases for cname */
    } else {
        cname = NULL;
    }

    /*
     * Invoke platform dependent mapping function
     */
    javaTZ = findJavaTZ_md(java_home_dir, cname);

    free((void *)java_home_dir);
    if (cname != NULL) {
        free((void *)cname);
    }

    if (javaTZ != NULL) {
        jstring jstrJavaTZ = JNU_NewStringPlatform(env, javaTZ);
        free((void *)javaTZ);
        return jstrJavaTZ;
    }
    return NULL;
}

重点:调用不同平台相关的映射函数

  /*
     * Invoke platform dependent mapping function
     */
    javaTZ = findJavaTZ_md(java_home_dir, cname);

去查找findJavaTZ_md方法时,发现存在分别在solaris和windows两个目录下。

image-20210706234905448

查了下这两个目录的差别:

因为OpenJDK里,Java标准库和部分工具的源码repo(jdk目录)里,BSD和Linux的平台相关源码都是在solaris目录里的。
原本Sun JDK的源码里平台相关的目录就是从solaris和windows这两个目录开始的,后来Unix系的平台相关代码全都放在solaris目录下了,共用大部分代码。

作者:RednaxelaFX
链接:https://www.zhihu.com/question/58982441/answer/170264788
来源:知乎

简单的理解就是:

window系统下,使用windows目录下编译的JDK代码

unix系的平台下,使用solaris目录下编译的JDK代码

了解不同系统下findJavaTZ_md方法执行

windows系统

/*
 * Detects the platform time zone which maps to a Java time zone ID.
 */
char *findJavaTZ_md(const char *java_home_dir, const char *country)
{
    char winZoneName[MAX_ZONE_CHAR];
    char winMapID[MAX_MAPID_LENGTH];
    char *std_timezone = NULL;
    int  result;

    winMapID[0] = 0;
    result = getWinTimeZone(winZoneName, winMapID);

    if (result != VALUE_UNKNOWN) {
        if (result == VALUE_GMTOFFSET) {
            std_timezone = _strdup(winZoneName);
        } else {
            std_timezone = matchJavaTZ(java_home_dir, result,
                                       winZoneName, winMapID, country);
        }
    }

    return std_timezone;
}

注释写得很清楚,获取“Time Zones”注册表中的当前时区

/*
 * Gets the current time zone entry in the "Time Zones" registry.
 */
static int getWinTimeZone(char *winZoneName, char *winMapID)
{
...
}

时区的设置方式:

image-202107086550950

那时区上的选择值是从哪取到的,上面有说了,是在注册表中取值

打开注册表 :Regedit-->

计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\

unix系的平台

findJavaTz_md()方法的注释上写得很清楚了:将平台时区ID映射为Java时区ID

/*
 * findJavaTZ_md() maps platform time zone ID to Java time zone ID
 * using <java_home>/lib/tzmappings. If the TZ value is not found, it
 * trys some libc implementation dependent mappings. If it still
 * can't map to a Java time zone ID, it falls back to the GMT+/-hh:mm
 * form. `country', which can be null, is not used for UNIX platforms.
 */
/*ARGSUSED1*/
char *
findJavaTZ_md(const char *java_home_dir, const char *country)
{
    char *tz;
    char *javatz = NULL;
    char *freetz = NULL;

    tz = getenv("TZ");

#ifdef __linux__
    if (tz == NULL) {
#else
#ifdef __solaris__
    if (tz == NULL || *tz == '\0') {
#endif
#endif
        tz = getPlatformTimeZoneID();
        freetz = tz;
    }

    /*
     * Remove any preceding ':'
     */
    if (tz != NULL && *tz == ':') {
        tz++;
    }

#ifdef __solaris__
    if (strcmp(tz, "localtime") == 0) {
        tz = getSolarisDefaultZoneID();
        freetz = tz;
    }
#endif

    if (tz != NULL) {
#ifdef __linux__
        /*
         * Ignore "posix/" prefix.
         */
        if (strncmp(tz, "posix/", 6) == 0) {
            tz += 6;
        }
#endif
        javatz = strdup(tz);
        if (freetz != NULL) {
            free((void *) freetz);
        }
    }
    return javatz;
}

步骤:

1、使用< Java home>/lib/tzmappings,。如果没有找到"TZ"变量,就进行第2步

2、 tz = getPlatformTimeZoneID(); 执行Linux特定的映射,如果找到,返回一个时区ID,否则返回null

【Linux】Centos7修改系统时区timezone方式:

timedatectl

image-202107086455780

修改时区

timedatectl  set-timezone Asia/Shanghai

image-2021070864438866

3、对比/etc/localtime与"/usr/share/zoneinfo目录下的文件,如果一致,就返回时区ID,没有则到第4步

4、返回到GMT

方法名称:DateUtil.dateSecond()

方法描述

当前时间,转换为{@link DateTime}对象,忽略毫秒部分

源码分析一

/**
	 * 当前时间,转换为{@link DateTime}对象,忽略毫秒部分
	 *
	 * @return 当前时间
	 * @since 4.6.2
	 */
	public static DateTime dateSecond() {
		return beginOfSecond(date());
	}
------------------------------------
    public static DateTime beginOfSecond(Date date) {
		return new DateTime(beginOfSecond(calendar(date)));
	}

--------------------------------------
    public static Calendar beginOfSecond(Calendar calendar) {
		return truncate(calendar, DateField.SECOND);
	}

到这里时,实际上要经过三步方法的调用

  1. calendar(date) 转换为Calendar对象
  2. beginOfSecond(Calendar calendar) 获取秒级别的开始时间,即忽略毫秒部分
  3. DateTime(Calendar calendar) Calendar对象转为DateTime对象
//CalendarUtil 类
/**
	 * 获取秒级别的开始时间,即忽略毫秒部分
	 *
	 * @param calendar 日期 {@link Calendar}
	 * @return {@link Calendar}
	 * @since 4.6.2
	 */
	public static Calendar beginOfSecond(Calendar calendar) {
		return truncate(calendar, DateField.SECOND);
	}

---------------------------------
    /**
	 * 修改日期为某个时间字段起始时间
	 *
	 * @param calendar  {@link Calendar}
	 * @param dateField 时间字段
	 * @return 原{@link Calendar}
	 */
	public static Calendar truncate(Calendar calendar, DateField dateField) {
		return DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.TRUNCATE);
	}

---------------------------------
    //DateModifier类
    /**
	 * 修改日期
	 *
	 * @param calendar {@link Calendar}
	 * @param dateField 日期字段,即保留到哪个日期字段
	 * @param modifyType 修改类型,包括舍去、四舍五入、进一等
	 * @return 修改后的{@link Calendar}
	 */
	public static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType) {
		// AM_PM上下午特殊处理
		if (Calendar.AM_PM == dateField) {
			boolean isAM = DateUtil.isAM(calendar);
			switch (modifyType) {
			case TRUNCATE:
				calendar.set(Calendar.HOUR_OF_DAY, isAM ? 0 : 12);
				break;
			case CEILING:
				calendar.set(Calendar.HOUR_OF_DAY, isAM ? 11 : 23);
				break;
			case ROUND:
				int min = isAM ? 0 : 12;
				int max = isAM ? 11 : 23;
				int href = (max - min) / 2 + 1;
				int value = calendar.get(Calendar.HOUR_OF_DAY);
				calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max);
				break;
			}
			// 处理下一级别字段
			return modify(calendar, dateField + 1, modifyType);
		}

		// 循环处理各级字段,精确到毫秒字段
		for (int i = dateField + 1; i <= Calendar.MILLISECOND; i++) {
			if (ArrayUtil.contains(IGNORE_FIELDS, i)) {
				// 忽略无关字段(WEEK_OF_MONTH)始终不做修改
				continue;
			}

			// 在计算本周的起始和结束日时,月相关的字段忽略。
			if (Calendar.WEEK_OF_MONTH == dateField || Calendar.WEEK_OF_YEAR == dateField) {
				if (Calendar.DAY_OF_MONTH == i) {
					continue;
				}
			} else {
				// 其它情况忽略周相关字段计算
				if (Calendar.DAY_OF_WEEK == i) {
					continue;
				}
			}

			modifyField(calendar, i, modifyType);
		}
		return calendar;
	}

image-202107087711643

循环处理各级字段:

1、// 忽略无关字段(WEEK_OF_MONTH)始终不做修改

/** 忽略的计算的字段 */
private static final int[] IGNORE_FIELDS = new int[] { //
      Calendar.HOUR_OF_DAY, // 与HOUR同名
      Calendar.AM_PM, // 此字段单独处理,不参与计算起始和结束
      Calendar.DAY_OF_WEEK_IN_MONTH, // 不参与计算
      Calendar.DAY_OF_YEAR, // DAY_OF_MONTH体现
      Calendar.WEEK_OF_MONTH, // 特殊处理
      Calendar.WEEK_OF_YEAR // WEEK_OF_MONTH体现
};

2、// 在计算本周的起始和结束日时,月相关的字段忽略。

3、modifyField(calendar, i, modifyType); i=14(含义Calendar.MILLISECOND)

image-2021070874122944

4、calendar.set(field, DateUtil.getBeginValue(calendar, field));

image-2021070874456421

	/**
	 * 获取指定日期字段的最小值,例如分钟的最小值是0
	 *
	 * @param calendar  {@link Calendar}
	 * @param dateField {@link DateField}
	 * @return 字段最小值
	 * @see Calendar#getActualMinimum(int)
	 * @since 4.5.7
	 */
	public static int getBeginValue(Calendar calendar, int dateField) {
		if (Calendar.DAY_OF_WEEK == dateField) {
			return calendar.getFirstDayOfWeek();
		}
		return calendar.getActualMinimum(dateField);
	}

所以就得到我们想要的,忽略毫秒

calendar.set(field, DateUtil.getBeginValue(calendar, field));
-->calendar.set(field,0);//field=14(含义Calendar.MILLISECOND)

image-2021070874736271

方法名称:DateUtil.now()

方法描述

当前时间,格式 yyyy-MM-dd HH:mm:ss

源码分析一

/**
	 * 当前时间,格式 yyyy-MM-dd HH:mm:ss
	 *
	 * @return 当前时间的标准形式字符串
	 */
	public static String now() {
		return formatDateTime(new DateTime());
	}

------------------------------
    	/**
	 * 格式化日期时间<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);
	}

image-202107089719007

//FastDateFormat 是一个线程安全的实现。
public class FastDateFormat extends Format implements DateParser, DatePrinter {
    ...
}
    

凭什么说FastDateFormat是一个线程安全的实现,让我们来深挖一下

	/**
	 * 标准日期时间格式,精确到秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss
	 */
	public static final FastDateFormat NORM_DATETIME_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_PATTERN);

------------------------------------
    public static FastDateFormat getInstance(final String pattern) {
		return CACHE.getInstance(pattern, null, null);
	}

看到CACHE就来精神了,说明这里用了缓存

public F getInstance(final String pattern, TimeZone timeZone, Locale locale) {
		...
		F format = cInstanceCache.get(key);
		...
	}

cInstanceCache用的是ConcurrentHashMap,大家都知道ConcurrentHashMap是线程安全的。

好吧,现在承认FastDateFormat是一个线程安全的实现

ConcurrentMap<Tuple, F> cInstanceCache = new ConcurrentHashMap<>(7);

这边用的key是Tuple,看了下源码

image-202107089459733

/**
 * 不可变数组类型,用于多值返回<br>
 * 多值可以支持每个元素值类型不同
 * 
 * @author Looly
 *
 */
public class Tuple extends CloneSupport<Tuple> implements Iterable<Object>, Serializable{
	private static final long serialVersionUID = -7689304393482182157L;
	
	private final Object[] members;
	private int hashCode;
	private boolean cacheHash;
	
	/**
	 * 构造
	 * @param members 成员数组
	 */
	public Tuple(Object... members) {
		this.members = members;
	}
    ...
}

传统写法一

		//获取当前时间
		Date date = new Date();
		System.out.println(date.toString());
		//格式化时间
		SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss");
		System.out.println("当前时间为: " + ft.format(date));

对比

hutool的写法比较精简,且不需要重复new DateFormat实例对象,FastDateFormat的getInstance 是有缓存机制的。

方法名称:DateUtil.today()

方法描述

当前日期,格式 yyyy-MM-dd

源码分析一

	/**
	 * 当前日期,格式 yyyy-MM-dd
	 *
	 * @return 当前日期的标准形式字符串
	 */
	public static String today() {
		return formatDate(new DateTime());
	}

----------------------------
    	/**
	 * 格式化日期部分(不包括时间)<br>
	 * 格式 yyyy-MM-dd
	 *
	 * @param date 被格式化的日期
	 * @return 格式化后的字符串
	 */
	public static String formatDate(Date date) {
		if (null == date) {
			return null;
		}
		return DatePattern.NORM_DATE_FORMAT.format(date);
	}
//DatePattern类
/**
	 * 标准日期格式:yyyy-MM-dd
	 */
	public static final String NORM_DATE_PATTERN = "yyyy-MM-dd";
/**
 * 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd
 */
public static final FastDateFormat NORM_DATE_FORMAT = FastDateFormat.getInstance(NORM_DATE_PATTERN);

从上面的DateUtil.now的源码分析可知,**FastDateFormat.getInstance(NORM_DATE_PATTERN)**是有缓存机制的,请看上面的DateUtil.now的源码分析