开发问题解决方案
时间和时间戳的应用场景
Netty的ByteBuf基础知识
JVM生产环境调优工具
i18n注意事项
导致缓存击穿的代码示例
CPU高的问题排查方法
服务器性能监控
复费率读写接口标准
本文档使用 MrDoc 发布
-
+
首页
时间和时间戳的应用场景
# 时间和时间戳的应用场景 在开发过程中,经常会遇到记录时间的场景,例如:创建时间createTime,抄表时间戳timestamp。 Java中有java.util.Date、java.time.LocalDateTime等类,还有Long型时间戳,什么场景用什么类型就存在一定的差异性了。 # 1. Java类 ## 1.1 java.util.Date类 Date类是我们最常用到的时间类了,常见用法如下: ```java Date date = new Date(); // 本质上也是调用了new Date(long timestamp) Calendar.getInstance().getTime() ``` 构造函数如下: ```java public class Date implements java.io.Serializable, Cloneable, Comparable<Date> { private transient long fastTime; public Date() { this(System.currentTimeMillis()); } public Date(long date) { fastTime = date; } } ``` 可以看出,Date对象本质上也是持有了一个时间戳`long fastTime`,表示自格林威治时间( GMT)1970年1月1日0点至Date所表示时刻所经过的毫秒数。 Date类本身时没有时区概念的,所以在格式化为字符串或者存储到数据库中时,需要指定一个时区。一般接口中格式化时,会默认采用服务器时区,而存储到数据库时,会使用服务器的时区设置,例如:MongoDB默认时区为UTC,也就是+00:00,所以我们在数据库中看到的`createTime`都比实际时间要向前推8小时。 ### 1.1.1 SimpleDateFormat 说到Date类,不得不提的就是`java.text.SimpleDateFormat`了,这个类非常坑——**它是线程不安全的**。 网上非常常见的做法是写一个DateUtils类,把SimpleDateFormat按照不同的格式化需求,实例化为多个常量,然后在其他地方调用`SDF.parse()`等方法。 ```java public class SimpleDateFormatTest extends Thread { private static SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private String name; private String date; public SimpleDateFormatTest(String name, String date) { this.name = name; this.date = date; } @Override public void run() { try { Date d = SDF.parse(date); System.out.println(name + ": date:" + d); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { ExecutorService es = Executors.newFixedThreadPool(3); es.execute(new SimpleDateFormatTest("A", "2023-01-01 08:00:00")); es.execute(new SimpleDateFormatTest("B", "2022-01-01 10:00:00")); } } ``` 在多线程环境下执行,经常会抛出异常,有时候还会看到如下错误结果。 ```java 第一次 A: date:Sun Jan 01 08:00:00 CST 2023 B: date:Sun Jan 01 08:00:00 CST 2023 第二次 B: date:Fri Nov 14 08:00:00 CST 2200 A: date:Fri Nov 14 08:00:00 CST 2200 ``` 而且这种多线程并发的问题还极难复现和排查,由此可以看出,整个Date类的使用过程中存在许多坑。 ## 1.2 java.time包 曾经有个最烂Java类投票,Date/Calendar类排第二(第一是XML/DOM类),所以,在Java8中引入了**JSR 310**日期时间,都包括在java.time包中。这里面有3个常用时间类,分别代表日期、时间、日期+时间。 - `java.time.LocalDate` - `java.time.LocalTime` - `java.time.LocalDateTime` 简单看下这三个类的属性: ```java public class LocalDate { private final int year; private final short month; private final short day; } public class LocalTime { private final byte hour; private final byte minute; private final byte second; private final int nano; } public class LocalDateTime { private final LocalDate date; private final LocalTime time; } ``` 可以看到`LocalXXX`类的核心思路是解决**本地时间**的表示问题,和Date类对比下: ```bash Date(fastTime) + TimeZone => "2023-08-15 08:00:00" LocalDateTime => "2023-08-15 08:00:00" ``` LocalDateTime将`2023-08-15 08:00:00`拆分为年、月、日、时、分、秒、纳秒进行存储,直接跳过时区的转换过程,保存的是最终显示的结果。 > **注意:**LocalDateTime可以通过ZoneId进行时区转换,另外还有OffsetDateTime、ZonedDateTime类可以用来表示带时区的时间。 > ## 1.3 时间戳Long timestamp Java中的时间戳timestamp的并不是一个时间类,而是用`Long`型数值表示自格林威治时间( GMT)1970年1月1日0点至Date所表示时刻所经过的毫秒数。 缺点是无法表示1970年之前的时间。 # 2. 方案 ## 2.1 存在问题 大部分中小软件中,用户和服务器都在同一时区,所以用Date、LocalDateTime、时间戳并没有太大差别。 **在提供跨时区服务的平台中,存在一个核心问题——Date类本身不带时区,但是显示时需要一个时区,通常是服务器/数据库时区,这个时区与用户所在时区不一致时,产生视觉偏差。** 现在有一个日志类`Log` ```java public class Log { private Date createTime; } // 实例化 Log log = new Log(); log.setCreateTime(new Date()) ``` 假设此时Date对象持有的时间戳为1692086400,对应的时间如下: ```java 格林威治时间GMT:2023-08-15 00:00:00 北京时间CST:2023-08-15 08:00:0 ``` 当查询接口要将这个Log对象返回给前端时,需要将Date对象转为日期字符串,此时是需要有一个时区偏移量(例如:+08:00),这个是在代码中指定或默认使用服务器时区。 ```java { "createTime": "2023-08-15 08:00:00" } ``` 看起来这没有问题,但是当这条记录是由一个海外用户(例如:保加利亚+03:00)创建时,当地时间是`2023-08-15 03:00:00`,那么用户在查看这条记录时,就会发现时间是错的,因为接口返回的时间字符串是按照+08:00格式化的。 - 前端不知道服务器时区,所以无法在接口返回的`2023-08-15 08:00:00`格式化到+03:00时区 - 服务器不知道前端的时区,所以无法将时间按照前端的时区格式化 ## 2.2 最佳实践 那么,在实际设计一个系统时,我们应当怎样选择Date、LocalDateTime、时间戳呢? 我们先对比下Date和LocalDateTime的差异: - 不可变性:Date是可变类,这意味着可以直接修改Date对象的值。而LocalDateTime是不可变类,一旦创建后就不能被修改。如果需要修改LocalDateTime的值,只能通过创建新的对象来实现。 - 时区处理:Date类在内部表示日期和时间时,会考虑当前的系统时区。而LocalDateTime是不带时区信息的类,它仅仅表示日期和时间,没有时区的概念。如果需要处理时区,可以使用ZonedDateTime类来表示带有时区的日期和时间。 - 精确度:Date类精确到毫秒级别,但是它的设计存在一些问题,容易引发线程安全问题。而LocalDateTime提供了更好的精确度,可以表示纳秒级别的时间。 - API设计:Date类的API设计相对较旧,使用起来不够直观,并且一些方法已被标记为过时。而LocalDateTime是在Java 8中引入的,其API设计更加现代化和易于使用,提供了一系列方便的方法来处理日期和时间。 **在设计一个跨时区的平台时,我们先排除一个错误答案——Date类。很多时候用Date没问题,是因为还没到出问题的时候,例如:并发量不高,或者用户没关注时间数据。** 下面结合公司的实际进行分析: - 海外托管服务器,时区:GMT+0 - 安装在保加利亚的网关,时区:GMT+3 - 保加利亚的用户,时区:GMT+3 - 公司运维人员,时区:GMT+8 系统中包含两类常见数据: - **网关上报的实时状态数据** 在网站上显示网关上报的实时状态、能耗等数据时,是根据设备所在时区进行展示,所以可以根据报文中的时间戳+设备的时区属性,计算出最终的时间,然后用LocalDateTime进行存储和展示。 例如设备在2023-08-20 03:00:00上传了一条数据,那么不管是保加利亚的用户还是公司运维人员查看,都应该是2023-08-20 03:00:00,而不应跟着用户或服务器的时区变化。 - **用户在平台操作的日志** 平台在运行过程中,会生成一些日志,例如:用户操作日志。这些日志的生成时间必须按照发生时间顺序记录,不能按照用户所在时区进行记录。 > 示例: 公司运维人员在北京时间8点(保加利亚时间3点)创建了一个仪表A123456,保加利亚用户在当地时间4点发现多了个表,就删掉了,然后就出现了如下日志: 时间 操作 参数 2023-08-20 04:00:00 删除仪表 A123456 2023-08-20 08:00:00 新增仪表 A123456 > 这样的时间顺序显然不符合实际,那么能不能按照服务器时间保存为LocalDateTime呢? > 示例: 按照服务器时间保存上述日志: 时间 操作 参数 2023-08-20 00:00:00 新增仪表 A123456 2023-08-20 01:00:00 删除仪表 A123456 > 结果保加利亚用户查看日志时,看到的是: ```bash 时间 操作 参数 2023-08-20 00:00:00 新增仪表 A123456 2023-08-20 01:00:00 删除仪表 A123456 ``` 这个显然也不符合保加利亚用户的认知,因为他是4点删除的仪表,日志里却显示1点。 所以,日志里面的通常用`Long timestamp`来保存时间,并且在具体展示时,在前端浏览器中获取操作系统时区,然后将timestamp格式化为用户所在时区的时间。 > 一个很典型的场景:显示时间标签时,是按照我这个浏览者的时间来计算多少小时之前,而不是美国时间的15小时前。 > > >  > **结论:** **选择时间存储类型,取决于最终展示的需要,如果显示时间要根据用户所在时区变更,就保存为Long时间戳;如果要显示数据发生地的时间,就用LocalDateTime。** 举几个例子: - 日志类的时间——`Long` - 数据的创建和更新时间——`Long` - 设备的实时状态、能耗等时间——`LocalDateTime` ### 2.2.1 MySql中的timestamp 需要注意的是,MySql数据库中,有一个字段类型就叫timestamp,这个从很久前就基本不用了,因为它使用32位整数存储时间戳,最大值为2147483647,也就是最多可以用到 2038 年 01 月 19 日 03 时 14 分 07 秒,超过这个时间就会出错变成-2147483648。 所以,MySql中如果存储时间戳,可以直接使用bigint类型,也就是Java中的Long型范围。 # 附录 ### LocalDateUtils工具类 ```java import org.apache.commons.lang3.StringUtils; import java.math.BigDecimal; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAdjusters; import java.util.*; /** * 时间工具类 */ public class LocalDateUtils { public final static BigDecimal BIG_DECIMAL_1 = new BigDecimal("1"); public final static BigDecimal BIG_DECIMAL_60 = new BigDecimal("60"); public final static BigDecimal BIG_DECIMAL_100 = new BigDecimal("100"); /** * 显示年月日时分秒,例如 2015-08-11 09:51:53. */ public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; /** * 仅显示年月日,例如 2015-08-11. */ public static final String DATE_PATTERN = "yyyy-MM-dd"; /** * 仅显示时分秒,例如 09:51:53. */ public static final String TIME_PATTERN = "HH:mm:ss"; /** * 显示年月日时分秒(无符号),例如 20150811095153. */ public static final String UNSIGNED_DATETIME_PATTERN = "yyyyMMddHHmmss"; /** * 仅显示年月日(无符号),例如 20150811. */ public static final String UNSIGNED_DATE_PATTERN = "yyyyMMdd"; /** * 仅显示年月(无符号),例如 201508. */ public static final String UNSIGNED_MONTH_PATTERN = "yyyyMM"; /** * 春天; */ public static final Integer SPRING = 1; /** * 夏天; */ public static final Integer SUMMER = 2; /** * 秋天; */ public static final Integer AUTUMN = 3; /** * 冬天; */ public static final Integer WINTER = 4; /** * 星期日; */ public static final String SUNDAY = "星期日"; /** * 星期一; */ public static final String MONDAY = "星期一"; /** * 星期二; */ public static final String TUESDAY = "星期二"; /** * 星期三; */ public static final String WEDNESDAY = "星期三"; /** * 星期四; */ public static final String THURSDAY = "星期四"; /** * 星期五; */ public static final String FRIDAY = "星期五"; /** * 星期六; */ public static final String SATURDAY = "星期六"; /** * 年 */ private static final String YEAR = "year"; /** * 月 */ private static final String MONTH = "month"; /** * 周 */ private static final String WEEK = "week"; /** * 日 */ private static final String DAY = "day"; /** * 时 */ private static final String HOUR = "hour"; /** * 分 */ private static final String MINUTE = "minute"; /** * 秒 */ private static final String SECOND = "second"; public static String SERVER_TIMEZONE; public static String DEFAULT_STANDARD_TIME_ZONE; public static String DEFAULT_STANDARD_TIME_ZONE_HOUR; public static String DEFAULT_STANDARD_TIME_ZONE_MIN; public static Set<String> TIMEZONE_MIN_SET; public static Set<String> TIMEZONE_HOUR_SET; static { int offset = TimeZone.getDefault().getRawOffset(); int second = offset / 1000; int hr = second / 3600; int min = second % 60; StringBuilder sb = new StringBuilder(); String symbol = hr < 0 ? "-" : "+"; sb.append(symbol); String hrStr = String.valueOf(Math.abs(hr)); if (Math.abs(hr) < 10) { hrStr = "0" + hrStr; } sb.append(hrStr).append(":"); String minStr = String.valueOf(min); if (min < 10) { minStr = "0" + minStr; } sb.append(minStr); SERVER_TIMEZONE = sb.toString(); DEFAULT_STANDARD_TIME_ZONE = SERVER_TIMEZONE; DEFAULT_STANDARD_TIME_ZONE_HOUR = String.valueOf(hr); DEFAULT_STANDARD_TIME_ZONE_MIN = minStr; TIMEZONE_HOUR_SET = new HashSet<>(); for (int i = -12; i <= 14; i++) { TIMEZONE_HOUR_SET.add(String.valueOf(i)); } TIMEZONE_MIN_SET = new HashSet<>(); TIMEZONE_MIN_SET.add("00"); TIMEZONE_MIN_SET.add("15"); TIMEZONE_MIN_SET.add("30"); TIMEZONE_MIN_SET.add("45"); } /** * 获取当前日期和时间字符串. * * @return String 日期时间字符串,例如 2015-08-11 09:51:53 */ public static String getLocalDateTimeStr() { return format(LocalDateTime.now(), DATETIME_PATTERN); } /** * 获取当前日期字符串. * * @return String 日期字符串,例如2015-08-11 */ public static String getLocalDateStr() { return format(LocalDate.now(), DATE_PATTERN); } /** * 获取当前时间字符串. * * @return String 时间字符串,例如 09:51:53 */ public static String getLocalTimeStr() { return format(LocalTime.now(), TIME_PATTERN); } /** * 获取当前星期字符串. * * @return String 当前星期字符串,例如 星期二 */ public static String getDayOfWeekStr() { return format(LocalDate.now(), "E"); } /** * 获取指定日期是星期几 * * @param localDate 日期 * @return String 星期几 */ public static String getDayOfWeekStr(LocalDate localDate) { String[] weekOfDays = {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}; int dayOfWeek = localDate.getDayOfWeek().getValue() - 1; return weekOfDays[dayOfWeek]; } /** * 获取日期时间字符串 * * @param temporal 需要转化的日期时间 * @param pattern 时间格式 * @return String 日期时间字符串,例如 2015-08-11 09:51:53 */ public static String format(TemporalAccessor temporal, String pattern) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); return dateTimeFormatter.format(temporal); } /** * 日期时间字符串转换为日期时间(java.time.LocalDateTime) * * @param localDateTimeStr 日期时间字符串 * @param pattern 日期时间格式 例如DATETIME_PATTERN * @return LocalDateTime 日期时间 */ public static LocalDateTime parseLocalDateTime(String localDateTimeStr, String pattern) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); return LocalDateTime.parse(localDateTimeStr, dateTimeFormatter); } /** * 日期字符串转换为日期(java.time.LocalDate) * * @param localDateStr 日期字符串 * @param pattern 日期格式 例如DATE_PATTERN * @return LocalDate 日期 */ public static LocalDate parseLocalDate(String localDateStr, String pattern) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); return LocalDate.parse(localDateStr, dateTimeFormatter); } /** * 获取指定日期时间加上指定数量日期时间单位之后的日期时间. * * @param localDateTime 日期时间 * @param num 数量 * @param chronoUnit 日期时间单位 * @return LocalDateTime 新的日期时间 */ public static LocalDateTime plus(LocalDateTime localDateTime, int num, ChronoUnit chronoUnit) { return localDateTime.plus(num, chronoUnit); } /** * 获取指定日期时间减去指定数量日期时间单位之后的日期时间. * * @param localDateTime 日期时间 * @param num 数量 * @param chronoUnit 日期时间单位 * @return LocalDateTime 新的日期时间 */ public static LocalDateTime minus(LocalDateTime localDateTime, int num, ChronoUnit chronoUnit) { return localDateTime.minus(num, chronoUnit); } /** * 根据ChronoUnit计算两个日期时间之间相隔日期时间 * * @param start 开始日期时间 * @param end 结束日期时间 * @param chronoUnit 日期时间单位 * @return long 相隔日期时间 */ public static long getChronoUnitBetween(LocalDateTime start, LocalDateTime end, ChronoUnit chronoUnit) { return Math.abs(start.until(end, chronoUnit)); } /** * 根据ChronoUnit计算两个日期之间相隔年数或月数或天数 * * @param start 开始日期 * @param end 结束日期 * @param chronoUnit 日期时间单位,(ChronoUnit.YEARS,ChronoUnit.MONTHS,ChronoUnit.WEEKS,ChronoUnit.DAYS) * @return long 相隔年数或月数或天数 */ public static long getChronoUnitBetween(LocalDate start, LocalDate end, ChronoUnit chronoUnit) { return Math.abs(start.until(end, chronoUnit)); } /** * 获取本年第一天的日期字符串 * * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfYearStr() { return getFirstDayOfYearStr(LocalDateTime.now()); } /** * 获取本年最后一天的日期字符串 * * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfYearStr() { return getLastDayOfYearStr(LocalDateTime.now()); } /** * 获取指定日期当年第一天的日期字符串 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfYearStr(LocalDateTime localDateTime) { return getFirstDayOfYearStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期当年最后一天的日期字符串 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfYearStr(LocalDateTime localDateTime) { return getLastDayOfYearStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期当年第一天的日期字符串,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfYearStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.withDayOfYear(1).withHour(0).withMinute(0).withSecond(0), pattern); } /** * 获取指定日期当年最后一天的日期字符串,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfYearStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.with(TemporalAdjusters.lastDayOfYear()).withHour(23).withMinute(59).withSecond(59), pattern); } /** * 获取本月第一天的日期字符串 * * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfMonthStr() { return getFirstDayOfMonthStr(LocalDateTime.now()); } /** * 获取本月最后一天的日期字符串 * * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfMonthStr() { return getLastDayOfMonthStr(LocalDateTime.now()); } /** * 获取指定日期当月第一天的日期字符串 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getFirstDayOfMonthStr(LocalDateTime localDateTime) { return getFirstDayOfMonthStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期当月最后一天的日期字符串 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfMonthStr(LocalDateTime localDateTime) { return getLastDayOfMonthStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期当月第一天的日期字符串,带日期格式化参数 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfMonthStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0), pattern); } /** * 获取指定日期当月最后一天的日期字符串,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfMonthStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59), pattern); } /** * 获取本周第一天的日期字符串 * * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfWeekStr() { return getFirstDayOfWeekStr(LocalDateTime.now()); } /** * 获取本周最后一天的日期字符串 * * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfWeekStr() { return getLastDayOfWeekStr(LocalDateTime.now()); } /** * 获取指定日期当周第一天的日期字符串,这里第一天为周一 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfWeekStr(LocalDateTime localDateTime) { return getFirstDayOfWeekStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期当周最后一天的日期字符串,这里最后一天为周日 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfWeekStr(LocalDateTime localDateTime) { return getLastDayOfWeekStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期当周第一天的日期字符串,这里第一天为周一,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getFirstDayOfWeekStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0), pattern); } /** * 获取指定日期当周最后一天的日期字符串,这里最后一天为周日,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getLastDayOfWeekStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.with(DayOfWeek.SUNDAY).withHour(23).withMinute(59).withSecond(59), pattern); } /** * 获取今天开始时间的日期字符串 * * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getStartTimeOfDayStr() { return getStartTimeOfDayStr(LocalDateTime.now()); } /** * 获取今天结束时间的日期字符串 * * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getEndTimeOfDayStr() { return getEndTimeOfDayStr(LocalDateTime.now()); } /** * 获取指定日期开始时间的日期字符串 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 00:00:00 */ public static String getStartTimeOfDayStr(LocalDateTime localDateTime) { return getStartTimeOfDayStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期结束时间的日期字符串 * * @param localDateTime 指定日期时间 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getEndTimeOfDayStr(LocalDateTime localDateTime) { return getEndTimeOfDayStr(localDateTime, DATETIME_PATTERN); } /** * 获取指定日期开始时间的日期字符串,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd HH:mm:ss */ public static String getStartTimeOfDayStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.withHour(0).withMinute(0).withSecond(0), pattern); } /** * 获取指定日期结束时间的日期字符串,带日期格式化参数 * * @param localDateTime 指定日期时间 * @param pattern 日期时间格式 * @return String 格式:yyyy-MM-dd 23:59:59 */ public static String getEndTimeOfDayStr(LocalDateTime localDateTime, String pattern) { return format(localDateTime.withHour(23).withMinute(59).withSecond(59), pattern); } /** * 切割日期。按照周期切割成小段日期段。例如: <br> * * @param startDate 开始日期(yyyy-MM-dd) * @param endDate 结束日期(yyyy-MM-dd) * @param period 周期(天,周,月,年) * @return 切割之后的日期集合 * <li>startDate="2019-02-28",endDate="2019-03-05",period="day"</li> * <li>结果为:[2019-02-28, 2019-03-01, 2019-03-02, 2019-03-03, 2019-03-04, 2019-03-05]</li><br> * <li>startDate="2019-02-28",endDate="2019-03-25",period="week"</li> * <li>结果为:[2019-02-28,2019-03-06, 2019-03-07,2019-03-13, 2019-03-14,2019-03-20, * 2019-03-21,2019-03-25]</li><br> * <li>startDate="2019-02-28",endDate="2019-05-25",period="month"</li> * <li>结果为:[2019-02-28,2019-02-28, 2019-03-01,2019-03-31, 2019-04-01,2019-04-30, * 2019-05-01,2019-05-25]</li><br> * <li>startDate="2019-02-28",endDate="2020-05-25",period="year"</li> * <li>结果为:[2019-02-28,2019-12-31, 2020-01-01,2020-05-25]</li><br> */ public static List<String> splitDateList(String startDate, String endDate, String period) { List<String> result = new ArrayList<>(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_PATTERN); LocalDate end = LocalDate.parse(endDate, dateTimeFormatter); LocalDate start = LocalDate.parse(startDate, dateTimeFormatter); LocalDate tmp = start; switch (period) { case DAY: while (start.isBefore(end) || start.isEqual(end)) { result.add(start.toString()); start = start.plusDays(1); } break; case WEEK: while (tmp.isBefore(end) || tmp.isEqual(end)) { if (tmp.plusDays(6).isAfter(end)) { result.add(tmp + "," + end); } else { result.add(tmp + "," + tmp.plusDays(6)); } tmp = tmp.plusDays(7); } break; case MONTH: while (tmp.isBefore(end) || tmp.isEqual(end)) { LocalDate lastDayOfMonth = tmp.with(TemporalAdjusters.lastDayOfMonth()); if (lastDayOfMonth.isAfter(end)) { result.add(tmp + "," + end); } else { result.add(tmp + "," + lastDayOfMonth); } tmp = lastDayOfMonth.plusDays(1); } break; case YEAR: while (tmp.isBefore(end) || tmp.isEqual(end)) { LocalDate lastDayOfYear = tmp.with(TemporalAdjusters.lastDayOfYear()); if (lastDayOfYear.isAfter(end)) { result.add(tmp + "," + end); } else { result.add(tmp + "," + lastDayOfYear); } tmp = lastDayOfYear.plusDays(1); } break; default: break; } return result; } /** * 获取当前的时间戳 * * @return 时间戳 */ public static Long unixTimestamp() { return Clock.systemUTC().millis(); } /** * 获取当前的时间戳 * * @return 时间戳 */ public static Long unixTimestampInSecond() { return unixTimestamp() / 1000; } /** * LocalDateTime对象转为Date对象 * * @param localDateTime 时间对象 * @param atZone 表示时间对象是哪个时区生成的 * @return 在atZone生成的localDateTime,被转换到服务器所在时区的Date对象 */ public static Date localDateTimeToDate(LocalDateTime localDateTime, String atZone) { return Date.from(localDateTime.toInstant(parseZoneOffset(atZone))); } /** * 时间戳转为Date对象 * * @param timestamp * @param atZone * @return */ public static Date unixTimestampToDate(Long timestamp, String atZone) { if (timestamp == null) { return null; } if (StringUtils.isBlank(atZone)) { atZone = localTimezone(); } LocalDateTime localDateTime = fromTimestamp(timestamp, atZone); return localDateTimeToDate(localDateTime, atZone); } /** * 当前服务器时间 * @return */ public static Date serverDate() { LocalDateTime now = LocalDateTime.now(); Date date = Date.from(now.toInstant(parseZoneOffset(SERVER_TIMEZONE))); return date; } /** * LocalDateTime转为UnixTimestamp * * @param localDateTime 时间对象 * @param atZone 时间对象所在时区 * @return UnixTimestamp */ public static Long localDateTimeToMillis(LocalDateTime localDateTime, String atZone) { return localDateTime.toInstant(parseZoneOffset(atZone)).toEpochMilli(); } /** * LocalDateTime转为UnixTimestamp秒数 * * @param localDateTime 时间对象 * @param atZone 时间对象所在时区 * @return UnixTimestamp秒数 */ public static Long localDateTimeToSecond(LocalDateTime localDateTime, String atZone) { return localDateTime.toEpochSecond(parseZoneOffset(atZone)); } /** * LocalDate的开始时间转为UnixTimestamp * * @param localDate 日期对象 * @param atZone 时间对象所在时区 * @return UnixTimestamp */ public static Long localDateStartToMillis(LocalDate localDate, String atZone) { return localDate.atStartOfDay(parseZoneOffset(atZone)).toInstant().toEpochMilli(); } /** * LocalDate的开始时间转为UnixTimestamp秒数 * * @param localDate 日期对象 * @param atZone 时间对象所在时区 * @return UnixTimestamp秒数 */ public static Long localDateStartToSecond(LocalDate localDate, String atZone) { return localDateStartToMillis(localDate, atZone) / 1000; } /** * 字符串转时区偏移量 * * @param atZone 所在时区的字符串 * @return 时区偏移用于指明LocalDateTime所在时区 */ public static ZoneOffset parseZoneOffset(String atZone) { if (StringUtils.isBlank(atZone)) { atZone = localTimezone(); } try { return ZoneOffset.of(atZone); } catch (Exception e) { return ZoneOffset.of(localTimezone()); } } /** * 时间戳转LocalDateTime * * @param timestamp 时间戳 * @param timezone 所在时区 * @return 指定时区的时间 */ public static LocalDateTime fromTimestamp(Long timestamp, String timezone) { try { Long second = timestamp; long nano = 0L; if (timestamp > Integer.MAX_VALUE) { second = timestamp / 1000; nano = timestamp - second * 1000; nano *= 1000000; } LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(second, Math.toIntExact(nano), parseZoneOffset(timezone)); return localDateTime; } catch (Exception e) { return null; } } /** * 获取服务器所在时区 * * @return */ public static String localTimezone() { int offset = TimeZone.getDefault().getRawOffset(); int second = offset / 1000; int hr = second / 3600; int min = second % 60; StringBuilder sb = new StringBuilder(); String symbol = hr < 0 ? "-" : "+"; sb.append(symbol); if (Math.abs(hr) < 10) { sb.append("0"); } sb.append(Math.abs(hr)).append(":"); if (min < 10) { sb.append("0"); } sb.append(min); return sb.toString(); } public static String formatTimezone(String hour, String minute) { if (StringUtils.isBlank(hour)) { hour = LocalDateUtils.DEFAULT_STANDARD_TIME_ZONE_HOUR; } if (StringUtils.isBlank(minute)) { minute = LocalDateUtils.DEFAULT_STANDARD_TIME_ZONE_MIN; } if (hour.indexOf(".") > 0) { BigDecimal decimal = new BigDecimal(hour); hour = String.valueOf(decimal.intValue()); int min = decimal.subtract(new BigDecimal(hour)) .multiply(new BigDecimal("60")) .intValue(); minute = String.valueOf(min); if (min < 10) { minute = "0" + minute; } } int hr = Integer.valueOf(hour); int min = Integer.valueOf(minute); StringBuilder sb = new StringBuilder(); String symbol = hr < 0 ? "-" : "+"; sb.append(symbol); if (Math.abs(hr) < 10) { sb.append("0"); } sb.append(Math.abs(hr)).append(":"); if (min < 10) { sb.append("0"); } sb.append(min); return sb.toString(); } } ```
admin
2024年2月22日 08:52
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码