hutool工具之DateUtil使用DateUtil.beginOfDay(calendar)和DateUtil.beginOfMonth(calendar)的一次意外记录

文章讲述了在项目中遇到的功能需求,需要定时获取第三方系统的全年费用数据并按日、月、年统计。作者通过Hutool库实现代码,但在计算本日费用时遇到问题,经调试和源码分析发现Hutool在处理Calendar对象时可能会影响时间,最终给出了优化建议:使用DateTime而非共享Calendar实例以避免时间偏差。
摘要由CSDN通过智能技术生成

功能需求

很简单的需求,项目定时任务,需要定时获取截止到现在的全年费用金额数据(第三方系统),然后分别统计本日费用总和、本月费用总和、本年费用总和。

处理思路

获取当年第一天开始时间Y、本月第一天开始时间M、当日开始时间D、现在截至时间N,发送接口查询请求,获取 Y - N之间的数据,分别统计汇总,得出结果。

代码处理

此处依赖为

	 <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
// 获取本年开始时间
long yearStartTime = DateUtil.beginOfYear(calendar).getTimeInMillis();
// 获取本月开始时间
long monthStartTime = DateUtil.beginOfMonth(calendar).getTimeInMillis();
// 获取本日开始时间
long dayStartTime = DateUtil.beginOfDay(calendar).getTimeInMillis();
// 获取通用截至时间
long commonEndTime  = DateUtil.date(calendar).getTime();

/**
 * 处理金额
 *
 * @param list           列表
 * @param monthStartTime 月开始时间
 * @param dayStartTime   一天开始时间
 * @param commonEndTime   通用结束日期
 * @return {@link Map}<{@link String}, {@link BigDecimal}>
 */
private <T>  Map<String, BigDecimal> processMoney(
            List<T> list, Long monthStartTime, Long dayStartTime, Long commonEndTime
    ) {
        Map<String, BigDecimal> finalMap = new HashMap<>();
        BigDecimal yearAmountTemp = ZERO; // 年报销总额初始化
        BigDecimal monthAmountTemp = ZERO; // 月报销总额初始化
        BigDecimal dayAmountTemp = ZERO; // 日报销总额初始化
        // 遍历报销列表,累计年、月、日的报销金额
        for (T responseDTO : list) {
            yearAmountTemp = yearAmountTemp.add(responseDTO.getPaymentAmount().getAmount());
            if (isInPeriod(responseDTO.getProcInstEndTime(), monthStartTime, commonEndTime)) {
                monthAmountTemp = monthAmountTemp.add(responseDTO.getPaymentAmount().getAmount());
                if (isInPeriod(responseDTO.getProcInstEndTime(), dayStartTime, commonEndTime)) {
                    dayAmountTemp = dayAmountTemp.add(responseDTO.getPaymentAmount().getAmount());
                }
            }
        }
        finalMap.put(YEAR_TOTAL_EXPENSE_AMOUNT, yearAmountTemp);
        finalMap.put(MONTH_TOTAL_EXPENSE_AMOUNT, monthAmountTemp);
        finalMap.put(DAY_TOTAL_EXPENSE_AMOUNT, dayAmountTemp);
        return finalMap;
    }

异常出现

本日费用计算无论如何都是空的,本月和本年费用都存在正确的值,后面核实,确实存在当日费用汇总,但是为啥统计出问题了呢?processMoney方法不可能存在异常的吧?

代码debug

	遇事不决,debug启动!
	然后就发现,不对劲,不对劲,怎么都debug进不去这个方法
 if (isInPeriod(responseDTO.getProcInstEndTime(), dayStartTime, commonEndTime)) {
     dayAmountTemp=dayAmountTemp.add(responseDTO.getPaymentAmount().getAmount());
}

然后怀疑是时间获取上出问题了,准备拉一个test测试时间过去的这段代码

test测试

	直接上图片解释

在这里插入图片描述
什么原因呢?接着往下看
在这里插入图片描述

在这里插入图片描述

什么!calendar.getTime() 时间变了!!!

好,那么好,这时候就可以猜测,hutool工具的处理Calendar类型的变量是对它存在修改的!

开始看源码

	//处理年开始的方法
	public static Calendar beginOfYear(Calendar calendar) {
		// DateField.YEAR ,其中YEAR(1)
	   	return truncate(calendar, DateField.YEAR);
	}
	// 这步其实已经看出来了是存在更新修改的
	public static Calendar truncate(Calendar calendar, DateField dateField) {
	    return DateModifier.modify(calendar, dateField.getValue(), ModifyType.TRUNCATE);
	}
	public static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType) {
	    return modify(calendar, dateField, modifyType, false);
	}
	// 更新的方法,modifyType 为更新类型,truncateMillisecond为是否更新到ms级别
    public static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType, boolean truncateMillisecond) {
       int i;
       if (9 == dateField) {
           boolean isAM = DateUtil.isAM(calendar);
           switch (modifyType) {
               case TRUNCATE:
               		// 主要使用的方法,存在set的方法修改变量calendar的值的
                   calendar.set(11, isAM ? 0 : 12);
                   break;
               case CEILING:
                   calendar.set(11, isAM ? 11 : 23);
                   break;
               case ROUND:
                   i = isAM ? 0 : 12;
                   int max = isAM ? 11 : 23;
                   int href = (max - i) / 2 + 1;
                   int value = calendar.get(11);
                   calendar.set(11, value < href ? i : max);
           }

           return modify(calendar, dateField + 1, modifyType);
       } else {
           int endField = truncateMillisecond ? 13 : 14;

           for(i = dateField + 1; i <= endField; ++i) {
               if (!ArrayUtil.contains(IGNORE_FIELDS, i)) {
                   if (4 != dateField && 3 != dateField) {
                       if (7 == i) {
                           continue;
                       }
                   } else if (5 == i) {
                       continue;
                   }

                   modifyField(calendar, i, modifyType);
               }
           }

           if (truncateMillisecond) {
               calendar.set(14, 0);
           }

           return calendar;
       }
   }

关键代码

	// 关键代码
    private static void modifyField(Calendar calendar, int field, ModifyType modifyType) {
       if (10 == field) {
           field = 11;
       }

       switch (modifyType) {
           case TRUNCATE:
           		// 更新了calendar的值
               calendar.set(field, DateUtil.getBeginValue(calendar, field));
               break;
           case CEILING:
               calendar.set(field, DateUtil.getEndValue(calendar, field));
               break;
           case ROUND:
               int min = DateUtil.getBeginValue(calendar, field);
               int max = DateUtil.getEndValue(calendar, field);
               int href;
               if (7 == field) {
                   href = (min + 3) % 7;
               } else {
                   href = (max - min) / 2 + 1;
               }

               int value = calendar.get(field);
               // 修改 calendar 属性字段,类似YEAR、MONTH、DAY
               calendar.set(field, value < href ? min : max);
       }

   }

到这里,一切都清晰明了!

下面是小许这次工具类的经验总结:

  1. 遇事不决开始debug
  2. 快速定位bug范围,拉出代码进行单元测试
  3. 定位bug,查出原因,查看源码学习(避免踩雷)

hutool类使用DateUtil的总结
1、使用DateUtil.beginOfDay(calendar)和DateUtil.beginOfMonth(calendar) 等入参为Calendar对象的方法时,需要注意,此时此变量是否发生改变,看一下ai的回答吧
在这里插入图片描述
2、 为了解决使用calendar对象入参发生改变的问题

  • 每次入参都使用全新的calendar实例(非常极端情况下会出现跨天,导致系统逻辑bug)
	long dayStartTime = DateUtil.beginOfDay(Calendar.getInstance()).getTimeInMillis();
  • 使用DateTime作为入参(推荐),其底层还是返回calendar对象实例,但是时间上不会存在偏差
	long dayStartTime = DateUtil.beginOfDay(dateTime).getTime();

源码核心代码

 	public static DateTime beginOfDay(Date date) {
        return new DateTime(beginOfDay(calendar(date)));
    }
    public static Calendar calendar(Date date) {
        return date instanceof DateTime ? ((DateTime)date).toCalendar() : calendar(date.getTime());
    }
     public Calendar toCalendar() {
        return this.toCalendar(Locale.getDefault(Category.FORMAT));
    }
        public Calendar toCalendar(TimeZone zone, Locale locale) {
        if (null == locale) {
            locale = Locale.getDefault(Category.FORMAT);
        }

        Calendar cal = null != zone ? Calendar.getInstance(zone, locale) : Calendar.getInstance(locale);
        cal.setFirstDayOfWeek(this.firstDayOfWeek.getValue());
        if (this.minimalDaysInFirstWeek > 0) {
            cal.setMinimalDaysInFirstWeek(this.minimalDaysInFirstWeek);
        }

        cal.setTime(this);
        return cal;
    }

最后谢谢大家看到这里啦!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值