news 2026/4/16 11:03:46

Spring Boot 日期范围(查询专用DTO)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 日期范围(查询专用DTO)
以下spring boot 项目代码: package com.weiyu.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Pattern; import lombok.Data; import org.springframework.util.StringUtils; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Optional; /** * 日期范围 */ @Data @SuppressWarnings("unused") public class DateRange { /** * 开始日期 */ //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串 @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串 //@DateTimeFormat(pattern = "yyyy-MM-dd") // 对JSON请求无效,可以移除 @JsonFormat(pattern = "yyyy-MM-dd") // 对JSON请求有效,但用于String类型时作用有限,主要用于日期转换,可以考虑保留或移除 private String beginDate; /** * 结束日期 */ //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串 @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串 //@DateTimeFormat(pattern = "yyyy-MM-dd") // 对JSON请求无效,可以移除 @JsonFormat(pattern = "yyyy-MM-dd") // 对JSON请求有效,但用于String类型时作用有限,主要用于日期转换,可以考虑保留或移除 private String endDate; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 添加缓存字段,避免重复解析 @JsonIgnore private transient LocalDate cachedBeginDate; @JsonIgnore private transient LocalDate cachedEndDate; // ========== 业务方法 ========== /** * 获取解析后的开始日期(计算属性,使用缓存) * 注意:此方法提供更直接的LocalDate访问,减少重复解析 */ @JsonIgnore public Optional<LocalDate> getParsedBeginDate() { if (cachedBeginDate != null) { return Optional.of(cachedBeginDate); } if (!StringUtils.hasText(beginDate)) { return Optional.empty(); } try { cachedBeginDate = LocalDate.parse(beginDate, DATE_FORMATTER); return Optional.of(cachedBeginDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取解析后的结束日期(计算属性,使用缓存) * 注意:此方法提供更直接的LocalDate访问,减少重复解析 */ @JsonIgnore public Optional<LocalDate> getParsedEndDate() { if (cachedEndDate != null) { return Optional.of(cachedEndDate); } if (!StringUtils.hasText(endDate)) { return Optional.empty(); } try { cachedEndDate = LocalDate.parse(endDate, DATE_FORMATTER); return Optional.of(cachedEndDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取开始日期时间(计算属性) */ @JsonIgnore public Optional<LocalDateTime> getBeginDateTime() { return parseDate(beginDate, true); } /** * 获取结束日期时间(计算属性) */ @JsonIgnore public Optional<LocalDateTime> getEndDateTime() { return parseDate(endDate, false); } /** * 验证日期范围是否有效(开始日期 ≤ 结束日期) * 注意:当日期为空字符串时,返回 true */ @JsonIgnore @AssertTrue(message = "开始日期不能晚于结束日期") // @AssertTrue 专门用于跨字段验证(涉及多个字段的业务逻辑验证) public boolean isValid() { Optional<LocalDate> beginOpt = parseLocalDate(beginDate); Optional<LocalDate> endOpt = parseLocalDate(endDate); if (beginOpt.isPresent() && endOpt.isPresent()) { return !beginOpt.get().isAfter(endOpt.get()); } return true; // 允许部分为空 } /** * 是否有有效的日期范围 * 返回:true=有有效日期范围,false=无日期范围或无效 */ @JsonIgnore @SuppressWarnings("BooleanMethodIsAlwaysInverted") // 抑制反转方法警告 public boolean hasValidDateRange() { // 两个都有值且有效 return StringUtils.hasText(beginDate) && StringUtils.hasText(endDate) && isValid(); } /** * 检查是否选择了日期范围(无论是否有效) */ @JsonIgnore public boolean hasDateRange() { return StringUtils.hasText(beginDate) || StringUtils.hasText(endDate); } /** * 检查日期范围是否完整(两个都有值或两个都没有值) */ @JsonIgnore public boolean isComplete() { boolean hasBegin = StringUtils.hasText(beginDate); boolean hasEnd = StringUtils.hasText(endDate); return (hasBegin && hasEnd) || (!hasBegin && !hasEnd); } /** * 检查日期范围是否不完整(只有一个有值) */ @JsonIgnore public boolean isIncomplete() { boolean hasBegin = StringUtils.hasText(beginDate); boolean hasEnd = StringUtils.hasText(endDate); return hasBegin != hasEnd; // 异或:只有一个有值 } /** * 创建新的DateRange */ public static DateRange of(String beginDate, String endDate) { DateRange range = new DateRange(); range.setBeginDate(beginDate); range.setEndDate(endDate); return range; } // ========== 自定义setter以支持缓存清理 ========== /** * 自定义setter,设置beginDate时清除缓存 * 注意:使用@Setter注解时无法直接添加缓存清理逻辑,需要显式定义setter */ public void setBeginDate(String beginDate) { this.beginDate = beginDate; this.cachedBeginDate = null; // 清除缓存 } /** * 自定义setter,设置endDate时清除缓存 * 注意:使用@Setter注解时无法直接添加缓存清理逻辑,需要显式定义setter */ public void setEndDate(String endDate) { this.endDate = endDate; this.cachedEndDate = null; // 清除缓存 } // ========== 私有方法 ========== /** * 安全的日期解析方法 */ private Optional<LocalDateTime> parseDate(String dateStr, boolean isStartDay) { return parseLocalDate(dateStr) .map(date -> isStartDay ? date.atStartOfDay() : date.atTime(LocalTime.MAX)); } /** * 解析LocalDate */ private Optional<LocalDate> parseLocalDate(String dateStr) { if (!StringUtils.hasText(dateStr)) { return Optional.empty(); } try { // 优先使用缓存 if (beginDate != null && beginDate.equals(dateStr) && cachedBeginDate != null) { return Optional.of(cachedBeginDate); } if (endDate != null && endDate.equals(dateStr) && cachedEndDate != null) { return Optional.of(cachedEndDate); } LocalDate parsed = LocalDate.parse(dateStr, DATE_FORMATTER); // 更新缓存 if (beginDate != null && beginDate.equals(dateStr)) { cachedBeginDate = parsed; } else if (endDate != null && endDate.equals(dateStr)) { cachedEndDate = parsed; } return Optional.of(parsed); } catch (Exception e) { return Optional.empty(); } } // ========== Builder模式支持 ========== @JsonIgnore public DateRangeBuilder toBuilder() { return new DateRangeBuilder() .beginDate(beginDate) .endDate(endDate); } public static DateRangeBuilder builder() { return new DateRangeBuilder(); } public static class DateRangeBuilder { private String beginDate; private String endDate; public DateRangeBuilder beginDate(String beginDate) { this.beginDate = beginDate; return this; } public DateRangeBuilder endDate(String endDate) { this.endDate = endDate; return this; } public DateRange build() { return DateRange.of(beginDate, endDate); } } } 请基础于缓存思路,还可以优化getBeginDateTime和getEndDateTime吗?

基于缓存思路,我可以进一步优化getBeginDateTimegetEndDateTime方法,以下是优化后的代码:

java

package com.weiyu.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Pattern; import lombok.Data; import org.springframework.util.StringUtils; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Optional; /** * 日期范围 */ @Data @SuppressWarnings("unused") public class DateRange { /** * 开始日期 */ @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串 @JsonFormat(pattern = "yyyy-MM-dd") private String beginDate; /** * 结束日期 */ @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串 @JsonFormat(pattern = "yyyy-MM-dd") private String endDate; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 添加缓存字段,避免重复解析 @JsonIgnore private transient LocalDate cachedBeginDate; @JsonIgnore private transient LocalDate cachedEndDate; // 新增:缓存日期时间对象 @JsonIgnore private transient LocalDateTime cachedBeginDateTime; @JsonIgnore private transient LocalDateTime cachedEndDateTime; // ========== 业务方法 ========== /** * 获取解析后的开始日期(计算属性,使用缓存) */ @JsonIgnore public Optional<LocalDate> getParsedBeginDate() { if (cachedBeginDate != null) { return Optional.of(cachedBeginDate); } if (!StringUtils.hasText(beginDate)) { return Optional.empty(); } try { cachedBeginDate = LocalDate.parse(beginDate, DATE_FORMATTER); return Optional.of(cachedBeginDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取解析后的结束日期(计算属性,使用缓存) */ @JsonIgnore public Optional<LocalDate> getParsedEndDate() { if (cachedEndDate != null) { return Optional.of(cachedEndDate); } if (!StringUtils.hasText(endDate)) { return Optional.empty(); } try { cachedEndDate = LocalDate.parse(endDate, DATE_FORMATTER); return Optional.of(cachedEndDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取开始日期时间(计算属性,使用缓存优化) */ @JsonIgnore public Optional<LocalDateTime> getBeginDateTime() { // 优先返回缓存 if (cachedBeginDateTime != null) { return Optional.of(cachedBeginDateTime); } Optional<LocalDate> beginDateOpt = getParsedBeginDate(); if (!beginDateOpt.isPresent()) { return Optional.empty(); } // 计算并缓存 cachedBeginDateTime = beginDateOpt.get().atStartOfDay(); return Optional.of(cachedBeginDateTime); } /** * 获取结束日期时间(计算属性,使用缓存优化) */ @JsonIgnore public Optional<LocalDateTime> getEndDateTime() { // 优先返回缓存 if (cachedEndDateTime != null) { return Optional.of(cachedEndDateTime); } Optional<LocalDate> endDateOpt = getParsedEndDate(); if (!endDateOpt.isPresent()) { return Optional.empty(); } // 计算并缓存 cachedEndDateTime = endDateOpt.get().atTime(LocalTime.MAX); return Optional.of(cachedEndDateTime); } /** * 获取结束日期时间(精确到23:59:59,适用于数据库查询) */ @JsonIgnore public Optional<LocalDateTime> getEndDateTimeForQuery() { // 可以使用相同的缓存逻辑 return getEndDateTime(); } /** * 获取结束日期时间(精确到当天结束的纳秒,更精确的版本) */ @JsonIgnore public Optional<LocalDateTime> getEndDateTimeWithNano() { if (cachedEndDateTime != null) { return Optional.of(cachedEndDateTime); } Optional<LocalDate> endDateOpt = getParsedEndDate(); if (!endDateOpt.isPresent()) { return Optional.empty(); } // 精确到当天的最后时刻(23:59:59.999999999) cachedEndDateTime = endDateOpt.get().atTime(LocalTime.MAX); return Optional.of(cachedEndDateTime); } /** * 验证日期范围是否有效(开始日期 ≤ 结束日期) */ @JsonIgnore @AssertTrue(message = "开始日期不能晚于结束日期") public boolean isValid() { Optional<LocalDate> beginOpt = getParsedBeginDate(); // 使用缓存方法 Optional<LocalDate> endOpt = getParsedEndDate(); // 使用缓存方法 if (beginOpt.isPresent() && endOpt.isPresent()) { return !beginOpt.get().isAfter(endOpt.get()); } return true; // 允许部分为空 } /** * 是否有有效的日期范围 */ @JsonIgnore @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean hasValidDateRange() { return StringUtils.hasText(beginDate) && StringUtils.hasText(endDate) && isValid(); } /** * 检查是否选择了日期范围(无论是否有效) */ @JsonIgnore public boolean hasDateRange() { return StringUtils.hasText(beginDate) || StringUtils.hasText(endDate); } /** * 检查日期范围是否完整(两个都有值或两个都没有值) */ @JsonIgnore public boolean isComplete() { boolean hasBegin = StringUtils.hasText(beginDate); boolean hasEnd = StringUtils.hasText(endDate); return (hasBegin && hasEnd) || (!hasBegin && !hasEnd); } /** * 检查日期范围是否不完整(只有一个有值) */ @JsonIgnore public boolean isIncomplete() { boolean hasBegin = StringUtils.hasText(beginDate); boolean hasEnd = StringUtils.hasText(endDate); return hasBegin != hasEnd; // 异或:只有一个有值 } /** * 创建新的DateRange */ public static DateRange of(String beginDate, String endDate) { DateRange range = new DateRange(); range.setBeginDate(beginDate); range.setEndDate(endDate); return range; } // ========== 自定义setter以支持缓存清理 ========== /** * 自定义setter,设置beginDate时清除相关缓存 */ public void setBeginDate(String beginDate) { this.beginDate = beginDate; this.cachedBeginDate = null; // 清除日期缓存 this.cachedBeginDateTime = null; // 清除日期时间缓存 } /** * 自定义setter,设置endDate时清除相关缓存 */ public void setEndDate(String endDate) { this.endDate = endDate; this.cachedEndDate = null; // 清除日期缓存 this.cachedEndDateTime = null; // 清除日期时间缓存 } // ========== 新增:缓存管理方法 ========== /** * 清除所有缓存 */ @JsonIgnore public void clearCache() { this.cachedBeginDate = null; this.cachedEndDate = null; this.cachedBeginDateTime = null; this.cachedEndDateTime = null; } /** * 预加载所有缓存(适用于频繁访问的场景) */ @JsonIgnore public void preloadCache() { getBeginDateTime(); // 这会触发缓存加载 getEndDateTime(); // 这会触发缓存加载 } /** * 检查缓存是否已加载 */ @JsonIgnore public boolean isCacheLoaded() { return cachedBeginDateTime != null && cachedEndDateTime != null; } // ========== Builder模式支持 ========== @JsonIgnore public DateRangeBuilder toBuilder() { return new DateRangeBuilder() .beginDate(beginDate) .endDate(endDate); } public static DateRangeBuilder builder() { return new DateRangeBuilder(); } public static class DateRangeBuilder { private String beginDate; private String endDate; public DateRangeBuilder beginDate(String beginDate) { this.beginDate = beginDate; return this; } public DateRangeBuilder endDate(String endDate) { this.endDate = endDate; return this; } public DateRange build() { return DateRange.of(beginDate, endDate); } } }

优化说明:

1.新增缓存字段

java

@JsonIgnore private transient LocalDateTime cachedBeginDateTime; @JsonIgnore private transient LocalDateTime cachedEndDateTime;

添加了两个新的缓存字段,专门用于存储LocalDateTime对象,避免每次调用都要重新创建。

2.优化getBeginDateTime()getEndDateTime()方法

  • 缓存优先:首先检查缓存是否存在,如果存在直接返回

  • 复用已解析的日期:使用getParsedBeginDate()getParsedEndDate()方法,这些方法已经有缓存机制

  • 懒加载:只在第一次访问时计算并缓存结果

3.增强的setter方法

更新setter方法,同时清除日期和日期时间的缓存:

java

public void setBeginDate(String beginDate) { this.beginDate = beginDate; this.cachedBeginDate = null; // 清除日期缓存 this.cachedBeginDateTime = null; // 清除日期时间缓存 }

4.新增缓存管理方法

  • clearCache():清除所有缓存

  • preloadCache():预加载所有缓存(适用于频繁访问的场景)

  • isCacheLoaded():检查缓存是否已加载

5.性能优势

  1. 减少重复计算:避免每次调用都重新解析字符串和创建LocalDateTime对象

  2. 内存效率:使用transient确保缓存字段不会被序列化

  3. 线程安全:由于DateRange对象通常用于单个请求,不需要考虑线程安全问题

  4. 一致性:缓存保证了多次调用返回相同的对象

6.使用建议

java

// 示例:高效使用优化后的DateRange DateRange range = new DateRange(); range.setBeginDate("2024-01-01"); range.setEndDate("2024-12-31"); // 第一次调用会计算并缓存 Optional<LocalDateTime> beginTime = range.getBeginDateTime(); Optional<LocalDateTime> endTime = range.getEndDateTime(); // 后续调用直接使用缓存 beginTime = range.getBeginDateTime(); // 从缓存返回 endTime = range.getEndDateTime(); // 从缓存返回 // 如果需要重置 range.setBeginDate("2024-02-01"); // 自动清除相关缓存

这些优化特别适用于:

  • 在同一个请求中多次访问日期时间

  • 复杂的业务逻辑需要多次使用开始/结束时间

  • 性能敏感的场景


@PostMapping("/page") public Result<PageResult<CapitalInfoVO>> queryPage(@RequestBody @Valid CapitalInfoQueryDTO queryDTO) { String endpoint = "/capital/info/page"; String method = "queryPage"; log.info("【资金信息】查询资金信息分页,{},{},queryDTO = {}", endpoint, method, SecurityUtils.safeForLog(queryDTO)); 为什么会这样输出:【资金信息】查询资金信息分页,/capital/info/page,queryPage,queryDTO = CapitalInfoQueryDTO(super=BasePageQuery(current=1, size=20), capitalNo=11, capitalName=, capitalType=, capitalIndexType=, capitalAccount=, capitalSource=, capitalIndexSource=, capitalYear=null, capitalState=null, remark=, createDateRange=DateRange(beginDate=2026-02-01, endDate=2026-02-10, cachedBeginDate=2026-02-01, cachedEndDate=2026-02-10, cachedBeginDateTime=null, cachedEndDateTime=null)) 最终结果是不想输出缓存的内容

@Data注解会生成toString()方法,默认包含所有字段,包括缓存字段。有几种方式可以避免在日志中输出缓存内容:

解决方案1:自定义toString()方法(推荐)

DateRange类中添加自定义的toString()方法:

java

@Override public String toString() { return "DateRange(beginDate=" + this.beginDate + ", endDate=" + this.endDate + ")"; }

这样会覆盖 Lombok 生成的toString()方法,只输出需要的字段。

解决方案2:使用@ToString注解排除字段

替换@Data注解为组合注解,使用@ToString排除缓存字段:

java

// 替换 @Data 为以下注解组合 @Getter @Setter @EqualsAndHashCode @ToString(onlyExplicitlyIncluded = true) public class DateRange { @ToString.Include private String beginDate; @ToString.Include private String endDate; // 缓存字段不添加 @ToString.Include 注解 @JsonIgnore private transient LocalDate cachedBeginDate; @JsonIgnore private transient LocalDate cachedEndDate; @JsonIgnore private transient LocalDateTime cachedBeginDateTime; @JsonIgnore private transient LocalDateTime cachedEndDateTime; // ... 其他代码保持不变 }

解决方案3:在@Data基础上添加@ToString(exclude)

java

@Data @ToString(exclude = {"cachedBeginDate", "cachedEndDate", "cachedBeginDateTime", "cachedEndDateTime"}) public class DateRange { // ... 其他代码保持不变 }

解决方案4:在日志工具方法中过滤

修改SecurityUtils.safeForLog方法,排除缓存字段:

java

public class SecurityUtils { public static String safeForLog(Object obj) { if (obj == null) { return "null"; } // 如果是 DateRange 对象,输出自定义格式 if (obj instanceof DateRange) { DateRange range = (DateRange) obj; return String.format("DateRange(beginDate=%s, endDate=%s)", range.getBeginDate(), range.getEndDate()); } // 其他对象使用默认 toString return obj.toString(); } }

解决方案5:完整优化后的DateRange类(推荐方案1+3)

java

package com.weiyu.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.ToString; import org.springframework.util.StringUtils; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Optional; /** * 日期范围 */ @Data @ToString(exclude = {"cachedBeginDate", "cachedEndDate", "cachedBeginDateTime", "cachedEndDateTime"}) @SuppressWarnings("unused") public class DateRange { /** * 开始日期 */ @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private String beginDate; /** * 结束日期 */ @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private String endDate; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 添加缓存字段,避免重复解析 @JsonIgnore private transient LocalDate cachedBeginDate; @JsonIgnore private transient LocalDate cachedEndDate; @JsonIgnore private transient LocalDateTime cachedBeginDateTime; @JsonIgnore private transient LocalDateTime cachedEndDateTime; // 如果还需要进一步简化 toString 输出,可以添加自定义 toString 方法 /* @Override public String toString() { return String.format("DateRange(beginDate=%s, endDate=%s)", beginDate, endDate); } */ // ========== 业务方法 ========== /** * 获取解析后的开始日期(计算属性,使用缓存) */ @JsonIgnore public Optional<LocalDate> getParsedBeginDate() { if (cachedBeginDate != null) { return Optional.of(cachedBeginDate); } if (!StringUtils.hasText(beginDate)) { return Optional.empty(); } try { cachedBeginDate = LocalDate.parse(beginDate, DATE_FORMATTER); return Optional.of(cachedBeginDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取解析后的结束日期(计算属性,使用缓存) */ @JsonIgnore public Optional<LocalDate> getParsedEndDate() { if (cachedEndDate != null) { return Optional.of(cachedEndDate); } if (!StringUtils.hasText(endDate)) { return Optional.empty(); } try { cachedEndDate = LocalDate.parse(endDate, DATE_FORMATTER); return Optional.of(cachedEndDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取开始日期时间(计算属性,使用缓存优化) */ @JsonIgnore public Optional<LocalDateTime> getBeginDateTime() { if (cachedBeginDateTime != null) { return Optional.of(cachedBeginDateTime); } Optional<LocalDate> beginDateOpt = getParsedBeginDate(); if (!beginDateOpt.isPresent()) { return Optional.empty(); } cachedBeginDateTime = beginDateOpt.get().atStartOfDay(); return Optional.of(cachedBeginDateTime); } /** * 获取结束日期时间(计算属性,使用缓存优化) */ @JsonIgnore public Optional<LocalDateTime> getEndDateTime() { if (cachedEndDateTime != null) { return Optional.of(cachedEndDateTime); } Optional<LocalDate> endDateOpt = getParsedEndDate(); if (!endDateOpt.isPresent()) { return Optional.empty(); } cachedEndDateTime = endDateOpt.get().atTime(LocalTime.MAX); return Optional.of(cachedEndDateTime); } // ... 其他方法保持不变 }

推荐方案

方案1(自定义toString())是最推荐的方式,因为:

  1. 最可控:完全控制输出格式

  2. 最简洁:只输出必要信息,避免日志过大

  3. 最安全:不会意外泄露内部状态(缓存状态属于内部实现细节)

使用自定义toString()方法后,日志输出会变为:

text

DateRange(beginDate=2026-02-01, endDate=2026-02-10)

这样既简洁又安全,符合日志记录的最佳实践。


最终优化代码:

package com.weiyu.model; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.ToString; import org.springframework.util.StringUtils; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Optional; /** * 日期范围(查询专用DTO) */ @Data @ToString(exclude = {"cachedBeginDate", "cachedEndDate", "cachedBeginDateTime", "cachedEndDateTime"}) // toString时排除缓存字段 @SuppressWarnings("unused") public class DateRange { /** * 开始日期 */ //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用分支(|) //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用分支(|) //@Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用可选组(?),现代正则最佳实践 @Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})?$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用可选组(?),现代正则最佳实践 //@DateTimeFormat(pattern = "yyyy-MM-dd") // 格式化入参,前端→后端(接收参数),处理表单提交、URL参数、请求参数中的日期时间字符串转换,对JSON请求无效,可以移除 //@JsonFormat(pattern = "yyyy-MM-dd") // 格式化出参,后端→前端(返回数据),处理 JSON 数据与 Java 对象之间的转换,但用于String类型时作用有限,可以移除 private String beginDate; /** * 结束日期 */ //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用分支(|) //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用分支(|) //@Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用可选组(?),现代正则最佳实践 @Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})?$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用可选组(?),现代正则最佳实践 //@DateTimeFormat(pattern = "yyyy-MM-dd") // 格式化入参,前端→后端(接收参数),处理表单提交、URL参数、请求参数中的日期时间字符串转换,对JSON请求无效,可以移除 //@JsonFormat(pattern = "yyyy-MM-dd") // 格式化出参,后端→前端(返回数据),处理 JSON 数据与 Java 对象之间的转换,但用于String类型时作用有限,可以移除 private String endDate; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 缓存字段,避免重复解析 @JsonIgnore private transient LocalDate cachedBeginDate; @JsonIgnore private transient LocalDate cachedEndDate; @JsonIgnore private transient LocalDateTime cachedBeginDateTime; @JsonIgnore private transient LocalDateTime cachedEndDateTime; // ========== 业务方法 ========== /** * 获取解析后的开始日期(计算属性,使用缓存) * 注意:此方法提供更直接的LocalDate访问,减少重复解析 */ @JsonIgnore public Optional<LocalDate> getParsedBeginDate() { // 优先返回缓存 if (cachedBeginDate != null) { return Optional.of(cachedBeginDate); } if (!StringUtils.hasText(beginDate)) { return Optional.empty(); } try { // 计算并缓存 cachedBeginDate = LocalDate.parse(beginDate, DATE_FORMATTER); return Optional.of(cachedBeginDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取解析后的结束日期(计算属性,使用缓存) * 注意:此方法提供更直接的LocalDate访问,减少重复解析 */ @JsonIgnore public Optional<LocalDate> getParsedEndDate() { // 优先返回缓存 if (cachedEndDate != null) { return Optional.of(cachedEndDate); } if (!StringUtils.hasText(endDate)) { return Optional.empty(); } try { // 计算并缓存 cachedEndDate = LocalDate.parse(endDate, DATE_FORMATTER); return Optional.of(cachedEndDate); } catch (Exception e) { return Optional.empty(); } } /** * 获取开始日期时间(计算属性,使用缓存) * 注意:此方法提供更直接的LocalDateTime访问,减少重复解析 */ @JsonIgnore public Optional<LocalDateTime> getBeginDateTime() { // 优先返回缓存 if (cachedBeginDateTime != null) { return Optional.of(cachedBeginDateTime); } Optional<LocalDate> beginDateOpt = getParsedBeginDate(); if (beginDateOpt.isEmpty()) { return Optional.empty(); } // 计算并缓存 cachedBeginDateTime = beginDateOpt.get().atStartOfDay(); return Optional.of(cachedBeginDateTime); } /** * 获取结束日期时间(计算属性,使用缓存) * 注意:此方法提供更直接的LocalDateTime访问,减少重复解析 */ @JsonIgnore public Optional<LocalDateTime> getEndDateTime() { // 优先返回缓存 if (cachedEndDateTime != null) { return Optional.of(cachedEndDateTime); } Optional<LocalDate> endDateOpt = getParsedEndDate(); if (endDateOpt.isEmpty()) { return Optional.empty(); } // 计算并缓存 cachedEndDateTime = endDateOpt.get().atTime(LocalTime.MAX); return Optional.of(cachedEndDateTime); } /** * 验证日期范围是否有效(开始日期 ≤ 结束日期) * 注意:当日期为空字符串时,返回 true */ @JsonIgnore @AssertTrue(message = "开始日期不能晚于结束日期") // @AssertTrue 专门用于跨字段验证(涉及多个字段的业务逻辑验证) public boolean isValid() { Optional<LocalDate> beginOpt = getParsedBeginDate(); // 使用缓存方法 Optional<LocalDate> endOpt = getParsedEndDate(); // 使用缓存方法 if (beginOpt.isPresent() && endOpt.isPresent()) { return !beginOpt.get().isAfter(endOpt.get()); } return true; // 允许部分为空 } /** * 是否有有效的日期范围 * 返回:true=有有效日期范围,false=无日期范围或无效 */ @JsonIgnore @SuppressWarnings("BooleanMethodIsAlwaysInverted") // 抑制反转方法警告 public boolean hasValidDateRange() { // 两个都有值且有效 return StringUtils.hasText(beginDate) && StringUtils.hasText(endDate) && isValid(); } /** * 检查是否选择了日期范围(无论是否有效) */ @JsonIgnore public boolean hasDateRange() { return StringUtils.hasText(beginDate) || StringUtils.hasText(endDate); } /** * 检查日期范围是否完整(两个都有值或两个都没有值) */ @JsonIgnore public boolean isComplete() { boolean hasBegin = StringUtils.hasText(beginDate); boolean hasEnd = StringUtils.hasText(endDate); return (hasBegin && hasEnd) || (!hasBegin && !hasEnd); } /** * 检查日期范围是否不完整(只有一个有值) */ @JsonIgnore public boolean isIncomplete() { boolean hasBegin = StringUtils.hasText(beginDate); boolean hasEnd = StringUtils.hasText(endDate); return hasBegin != hasEnd; // 异或:只有一个有值 } /** * 创建新的DateRange */ public static DateRange of(String beginDate, String endDate) { DateRange range = new DateRange(); range.setBeginDate(beginDate); range.setEndDate(endDate); return range; } // ========== 自定义setter以支持缓存清理 ========== /** * 自定义setter,设置beginDate时清除缓存 * 注意:使用@Setter注解时无法直接添加缓存清理逻辑,需要显式定义setter */ public void setBeginDate(String beginDate) { this.beginDate = beginDate; this.cachedBeginDate = null; // 清除日期缓存 this.cachedBeginDateTime = null; // 清除日期时间缓存 } /** * 自定义setter,设置endDate时清除缓存 * 注意:使用@Setter注解时无法直接添加缓存清理逻辑,需要显式定义setter */ public void setEndDate(String endDate) { this.endDate = endDate; this.cachedEndDate = null; // 清除日期缓存 this.cachedEndDateTime = null; // 清除日期时间缓存 } /** * 清除所有缓存 */ @JsonIgnore public void clearCache() { this.cachedBeginDate = null; this.cachedEndDate = null; this.cachedBeginDateTime = null; this.cachedEndDateTime = null; } /** * 预加载所有缓存(适用于频繁访问的场景) */ @JsonIgnore public void preloadCache() { getBeginDateTime(); // 这会触发缓存加载 getEndDateTime(); // 这会触发缓存加载 } /** * 检查缓存是否已加载 */ @JsonIgnore public boolean isCacheLoaded() { return cachedBeginDateTime != null && cachedEndDateTime != null; } // ========== Builder模式支持 ========== @JsonIgnore public DateRangeBuilder toBuilder() { return new DateRangeBuilder() .beginDate(beginDate) .endDate(endDate); } public static DateRangeBuilder builder() { return new DateRangeBuilder(); } public static class DateRangeBuilder { private String beginDate; private String endDate; public DateRangeBuilder beginDate(String beginDate) { this.beginDate = beginDate; return this; } public DateRangeBuilder endDate(String endDate) { this.endDate = endDate; return this; } public DateRange build() { return DateRange.of(beginDate, endDate); } } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/12 18:56:01

惊艳!Qwen3-TTS生成10国语言语音效果实测

惊艳&#xff01;Qwen3-TTS生成10国语言语音效果实测 获取更多AI镜像 想探索更多AI镜像和应用场景&#xff1f;访问 CSDN星图镜像广场&#xff0c;提供丰富的预置镜像&#xff0c;覆盖大模型推理、图像生成、视频生成、模型微调等多个领域&#xff0c;支持一键部署。 1. 前言&a…

作者头像 李华
网站建设 2026/4/13 15:47:28

保姆级LongCat-Image-Edit指南:手把手教你图片魔法编辑

保姆级LongCat-Image-Edit指南&#xff1a;手把手教你图片魔法编辑 1. 这不是修图软件&#xff0c;是你的AI图像魔法师 你有没有试过——想把一张普通宠物照变成赛博朋克风格的机械猫&#xff0c;或者让家里的橘猫瞬间穿上宇航服漂浮在火星表面&#xff1f;传统修图工具要调图…

作者头像 李华
网站建设 2026/4/13 9:21:59

摄影工作室降本提效:cv_unet_image-colorization批量黑白图上色方案

摄影工作室降本提效&#xff1a;cv_unet_image-colorization批量黑白图上色方案 1. 项目简介 在摄影工作室的日常工作中&#xff0c;经常会遇到需要处理黑白照片的场景。无论是修复老照片&#xff0c;还是为艺术创作中的黑白作品上色&#xff0c;传统的手工上色方式既耗时又成…

作者头像 李华
网站建设 2026/4/10 17:50:42

DAMO-YOLO实战技巧:如何优化工业检测性能

DAMO-YOLO实战技巧&#xff1a;如何优化工业检测性能 1. 工业场景下的真实挑战&#xff1a;为什么标准YOLO不够用 在工厂产线、仓储分拣、电力巡检等工业视觉任务中&#xff0c;我们常遇到这样一组矛盾组合&#xff1a;既要识别毫米级螺丝缺陷&#xff0c;又要实时处理4K分辨…

作者头像 李华
网站建设 2026/4/16 11:02:48

一款开源、强大、简单易用的 .NET 假数据生成利器

前言 在日常的 .NET 项目开发中&#xff0c;我们经常需要大量的测试数据来验证业务逻辑、填充数据库、进行性能测试或者做产品演示。手动编写这些测试数据不仅枯燥乏味、浪费时间&#xff0c;而且难以覆盖各种边界场景。今天大姚给大家推荐一款开源、强大、简单易用的 .NET 假…

作者头像 李华