转至元数据结尾
转至元数据起始

通常情况下,在我们的程序里定义时间数据,就用java中的时间类型,如 java.util.Date、java.time.LocalDate、java.time.LocalDateTime。

如:

// 当前时间
Date date = new Date();
LocalDateTime now = LocalDateTime.now();

// 当前日期
LocalDate today = LocalDate.now();



日期比较,不要转成String进行比较

反例:

// 比较日期是否是今天
void foo(Date myDate) {
	String format1 = DateFormatUtils.format(myDate, "yyyy-MM-dd");
	String format2 = DateFormatUtils.format(new Date(), "yyyy-MM-dd");
	if (format1.equals(format2)){
	    ...
	}
}

正例:

使用 org.apache.commons.lang3.time.DateUtils#isSameDay(java.util.Date, java.util.Date),该util方法通过Calendar来比较两个Date。

// 比较日期是否是今天
void foo(Date myDate) {
	if (DateUtils.isSameDay(myDate, new Date())){
	    ...
	}
}


Date 类也提供了 before()  after() 方法来比较两个Date的先后关系。

//source code in class java.util.Date

/* Tests if this date is before the specified date. */
 public boolean before(Date when) { return getMillisOf(this) < getMillisOf(when); }

/* Tests if this date is after the specified date. */
public boolean after(Date when) { return getMillisOf(this) > getMillisOf(when); }




// 比较日期是否早于今天
void foo(Date myDate) {
	Date today = DateUtils.truncate(new Date(), Calendar.DATE); // 获取当前日期
	if (myDate.before(today)){
	    ...
	}
}


强大的org.apache.commons.lang3.time.DateUtils

org.apache.commons.lang3.time.DateUtils 是 Apache Commons Lang 库中的一个类,提供了一系列便捷的Date操作,可以避免手动编写复杂的日期计算代码。使用这些方法可以简化日期操作,提高代码的可读性和维护性。以下是一些 DateUtils 类中常用的方法:

1. 日期计算

  • addYears(Date date, int amount):在给定的日期上增加指定的年数。

  • addMonths(Date date, int amount):在给定的日期上增加指定的月数。

  • addWeeks(Date date, int amount):在给定的日期上增加指定的周数。

  • addDays(Date date, int amount):在给定的日期上增加指定的天数。

  • addHours(Date date, int amount):在给定的日期上增加指定的小时数。

  • addMinutes(Date date, int amount):在给定的日期上增加指定的分钟数。

  • addSeconds(Date date, int amount):在给定的日期上增加指定的秒数。

2. 日期比较

  • isSameDay(Date date1, Date date2):检查两个日期是否是同一天。

  • isSameInstant(Date date1, Date date2):检查两个日期是否是同一瞬时。

  • isSameLocalTime(Date date1, Date date2):检查两个日期是否是同一本地时间。

3. 日期截断

  • truncate(Date date, int field):截断给定日期到指定粒度,如年、月、日等。

4. 解析和格式化

  • parseDate(String str, String... parsePatterns):解析字符串为日期对象。

  • format(Date date, String pattern):格式化日期对象为指定格式的字符串。

5. 其他

  • round(Date date, int field):将日期舍入到指定粒度。


除了org.apache.commons.lang3.time.DateUtils以外,其他组件例如hutool日期工具,也都在Date操作方面提供了优秀的API。因此,就别往String上靠了,转来转去的挺麻烦,程序也不易读,甚至可能出bug。


Mybatisplus中,对Date属性做eq/between/gt等操作,参数值别傻傻用String了

如下代码中,YqfCertConsumeNotice.createTime是Date类型。

String startDate = DateUtil.format(DateUtil.offsetDay(date, offset), "yyyy-MM-dd HH:mm:ss");
String endDate = DateUtil.format(DateUtil.offsetMinute(date, -3), "yyyy-MM-dd HH:mm:ss");
return list(new LambdaQueryWrapper<YqfCertConsumeNotice>()
	            ...
	            .between(YqfCertConsumeNotice::getCreateTime, startDate, endDate));

如上代码,请改成

Date startDate = DateUtil.offsetDay(date, offset);
Date endDate = DateUtil.offsetMinute(date, -3);
return list(new LambdaQueryWrapper<YqfCertConsumeNotice>()
	            ...
	            .between(YqfCertConsumeNotice::getCreateTime, startDate, endDate));


如何获取某一天的期初时间? ---- 还在通过字符串拼接"00:00:00"来获取某一天的期初时间?

'2024-08-19' 与 '2024-08-19 00:00:00' 是同一时间。


反例:

一个Query类中有个field是 String createTimeBegin。页面或调用者为 createTimeBegin 赋的值是一个日期,如"2024-08-19"。

然后,处理程序里,对其进行了“画蛇添足”式的处理。

query.setCreateTimeBegin(query.getCreateTimeBegin() + "00:00:00");

又或者,在使用Mybatisplus的Wrapper对象时,经常看到这种“画蛇添足”式的代码

queryWrapper.ge(RefundRecord::getOrderEndTime, query.getCreateTimeBegin() + "00:00:00")


正例:

'yyyy-MM-dd' 与 'yyyy-MM-dd 00:00:00' 是同一时间。别再“画蛇添足”了。


还在通过字符串拼接"23:59:59"来指定时间区间最大值?

来看看下面几个代码片段,注意其中所拼接的 23:59:59

-- 代码片段1
LambdaQueryWrapper<EnterpriseProfit> queryWrapper =  new QueryWrapper<EnterpriseProfit>().lambda()
    ...
    .between(StringUtils.isNoneBlank(dto.getProfitCreateTimeBegin(),dto.getProfitCreateTimeEnd()),
            EnterpriseProfit::getProfitCreateTime, dto.getProfitCreateTimeBegin()+ " 00:00:00", dto.getProfitCreateTimeEnd()+ " 23:59:59");

-- 代码片段2
if(StringUtils.isNotBlank(query.getWithdrawStart()) && StringUtils.isNotBlank(query.getWithdrawEnd())) {
    request.setWithdrawalTimeBegin(LocalDateTime.parse(query.getWithdrawStart().trim() + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    request.setWithdrawalTimeEnd(LocalDateTime.parse(query.getWithdrawEnd().trim() + " 23:59:59", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}


-- 代码片段3
return new QueryWrapper<MerRefundRecord>().lambda()
    ...
    .between(StringUtils.isNoneBlank(vo.getOrderEndTimeBegin(), vo.getOrderEndTimeEnd()),
            MerRefundRecord::getOrderEndTime, vo.getOrderEndTimeBegin() + "00:00:00", vo.getOrderEndTimeEnd()+  "23:59:59");

请分析,代码片段3中是"23:59:59"而非" 23:59:59", 这是否bug?


查询特定日期范围内的数据,应使用半开半闭区间。即,要查询 2024-8-1到2024-8-7的数据,时间区间应该是[2024-8-1,2024-8-8) ,注意这里的最大值是 结束日期+1d。

敲黑板!-->如果用字符串拼接"23:59:59"的方式表示为闭区间[2024-8-1,2024-8-7 23:59:59],其实是有潜在问题的————其一,当时间数据的精度到millisecond(毫秒)时,会漏查数据;其二,就是要求开发者格外留意"23:59:59"串前面的空格字符,这显然增加了开发者的负担。

对于SpringMVC接口,时间数据,可以定义为String。不过,需要说明的是↓

仅仅是接口将这个参数定义为String。控制器方法内的程序里,要首先对该数据进行校验,并转换为Date,后面的程序都用这个Date来进行数据操作。绝不是从头到尾都用这个String的日期。

代码示例1:

@RestController
public class DateDemoController {
    @GetMapping("queryByDate")
    Result<List<Order>> queryByDate(String createDate) {
        if (StringUtils.isBlank(createDate)) {
            return Result.error("日期不能为空");
        }

        // ** 将入参createDate变量转换为Date类型的myDate,后面程序都用这个myDate。不再关注createDate。
        Date myDate = null;
        try {
            myDate = DateUtils.parseDate(createDate, "yyyy-MM-dd");
        } catch (ParseException e) {
            return Result.error("日期格式错误,应为 yyyy-MM-dd");
        }
        log.debug("经过转换后的日期参数={}", myDate);
        List<Order> orderList = getOrderList(myDate);
        return Result.success(orderList);
    }

    private List<Order> getOrderList(Date myDate) {
        LambdaQueryWrapper<Order> queryWrapper = Wrappers.lambdaQuery(new Order())
                .ge(Order::getCreateTime, myDate)
                .lt(Order::getCreateTime, DateUtils.addDays(myDate, 1));
        return myMapper.selectList(queryWrapper);
    }
}

反例:

下面代码将入参贯穿到最底层的sql语句里。方法入口只做了判空,但并没有校验 createDate的数据合法性。当 createDate 的值不是有效日期时,例如是"2024年8月7日",或甚至是一个字符串"abc",则会直接影响最终sql的执行。

@RestController
public class DateDemoController {
    @GetMapping("queryByDate")
    Result<List<Order>> queryByDate(String createDate) {
        if (StringUtils.isBlank(createDate)) {
            return Result.error("日期不能为空");
        }

        List<Order> orderList = getOrderList(createDate);
        return Result.success(orderList);
    }

    private List<Order> getOrderList(String myDate) {
        LambdaQueryWrapper<Order> queryWrapper = Wrappers.lambdaQuery(new Order())
                .ge(Order::getCreateTime, myDate)
                .le(Order::getCreateTime, myDate + "23:59:59");
        return myMapper.selectList(queryWrapper);
    }
}

代码示例2:

@RestController
public class DateDemoController {
    /**
     * http://localhost:8080/list?createDate=2024-08-19
     */
    @GetMapping({"/list"})
    public List<Order> list(OrderQuery orderQuery) {
        String string = orderQuery.toString();
        log.info("请求参数:{}", string);

        LambdaQueryWrapper<Order> queryWrapper = Wrappers.lambdaQuery(new Order())
                .ge(orderQuery.getCreateDate() != null, Order::getCreateTime, orderQuery.getCreateDate())
                .lt(orderQuery.getCreateDate() != null, Order::getCreateTime, DateUtils.addDays(orderQuery.getCreateDate(), 1));
        List<Order> orderList = myMapper.selectList(queryWrapper);
        return orderList;
    }
}


@Data
public class OrderQuery {
    @JsonSerialize(using = ToStringSerializer.class) private Long enterpriseId;
    private String enterpriseName;
    private String createDate;

    //** override getter
    public Date getCreateDate() {
        if (StringUtils.isBlank(createDate)) return null;
        try {
            return DateUtils.parseDate(createDate, "yyyy-MM-dd");
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}




编写评论...