Spring Boot查询条件判断错误原因分析
前端传来的Json数据:
{capitalNo: "", capitalName: "AAA", capitalType: "", capitalIndexType: "", capitalAccount: "",…}
capitalAccount
:
""
capitalIndexSource
:
""
capitalIndexType
:
""
capitalName
:
"AAA"
capitalNo
:
""
capitalSource
:
""
capitalStates
:
[]
capitalType
:
""
createDateRange
:
{beginDate: "", endDate: ""}
current
:
1
remark
:
""
size
:
20
后端spring boot 处理:
/**
* 获取资金信息分页
*
* @param queryDTO 查询对象
* @return {@link Result}<{@link PageResult}<{@link CapitalInfoVO}>>
*/
@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));
// 1、基本校验已由 @Valid 完成,Spring会自动执行以下验证:
// a. 检查@Pattern注解(格式验证)
// b. 调用@AssertTrue注解的方法(业务逻辑验证)
// 处理校验结果,由全局异常 【处理方法参数验证异常(MethodArgumentNotValidException)】 处理校验结果
// 2、处理业务逻辑校验,检查空条件
if (isAllQueryParamsEmpty(queryDTO)) {
log.warn("【资金信息】 至少需要一个查询条件,{},{}", endpoint, method);
return Result.error("请至少输入一个查询条件");
}
// 3、处理业务逻辑校验 - 日期范围
String dateRangeError = validateDateRange(queryDTO.getCreateDateRange());
if (dateRangeError != null) {
return Result.error(dateRangeError);
}
try {
PageResult<CapitalInfoVO> capitalInfoPage = capitalInfoService.queryPage(queryDTO);
return Result.success(capitalInfoPage);
} catch (Exception e) {
log.error("【资金信息】查询资金信息分页失败,{},{}", endpoint, method, e);
return Result.error("查询资金信息分页失败");
}
}
/**
* 检查所有查询条件是否都为空
*/
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
&& (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
&& (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
&& (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
&& (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
&& (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
&& (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
&& queryDTO.getCapitalYear() == null
&& queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty()
&& (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
&& queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()
&& queryDTO.getCreateDateRange().getEndDateTime().isEmpty();
}
为什么前端接收到的响应结果为:{"code":1,"message":"请至少输入一个查询条件","data":null}
前端接收到的响应 {"code":1,"message":"请至少输入一个查询条件","data":null} 是因为后端在 isAllQueryParamsEmpty 方法中错误地判断所有查询条件都为空,尽管前端传入了 capitalName 为 "AAA"。问题出在该方法的逻辑中,由于运算符优先级处理不当,导致实际判断结果与预期不符。
具体原因如下:
-
方法中使用了一系列
&&和||组合条件,但 Java 中&&优先级高于||,因此表达式被解析为:text
(前面所有 && 条件) || (capitalStates.isEmpty() && 剩余条件)
-
前面的
&&条件中包含了capitalName的非空判断((capitalName == null || capitalName.trim().isEmpty())),由于capitalName为 "AAA",该条件为false,导致前半部分整体为false。 -
后半部分中,
capitalStates为空数组,capitalStates.isEmpty()为true,remark为空字符串,日期范围也为空,因此后半部分整体为true。 -
最终整个表达式
false || true结果为true,导致方法返回true,触发“请至少输入一个查询条件”的错误。
解决方案:调整 isAllQueryParamsEmpty 方法中的逻辑,为 capitalStates 的判断添加明确的括号,确保所有条件正确组合。例如:
java
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
这段代码还需优化吗?
/**
* 检查所有查询条件是否都为空
*/
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
&& (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
&& (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
&& (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
&& (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
&& (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
&& (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
&& queryDTO.getCapitalYear() == null
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
&& (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
&& queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()
&& queryDTO.getCreateDateRange().getEndDateTime().isEmpty();
}
你的代码已经解决了之前运算符优先级的问题,整体逻辑清晰。不过,从健壮性、可读性和维护性角度,仍有几个可以优化的地方:
1. 潜在的空指针风险
-
问题:
queryDTO.getCreateDateRange()可能返回null,直接调用getBeginDateTime().isEmpty()会抛出NullPointerException。同样,beginDateTime和endDateTime也可能为null。 -
改进:先判空,再调用方法。
2. 代码冗长,可读性一般
-
大量重复的
(field == null || field.trim().isEmpty())模式,使方法难以阅读和维护。 -
改进:提取工具方法,如
isBlank(String str),或直接使用 Apache Commons Lang 的StringUtils.isBlank()。
3. 字段变更时的维护成本
-
如果以后增加或删除查询字段,需要手动修改此方法,容易遗漏。
-
改进:考虑使用反射或注解标记查询字段,但需权衡复杂度,通常简单项目直接维护即可。
4. 日期范围的空判断可能不够准确
-
如果
createDateRange对象不为空,但两个日期字符串都是空,业务上视为无日期条件,当前逻辑正确。 -
但需确保
beginDateTime和endDateTime的isEmpty()是安全的(已处理 null)。
优化建议示例
java
import org.apache.commons.lang3.StringUtils; // 或自己写工具类
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return isBlank(queryDTO.getCapitalNo())
&& isBlank(queryDTO.getCapitalName())
&& isBlank(queryDTO.getCapitalType())
&& isBlank(queryDTO.getCapitalIndexType())
&& isBlank(queryDTO.getCapitalAccount())
&& isBlank(queryDTO.getCapitalSource())
&& isBlank(queryDTO.getCapitalIndexSource())
&& queryDTO.getCapitalYear() == null
&& isCollectionEmpty(queryDTO.getCapitalStates())
&& isBlank(queryDTO.getRemark())
&& isDateRangeEmpty(queryDTO.getCreateDateRange());
}
private boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
private boolean isCollectionEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
private boolean isDateRangeEmpty(DateRange range) {
return range == null
|| (isBlank(range.getBeginDateTime()) && isBlank(range.getEndDateTime()));
}
如果不想引入外部库,自己写静态工具类:
java
public final class QueryUtils {
public static boolean isBlank(String str) { ... }
public static boolean isCollectionEmpty(Collection<?> c) { ... }
}
这样改造后:
-
健壮性提升:避免 NPE
-
可读性增强:条件语句一目了然
-
复用性高:工具方法可用于其他校验
是否需要更复杂的优化?
如果你的项目中有大量类似的 DTO 和全空判断,可以考虑基于注解(如 @QueryCondition)配合反射自动检查,但会增加复杂度。对于当前场景,上述小优化已足够。
这样改进后,是否可以了?
/**
* 检查所有查询条件是否都为空
*/
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
&& (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
&& (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
&& (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
&& (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
&& (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
&& (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
&& queryDTO.getCapitalYear() == null
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
&& (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());
}
你的改进已经很好地解决了空指针问题,逻辑也是正确的。现在当 createDateRange 为 null 时,两个日期条件会自动视为空,符合预期;当 createDateRange 不为 null 时,会检查两个日期字符串是否为空,也与原意图一致。
不过,如果 getBeginDateTime() 或 getEndDateTime() 可能返回 null(而不是空字符串),那么调用 isEmpty() 仍然会触发空指针异常。虽然大多数场景下日期字段会初始化为空字符串,但为了绝对健壮,可以进一步补充判空:
java
&& (queryDTO.getCreateDateRange() == null
|| (queryDTO.getCreateDateRange().getBeginDateTime() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()))
&& (queryDTO.getCreateDateRange() == null
|| (queryDTO.getCreateDateRange().getEndDateTime() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty()))
这样就能全面防护 null 值。当然,如果确定日期字段永远不会返回 null,当前的代码已经足够。
另外,如果你希望代码更简洁、可维护,可以考虑抽取工具方法(如 isBlank)来避免重复的判空逻辑,但这属于锦上添花的优化,不影响正确性。
总的来说,改进后的代码是可以的,已经满足当前需求。如果你对空指针有更高要求,再补充一层判空即可。
这样改后:
/**
* 检查所有查询条件是否都为空
*/
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
&& (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
&& (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
&& (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
&& (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
&& (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
&& (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
&& queryDTO.getCapitalYear() == null
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
&& (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
&& (queryDTO.getCreateDateRange() == null
|| (queryDTO.getCreateDateRange().getBeginDateTime() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()))
&& (queryDTO.getCreateDateRange() == null
|| (queryDTO.getCreateDateRange().getEndDateTime() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty()));
}
为什么idea会提示queryDTO.getCreateDateRange().getBeginDateTime() == null和queryDTO.getCreateDateRange().getEndDateTime() == null替换为isEmpty
IDEA 提示 queryDTO.getCreateDateRange().getBeginDateTime() == null 可替换为 isEmpty(),通常是因为 IDEA 认为 getBeginDateTime() 返回的对象(假设是 String)不可能为 null,或者它检测到你在同一个表达式中已经确保了调用对象非空,但字段本身仍可能为 null。实际上,这种提示可能来自不同的代码检查规则:
-
“可以替换为
isEmpty()”-
如果
getBeginDateTime()返回的是String,且 IDEA 通过某些分析(例如方法注解@NotNull或上下文)认为该方法不会返回null,那么== null的判断就是多余的,可以简化为isEmpty()。 -
但在你的代码中,你同时判断了
== null和isEmpty(),这是为了应对字段可能为null的情况。如果字段确实可能为null,那么== null的判断是必要的,不能直接替换为isEmpty()(因为null.isEmpty()会抛异常)。
-
-
建议使用
StringUtils.isEmpty()-
IDEA 可能检测到你在手动实现判空+判空字符串的组合,并建议使用 Apache Commons Lang 的
StringUtils.isEmpty()或StringUtils.isBlank(),这些工具方法内部已经处理了null和空字符串。 -
如果你已经引入了相关工具类,采纳提示可以让代码更简洁。
-
-
检查配置过于激进
-
某些 IDEA 的检查规则(如“Constant conditions & exceptions”)可能误判,认为
getBeginDateTime()不可能为null(比如从上下文推断它总是由框架注入,不会为null),从而建议简化。
-
建议处理方式
-
如果确定
getBeginDateTime()永远不会返回null(例如,DTO 中的字段在构造时已初始化为空字符串),则可以直接使用isEmpty(),不需要== null判断。 -
如果不确定或可能返回
null,保留双重判断是安全的,但代码略显冗长。可以:-
忽略 IDEA 提示(按
Alt+Enter选择“忽略”或调整检查级别)。 -
使用工具方法简化,例如:
java
import org.apache.commons.lang3.StringUtils; && (queryDTO.getCreateDateRange() == null || StringUtils.isEmpty(queryDTO.getCreateDateRange().getBeginDateTime())) && (queryDTO.getCreateDateRange() == null || StringUtils.isEmpty(queryDTO.getCreateDateRange().getEndDateTime()))这样既简洁又能处理
null。
-
关于你最后的代码
你的代码逻辑正确,已覆盖 createDateRange 为 null 以及日期字段为 null 或空字符串的情况,不会有运行时风险。IDEA 的提示仅属于代码风格建议,不影响功能。如果你希望消除提示,可以按上述方法调整;如果觉得当前写法清晰,也可以保留。
总结:提示的出现是因为 IDEA 认为有更简洁的写法(可能是误判或推荐工具类),你可以根据实际需求决定是否采纳。
结合原有的:
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);
}
}
}
最终优化为:
/**
* 检查所有查询条件是否都为空
*/
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
&& (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
&& (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
&& (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
&& (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
&& (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
&& (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
&& queryDTO.getCapitalYear() == null
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
&& (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());
}
这样是否合理?
你的最终代码已经解决了之前的问题,并且引入了 Optional.isEmpty() 来判断日期时间是否存在,这在 Java 11+ 环境下是可行的。但结合你提供的 DateRange 类设计,这个写法仍有几个值得商榷的地方,可能影响代码的健壮性和可读性。
当前代码分析
java
&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()) && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty())
优点
-
正确处理了
createDateRange为null的情况,避免 NPE。 -
利用
DateRange已有的getBeginDateTime()和getEndDateTime()方法,复用了解析逻辑。
潜在问题
-
Optional.isEmpty()的 Java 版本依赖Optional.isEmpty()是 Java 11 引入的方法。如果项目仍使用 Java 8,代码将无法编译。你需要确认项目的 JDK 版本。 -
将无效日期视为“空条件”可能引发歧义
getBeginDateTime()在日期字符串为空或解析失败(如 "2025-02-30")时返回Optional.empty()。
如果用户输入了格式正确但实际不存在的日期(如 2 月 30 日),@Pattern注解无法捕获这种逻辑错误,导致该无效日期被当成空条件处理,从而绕过“至少一个查询条件”的检查,最终查询时忽略了这个日期条件。用户期望得到错误提示,但系统却静默执行,可能造成困惑。 -
语义不够直观
getBeginDateTime().isEmpty()检查的是“是否存在有效的开始日期时间”,而当前方法的本意是“日期范围的字符串是否为空”。两者在概念上略有差异,阅读代码时需要理解DateRange的内部行为,增加了认知负担。
优化建议
方案一:直接检查日期字符串(推荐)
直接使用 DateRange 的 getBeginDate() 和 getEndDate() 进行判空,与其它字段的检查方式保持一致,逻辑更清晰,且不受解析失败的影响(日期有效性应由校验层处理)。
java
&& (queryDTO.getCreateDateRange() == null
|| ( (queryDTO.getCreateDateRange().getBeginDate() == null || queryDTO.getCreateDateRange().getBeginDate().trim().isEmpty())
&& (queryDTO.getCreateDateRange().getEndDate() == null || queryDTO.getCreateDateRange().getEndDate().trim().isEmpty()) ))
如果项目已引入 Spring,可以复用 StringUtils.hasText 使代码更简洁:
java
import org.springframework.util.StringUtils;
&& (queryDTO.getCreateDateRange() == null
|| (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
&& !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())))
方案二:保持使用 Optional.isEmpty(),但增强日期有效性校验
如果你希望沿用 getBeginDateTime().isEmpty(),建议在 DateRange 中增加对单个日期有效性的校验(例如通过 @AssertTrue 或自定义注解),确保无效日期能被提前拦截,不会混入业务逻辑。但这样会增加校验复杂度,且与当前 DateRange 的设计(解析失败返回 Optional.empty())不完全一致。
总结
当前代码是否合理?
-
在 Java 11+ 且你接受将无效日期视为空条件的前提下,可以工作。
-
但从代码健壮性和可维护性角度,建议改为直接检查日期字符串,理由如上。
最终优化后的代码(使用 StringUtils.hasText)示例:
java
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
&& (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
&& (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
&& (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
&& (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
&& (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
&& (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
&& queryDTO.getCapitalYear() == null
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
&& (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
&& (queryDTO.getCreateDateRange() == null
|| (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
&& !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())));
}
这样既避免了 NPE,又消除了对 Optional.isEmpty() 的依赖和无效日期被静默忽略的风险,同时与其他字段的判空风格保持一致。
最终优化:
/**
* 检查所有查询条件是否都为空
*/
private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
if (queryDTO == null) {
return true;
}
// return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
// && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
// && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
// && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
// && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
// && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
// && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
// && queryDTO.getCapitalYear() == null
// && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
// && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
// && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
// && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());
return StringUtils.hasText(queryDTO.getCapitalNo())
&& StringUtils.hasText(queryDTO.getCapitalName())
&& StringUtils.hasText(queryDTO.getCapitalType())
&& StringUtils.hasText(queryDTO.getCapitalIndexType())
&& StringUtils.hasText(queryDTO.getCapitalAccount())
&& StringUtils.hasText(queryDTO.getCapitalSource())
&& StringUtils.hasText(queryDTO.getCapitalIndexSource())
&& queryDTO.getCapitalYear() == null
&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
&& StringUtils.hasText(queryDTO.getRemark())
&& (queryDTO.getCreateDateRange() == null
|| (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
&& !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())));
}
更多推荐



所有评论(0)