[排坑]Spring Boot @RequestParam的required=true但参数依然为null

本文探讨了在Spring Boot中使用@RequestParam注解时,即使设置required=true,参数仍可能为null的情况。问题源于枚举类型的序列化处理,自定义的序列化在反序列化时返回了null,导致参数验证未生效。总结建议在反序列化时避免返回null,而是抛出异常以确保required=true的有效性。

使用场景:

使用Spring Boot的@RequestParam注解时,我知道required的默认值是true,理论上来讲,被注解标注的参数如果为null,Spring Boot应该抛出异常。但实际上没有。

问题描述:

	
    public RestObjectResponse<xx> xxx(@RequestParam(value = "role")UserRole role) {
        return xxx;
    }

从上面的代码来看,如果role为null,应该会直接抛出异常,因此在内部的代码中我就没有校验role是否为null,但事实上null被传进来了。

原因分析:

作为常用的类型,比如Integer、String等,为null会抛出异常。我们这个是枚举类型,遇到这样的问题,我们先思考下面的三个问题

  • required=true是判断传入的url参数或body参数为空吗?
  • required=true是判断最终方法传入被RequestParam修饰的参数对象为空吗?
  • 是不是对枚举的序列化处理出现了什么问题

先分析枚举序列化

因为我自定义了枚举的序列化,但是在反序列化过程中直接返回的null值。这也应该是造成参数为null的根源。那么通过这个现象我们可以思考,required=true判断的可能仅仅是参数是否为null,当参数不为空,但在反序列化过程中产生的null,是不被检查的。

总结

为了保证required=true 的可信度,在反序列化时,不应该返回null,如果出现不能反序列化的情况,应该抛出异常。

如下代码分页未生效,请优化代码 @ApiOperation(value = "订单多条件查询接口", notes = "订单多条件查询接口") @GetMapping("order_query") public Result<?> orderQuery( @RequestParam @ApiParam(value = "当前页") Integer current, @RequestParam @ApiParam(value = "页大小") Integer size, @RequestParam(required = false) @ApiParam(value = "会员编号") String memberId, @RequestParam(required = false) @ApiParam(value = "会员等级【A,B,C】") String memberLevel, @RequestParam(required = false) @ApiParam(value = "分账是否已完结: 0-未完结, 1-已完结") Boolean profitSharingFinished, @RequestParam(required = false) @ApiParam(value = "分账状态: PENDING-待分账, PROCESSING-分账中, SUCCESS-分账成功, PARTIAL_SUCCESS-部分成功, FAILED-分账失败, CLOSED-已关闭") String profitSharingStatus, @RequestParam(required = false) @ApiParam(value = "微信支付订单号。用户支付成功后,由回调通知传来。未支付时为空。") String transactionId, @RequestParam(required = false) @ApiParam(value = "订单状态: NOTPAY:未支付, SUCCESS:支付成功, CLOSED:已关闭, REFUND:已退款") String status, @RequestParam(required = false) @ApiParam(value = "订单支付成功时间-开始") Date startTime, @RequestParam(required = false) @ApiParam(value = "订单支付成功时间-结束") Date endTime ) { Page<Orders> page = new Page<>(current, size); Page<Orders> ordersPage = ordersService.lambdaQuery() .like(ObjectUtil.isNotEmpty(memberId), Orders::getMemberId, memberId) .like(ObjectUtil.isNotEmpty(memberLevel), Orders::getMemberLevel, memberLevel) .eq(ObjectUtil.isNotEmpty(profitSharingFinished), Orders::getProfitSharingFinished, profitSharingFinished) .eq(ObjectUtil.isNotEmpty(profitSharingStatus), Orders::getProfitSharingStatus, profitSharingStatus) .eq(ObjectUtil.isNotEmpty(transactionId), Orders::getTransactionId, transactionId) .eq(ObjectUtil.isNotEmpty(status), Orders::getStatus, status) .between((ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isEmpty(endTime)), Orders::getPaidAt, startTime, endTime) .orderByDesc(Orders::getCreatedAt) .page(page); return Result.ok(ordersPage); }
最新发布
09-13
<think> 我们注意到用户提供的代码中使用了`ordersService.lambdaQuery()`构建查询,并且最后调用了`.page(page)`方法,但用户反映分页未生效。 可能的原因分析: 1. 分页参数传递问题:确保`current`和`size`参数正确传递。 2. 条件构造问题:注意条件构造时的逻辑,特别是`.between`条件的使用。 3. 分页插件配置:检查是否配置了MyBatis-Plus的分页插件。 观察代码中条件构造部分,尤其是`.between((ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isEmpty(endTime)), ...)`这一行,这里存在逻辑问题: - 条件判断写的是:当startTime不为空且endTime为空时,才执行between?这显然不符合between的条件(需要开始和结束时间都不为空)。 - 另外,如果两个时间都为空,我们希望不加入条件;如果两个时间都不为空,才加入between条件;如果只有一个时间,可能需要单独处理(比如大于等于开始时间,或小于等于结束时间)。 此外,我们注意到在`.orderByDesc(Orders::getCreatedAt)`后面调用了`.page(page)`,这应该是正确的。 解决方案: 1. 修复时间范围条件:将时间范围条件拆分为多个条件判断,确保只有开始时间或结束时间存在时的处理逻辑。 2. 检查MyBatis-Plus分页插件是否配置。 由于用户提供的代码中没有显示分页插件的配置,我们先假设没有配置,所以需要提醒用户配置分页插件。 具体优化步骤: 步骤1:配置MyBatis-Plus分页插件(如果尚未配置) 在Spring Boot配置类中添加: ```java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 根据数据库类型选择 return interceptor; } } ``` 步骤2:修正时间范围查询条件 当前的时间范围条件逻辑有误,我们重构时间条件: ```java .between(ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isNotEmpty(endTime), Orders::getPaidAt, startTime, endTime) .ge(ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isEmpty(endTime), Orders::getPaidAt, startTime) .le(ObjectUtil.isEmpty(startTime) && ObjectUtil.isNotEmpty(endTime), Orders::getPaidAt, endTime) ``` 但是注意:上述写法虽然可以,但条件判断重复且复杂。我们可以采用更清晰的方式,逐个条件判断并添加。 由于lambdaQuery链式调用中条件方法(如between, ge, le)的第一个参数是boolean类型,我们可以这样写: 重构思路: 使用多个条件判断,分别处理: - 当startTime和endTime都存在时,使用between - 当只有startTime存在时,使用ge(大于等于) - 当只有endTime存在时,使用le(小于等于) 但是,MyBatis-Plus的条件构造器支持链式调用,每个条件独立判断,因此可以写成: ```java LambdaQueryChainWrapper<Orders> queryWrapper = ordersService.lambdaQuery() .like(ObjectUtil.isNotEmpty(memberId), Orders::getMemberId, memberId) .like(ObjectUtil.isNotEmpty(memberLevel), Orders::getMemberLevel, memberLevel) .eq(ObjectUtil.isNotEmpty(profitSharingFinished), Orders::getProfitSharingFinished, profitSharingFinished) .eq(ObjectUtil.isNotEmpty(profitSharingStatus), Orders::getProfitSharingStatus, profitSharingStatus) .eq(ObjectUtil.isNotEmpty(transactionId), Orders::getTransactionId, transactionId) .eq(ObjectUtil.isNotEmpty(status), Orders::getStatus, status); // 处理时间范围 if (ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isNotEmpty(endTime)) { queryWrapper.between(Orders::getPaidAt, startTime, endTime); } else if (ObjectUtil.isNotEmpty(startTime)) { queryWrapper.ge(Orders::getPaidAt, startTime); } else if (ObjectUtil.isNotEmpty(endTime)) { queryWrapper.le(Orders::getPaidAt, endTime); } // 序并分页 Page<Orders> ordersPage = queryWrapper.orderByDesc(Orders::getCreatedAt).page(page); ``` 但是,链式调用在每次条件方法调用后都会返回新的wrapper,所以上面的写法在条件分支中赋值回原变量是不必要的,因为每个条件方法都返回当前对象。 我们可以直接在链式调用中写条件,但是时间条件需要拆开写,因为条件判断逻辑相对独立。我们可以这样写: ```java LambdaQueryChainWrapper<Orders> queryWrapper = ordersService.lambdaQuery(); queryWrapper .like(ObjectUtil.isNotEmpty(memberId), Orders::getMemberId, memberId) .like(ObjectUtil.isNotEmpty(memberLevel), Orders::getMemberLevel, memberLevel) .eq(ObjectUtil.isNotEmpty(profitSharingFinished), Orders::getProfitSharingFinished, profitSharingFinished) .eq(ObjectUtil.isNotEmpty(profitSharingStatus), Orders::getProfitSharingStatus, profitSharingStatus) .eq(ObjectUtil.isNotEmpty(transactionId), Orders::getTransactionId, transactionId) .eq(ObjectUtil.isNotEmpty(status), Orders::getStatus, status); if (ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isNotEmpty(endTime)) { queryWrapper.between(Orders::getPaidAt, startTime, endTime); } else if (ObjectUtil.isNotEmpty(startTime)) { queryWrapper.ge(Orders::getPaidAt, startTime); } else if (ObjectUtil.isNotEmpty(endTime)) { queryWrapper.le(Orders::getPaidAt, endTime); } queryWrapper.orderByDesc(Orders::getCreatedAt); Page<Orders> ordersPage = queryWrapper.page(page); ``` 但是注意:LambdaQueryChainWrapper每次条件方法都是返回自身,所以可以直接链式调用,也可以分开写。 步骤3:注意分页参数 我们创建Page对象时使用的是`new Page<>(current, size)`,这里的current是当前页,从第一页开始,而MyBatis-Plus的Page构造器参数: - 第一个参数是当前页(从1开始) - 第二个参数是每页大小 用户传入的current应该是从1开始的,如果是0,则需要注意。 另外,检查传入的current和size是否有有效值(大于0),可以在方法开始添加: ```java if (current <= 0 || size <= 0) { // 返回错误信息或使用默认值 return Result.error("分页参数错误"); } ``` 但用户没有提到分页参数无效,所以我们先不处理。 步骤4:返回结果 当前返回的是`Result.ok(ordersPage)`,其中ordersPage是分页查询结果,应该包含了分页信息(总记录数、总页数等)。如果分页插件配置正确,且条件构造正确,应该可以生效。 综合以上,我们重写方法如下: ```java @ApiOperation(value = "订单多条件查询接口", notes = "订单多条件查询接口") @GetMapping("order_query") public Result<?> orderQuery( @RequestParam @ApiParam(value = "当前页", required = true) Integer current, @RequestParam @ApiParam(value = "页大小", required = true) Integer size, @RequestParam(required = false) @ApiParam(value = "会员编号") String memberId, @RequestParam(required = false) @ApiParam(value = "会员等级【A,B,C】") String memberLevel, @RequestParam(required = false) @ApiParam(value = "分账是否已完结: 0-未完结, 1-已完结") Boolean profitSharingFinished, @RequestParam(required = false) @ApiParam(value = "分账状态: PENDING-待分账, PROCESSING-分账中, SUCCESS-分账成功, PARTIAL_SUCCESS-部分成功, FAILED-分账失败, CLOSED-已关闭") String profitSharingStatus, @RequestParam(required = false) @ApiParam(value = "微信支付订单号。用户支付成功后,由回调通知传来。未支付时为空。") String transactionId, @RequestParam(required = false) @ApiParam(value = "订单状态: NOTPAY:未支付, SUCCESS:支付成功, CLOSED:已关闭, REFUND:已退款") String status, @RequestParam(required = false) @ApiParam(value = "订单支付成功时间-开始") Date startTime, @RequestParam(required = false) @ApiParam(value = "订单支付成功时间-结束") Date endTime ) { // 创建分页对象 Page<Orders> page = new Page<>(current, size); // 构建查询条件 LambdaQueryChainWrapper<Orders> queryWrapper = ordersService.lambdaQuery(); queryWrapper .like(ObjectUtil.isNotEmpty(memberId), Orders::getMemberId, memberId) .like(ObjectUtil.isNotEmpty(memberLevel), Orders::getMemberLevel, memberLevel) .eq(ObjectUtil.isNotEmpty(profitSharingFinished), Orders::getProfitSharingFinished, profitSharingFinished) .eq(ObjectUtil.isNotEmpty(profitSharingStatus), Orders::getProfitSharingStatus, profitSharingStatus) .eq(ObjectUtil.isNotEmpty(transactionId), Orders::getTransactionId, transactionId) .eq(ObjectUtil.isNotEmpty(status), Orders::getStatus, status); // 处理时间范围 if (ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isNotEmpty(endTime)) { queryWrapper.between(Orders::getPaidAt, startTime, endTime); } else if (ObjectUtil.isNotEmpty(startTime)) { queryWrapper.ge(Orders::getPaidAt, startTime); } else if (ObjectUtil.isNotEmpty(endTime)) { queryWrapper.le(Orders::getPaidAt, endTime); } // 序并分页 queryWrapper.orderByDesc(Orders::getCreatedAt); Page<Orders> ordersPage = queryWrapper.page(page); return Result.ok(ordersPage); } ``` 注意:这里使用了`ordersService.lambdaQuery()`,它是MyBatis-Plus提供的一种链式查询方式,返回的是`LambdaQueryChainWrapper`对象。 步骤5:确认ordersService的类型 确保`ordersService`是继承了`ServiceImpl`的类,这样才有`lambdaQuery()`方法。 如果上述配置和代码都正确,分页应该生效。 如果问题仍然存在,请检查: - 是否配置了分页插件(步骤1) - 数据库表是否有足够的数据进行分页(如果只有一页,可能看不出分页效果) - 是否在调用`page`方法之前正确设置了分页对象 最后,注意在时间查询条件中,我们使用了`Orders::getPaidAt`,请确认字段名是否正确,以及数据库中对应的字段名是否一致。 生成相关问题: 1. 如何配置MyBatis-Plus的分页插件? 2. MyBatis-Plus的LambdaQueryChainWrapper如何使用? 3. 如何在MyBatis-Plus中处理复杂的时间范围查询? 4. 如何确保分页查询的性能? 5. MyBatis-Plus的分页参数current和size有什么注意事项? </think> 针对您提供的订单多条件查询接口代码,分页未生效的主要原因是时间范围条件判断逻辑存在缺陷以及链式条件拼接方式问题。以下是优化后的完整解决方案: ### 一、问题诊断与修复方案 1. **时间范围条件逻辑错误** 原始代码:`.between((ObjectUtil.isNotEmpty(startTime) && ObjectUtil.isEmpty(endTime))...` 错误点:`ObjectUtil.isEmpty(endTime)` 导致仅当结束时间为空时才触发范围查询,与实际需求相反 2. **链式条件拼接缺陷** 多个条件未正确隔离,特别是时间范围条件未正确处理边界情况 ### 二、优化后的完整代码 ```java @ApiOperation(value = "订单多条件查询接口", notes = "订单多条件查询接口") @GetMapping("order_query") public Result<?> orderQuery( @RequestParam @ApiParam(value = "当前页", required = true) Integer current, @RequestParam @ApiParam(value = "页大小", required = true) Integer size, @RequestParam(required = false) @ApiParam(value = "会员编号") String memberId, @RequestParam(required = false) @ApiParam(value = "会员等级【A,B,C】") String memberLevel, @RequestParam(required = false) @ApiParam(value = "分账是否已完结: 0-未完结, 1-已完结") Boolean profitSharingFinished, @RequestParam(required = false) @ApiParam(value = "分账状态") String profitSharingStatus, @RequestParam(required = false) @ApiParam(value = "微信支付订单号") String transactionId, @RequestParam(required = false) @ApiParam(value = "订单状态") String status, @RequestParam(required = false) @ApiParam(value = "支付成功时间-开始") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime, @RequestParam(required = false) @ApiParam(value = "支付成功时间-结束") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime) { // 创建分页对象 Page<Orders> page = new Page<>(current, size); // 构建条件构造器 LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<>(); // 精确匹配条件 wrapper.like(StringUtils.hasText(memberId), Orders::getMemberId, memberId) .like(StringUtils.hasText(memberLevel), Orders::getMemberLevel, memberLevel) .eq(profitSharingFinished != null, Orders::getProfitSharingFinished, profitSharingFinished) .eq(StringUtils.hasText(profitSharingStatus), Orders::getProfitSharingStatus, profitSharingStatus) .eq(StringUtils.hasText(transactionId), Orders::getTransactionId, transactionId) .eq(StringUtils.hasText(status), Orders::getStatus, status); // 修复时间范围条件 (核心修复点) if (startTime != null && endTime != null) { wrapper.between(Orders::getPaidAt, startTime, endTime); } else if (startTime != null) { wrapper.ge(Orders::getPaidAt, startTime); } else if (endTime != null) { wrapper.le(Orders::getPaidAt, endTime); } // 添加序 wrapper.orderByDesc(Orders::getCreatedAt); // 执行分页查询 Page<Orders> ordersPage = ordersService.page(page, wrapper); return Result.ok(ordersPage); } ``` ### 三、关键优化点说明 1. **时间范围查询重构** ```java if (startTime != null && endTime != null) { wrapper.between(Orders::getPaidAt, startTime, endTime); } else if (startTime != null) { wrapper.ge(Orders::getPaidAt, startTime); // 大于等于开始时间 } else if (endTime != null) { wrapper.le(Orders::getPaidAt, endTime); // 小于等于结束时间 } ``` - 完整覆盖三种时间查询场景 - 使用 `ge()`/`le()` 替代 `between()` 处理单边界条件[^1] 2. **参数校验优化** - 使用 Spring 的 `StringUtils.hasText()` 替代 `ObjectUtil.isNotEmpty()` - 添加 `@DateTimeFormat` 注解规范化时间参数格式 ```java @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime ``` 3. **链式条件解耦** 将条件构造拆分为独立语句,避免复杂逻辑表达式导致的错误: ```java wrapper.like(..., Orders::getMemberId, memberId) .eq(..., Orders::getStatus, status); ``` 4. **分页执行规范** 使用 MyBatis-Plus 标准分页调用方式: ```java ordersService.page(page, wrapper); // 正确传递分页对象和条件 ``` ### 四、必要配置检查 确保项目中已配置 MyBatis-Plus 分页插件(未配置会导致分页失效): ```java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor paginationInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } ``` > 📌 **验证要点** > 1. 分页插件是否生效 > 2. 时间参数格式是否匹配 `yyyy-MM-dd HH:mm:ss` > 3. 数据库字段名与实体类属性是否一致 ### 五、最佳实践建议 1. **参数校验**:添加基础校验减少无效查询 ```java if (current < 1 || size < 1) { return Result.error("分页参数错误"); } ``` 2. **索引优化**:为查询条件字段(如 `paid_at`, `member_id`)添加数据库索引 3. **DTO封装**:建议使用 DTO 对象接收参数,避免长参数列表
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值