第1步:导入依赖
<!-- 校验框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
第2步:为需要校验的参数(实体类或方法中)加上校验规则
常见的校验注解
- @NotNull:确保字段不是null。适用于任何类型。
- @NotEmpty:确保字段既不是null也不是空(对于字符串意味着长度大于0,对于集合意味着至少包含一个元素)。适用于字符串、集合、数组等。
- @NotBlank:确保字符串字段不是null且至少包含一个非空白字符。只能用于字符串类型。
- @Length:字符串长度 max是字符串的最大长度 min是字符串的最小长度(对比于@Size,只能用于字符串)
- @Size(min=, max=):确保字段(字符串、集合、数组)的长度(或大小)在指定的最小值和最大值之间。适用于字符串、集合、数组等。
- @Min(value=) 和 @Max(value=):对数值类型字段设置最小值和最大值。适用于数值类型(Integer、Long等)。
- @Range :可以应用于整型(int、long等)和浮点型(float、double等)。校验数字的大小,min是数字最小值,max是数字的最大值
- @Email:确保字段是有效的电子邮件地址。适用于字符串类型。
- @Pattern(regexp=):确保字符串字段匹配指定的正则表达式。适用于字符串类型。
- @Past 和 @Future:校验日期是否为过去的时间或将来的时间。适用于日期类型(如LocalDate、LocalDateTime等)。
这些注解可以应用于模型类的字段上,以定义字段的验证规则。在Spring MVC的控制器中,可以使用@Valid
或@Validated
注解来触发这些验证。如果验证失败,Spring将抛出一个异常,可以通过全局异常处理来捕获这个异常并返回适当的响应给客户端。
请注意,@Valid
和@Validated
注解虽然都用于数据验证,但它们在功能和使用上有所不同。@Valid
是JSR 303/JSR 380 Bean Validation API的一部分,而@Validated
是Spring的特有注解,支持验证组的概念,允许更灵活地指定在特定情况下应用哪些验证约束。在实际开发中,可以根据需要选择使用哪个注解。
2.1 编辑校验组
实体类中的多个字段被赋予了校验规则,然而在不同的方法中,这些字段不一定是都需要被校验的。
(如下单和取消订单,取消订单需要校验取消订单的原因是否为空,而下单则不需要,但是方法中传的都是order这个实体类,因此区分校验组是必须的)
package com.gmgx.common;
import jakarta.validation.groups.Default;
public interface ValidGroup extends Default {
interface IdAndCancelReason extends ValidGroup {}
interface IdAndCancelReasonMember extends ValidGroup {}
interface IdAndRejectionReason extends ValidGroup {}
interface IdAndRejectCancelReason extends ValidGroup {}
interface IdAndShopId extends ValidGroup {}
interface ValidOrderSearchDto extends ValidGroup {}
interface SubmitNoItems extends ValidGroup {}
}
//public interface ValidGroup extends Default {
// interface Crud extends ValidGroup {
// interface create extends Crud {
// }
//
// interface Update extends Crud {
// }
//
// interface Query extends Crud {
// }
//
// interface Delete extends Crud {
// }
// }
//}
创建一个ValidGroup继承Default接口,在里面写接口继承ValidGroup,一个接口即是一个校验组。
2.2 为各种实体类添加校验规则
package com.gmgx.entity;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.gmgx.common.ValidGroup;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
/**
* <p>
* 订单表
* </p>
*
* @since 2024-09-11
*/
@Data
public class Orders implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键-订单号
*/
@NotBlank(groups = {ValidGroup.IdAndShopId.class,
ValidGroup.IdAndCancelReason.class, ValidGroup.IdAndRejectCancelReason.class,
ValidGroup.IdAndRejectionReason.class, ValidGroup.IdAndCancelReasonMember.class},
message = "订单id不能为空")
private String id;
/**
* 订单状态 1待付款,2待派送,3待取餐,4已派送,5已完成,6已取消,7外卖待审批,8堂食待审批
*/
private Integer status;
/**
* 下单用户
*/
private String memberId;
/**
* 地址id
*/
private String addressBookId;
/**
* 下单时间
*/
// @TableField(fill = FieldFill.INSERT)//新增时自动填充
private Date orderTime;
/**
* 所属店铺
*/
@NotBlank(groups = {ValidGroup.IdAndShopId.class}, message = "shopId不能为空")
private String shopId;
/**
* 结账时间
*/
private Date checkoutTime;
/**
* 支付方式 1微信,2支付宝
*/
private Integer payMethod;
/**
* 取餐码(四位数)
*/
private String pickupCode;
/**
* 实收金额
*/
private BigDecimal amount;
/**
* 备注
*/
private String remark;
/**
* 收件人手机号
*/
private String phone;
/**
* 收件地址
*/
private String address;
/**
* 收货人姓名
*/
private String consignee;
/**
* 0-外卖 1-堂食
*/
private String type;
@Length(min = 1, max = 32, groups = ValidGroup.IdAndCancelReason.class,
message = "管理后台取消订单的原因不能为空,且不能超过32个字")
private String cancelReason;
@Length(min = 1, max = 32, groups = ValidGroup.IdAndRejectionReason.class,
message = "商家拒单的原因不能为空,且不能超过32个字")
private String rejectionReason;
@Length(min = 1, max = 32, groups = ValidGroup.IdAndCancelReasonMember.class,
message = "用户取消订单的原因不能为空,且不能超过32个字")
private String cancelReasonMember;
@Length(min = 1, max = 32, groups = ValidGroup.IdAndRejectCancelReason.class,
message = "商家拒绝用户取消订单的原因不能为空,且不能超过32个字")
private String rejectCancelReason;
}
package com.gmgx.dto;
import com.gmgx.common.ValidGroup;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 用户下单 需要传入的参数封装成一个dto
*/
@Data
public class SubmitDto {
//........省略没有校验规则的字段
@Size(min = 1, message = "生成的订单必须至少含有一个商品", groups = ValidGroup.SubmitNoItems.class)
private List<ItemDto> items;
}
package com.gmgx.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.gmgx.common.ValidGroup;
import jakarta.validation.constraints.Past;
import lombok.Data;
import java.util.Date;
/**
* 封装多字段查询需要的条件dto
*/
@Data
public class OrderSearchDto {
//开始时间
@Past(groups = ValidGroup.ValidOrderSearchDto.class, message = "开始时间必须在当前时间之前")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//东八区
private Date startTime;
//结束时间
@Past(groups = ValidGroup.ValidOrderSearchDto.class, message = "结束时间必须在当前时间之前")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endTime;
//..........
}
2.3 在控制器中对需要校验参数的方法开启校验
package com.gmgx.controller;
import com.gmgx.common.Result;
import com.gmgx.common.ValidGroup;
import com.gmgx.dto.SubmitDto;
import com.gmgx.vo.OrderVo;
import com.gmgx.dto.OrderSearchDto;
import com.gmgx.entity.Orders;
import com.gmgx.exceptions.enumeration.ResponseEnum;
import com.gmgx.service.OrdersService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
//将@Validated写在类上,开启对所有方法的参数校验
@Validated
@RestController
@RequestMapping("/order")
@Tag(name = "订单控制器")
public class OrdersController {
@Resource
private OrdersService ordersService;
//管理后台
/**
* 取消订单(管理后台)
*
* @param
* @return 订单是否被取消的信息
*/
@Operation(summary = "后台取消订单", description = "需要传入id,cancelReason")
@PutMapping("cancel")
public Result<Object> cancel(@RequestBody @Validated(value = ValidGroup.IdAndCancelReason.class) Orders order) {
Boolean flag = ordersService.cancel(order);
if (flag) {
return Result.success("取消订单成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
/**
* 拒单
*
* @return 订单状态是否被修改为已取消的信息
*/
@Operation(summary = "商家拒单", description = "需要传入id,rejectionReason")
@PutMapping("rejection")
public Result<Object> rejection(@RequestBody @Validated(value = ValidGroup.IdAndRejectionReason.class) Orders order) {
Boolean flag = ordersService.rejection(order);
if (flag) {
return Result.success("拒单成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
/**
* 商家拒绝用户取消订单
*/
@Operation(summary = "商家拒绝用户取消订单",
description = "需要传入id,status(7外卖待审批,8堂食待审批),rejectionCancelReason")
@PutMapping("rejectCancel")
public Result<Object> rejectCancel(@RequestBody @Validated(ValidGroup.IdAndRejectCancelReason.class) Orders order) {
Boolean flag = ordersService.rejectCancel(order);
if (flag) {
return Result.success("拒单成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
/**
* 接单
* 需要传 id type shopid shopid 用于redis 拿值
*
* @return 订单状态是否被修改为已接单的信息
*/
@Operation(summary = "接单", description = "需要传入id,type,shopid")
@PutMapping("confirm")
public Result<Object> confirm(@RequestBody @Validated(ValidGroup.IdAndShopId.class) Orders order) {
Boolean flag = ordersService.confirm(order);
if (flag) {
return Result.success("接单成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
/**
* 派送订单
*
* @param id
* @return
*/
@Operation(summary = "派送订单", description = "需要传入id")
@PutMapping("delivery/{id}")
public Result<String> delivery(@PathVariable @NotBlank(message = "订单id不能为空") String id) {
Boolean flag = ordersService.delivery(id);
if (flag) {
return Result.success("修改订单的状态为已派送,请求成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
/**
* 完成订单
*
* @param id
* @return 订单状态是否被修改为已完成的信息
*/
@Operation(summary = "完成订单", description = "需要传入id")
@PutMapping("complete/{id}")
public Result<Object> complete(@PathVariable @NotBlank(message = "订单id不能为空") String id) {
Boolean flag = ordersService.complete(id);
if (flag) {
return Result.success("修改订单的状态为完成,请求成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
/**
* 订单搜索
*
* @return
*/
@Operation(summary = "订单搜索", description = "可选参数:开始,结束时间,订单状态。用户名、订单号、地址是模糊查询")
@PostMapping("conditionSearch")
//get请求没有请求体
public Result<Object> conditionSearch(@RequestBody @Validated(value = ValidGroup.ValidOrderSearchDto.class) OrderSearchDto dto) {
log.info("dto:{}", dto);
List<Orders> orders = ordersService.conditionSearch(dto);
if (orders.size() > 0) {
return Result.success(orders);
}
return Result.error(ResponseEnum.CONDITION_SEARCH_ERROR);
}
/**
* 查询订单详情
*
* @param oId 这是订单id
* @return
*/
@Operation(summary = "查询订单详情(管理端和客户端通用)", description = "传入参数 订单的id")
@GetMapping("details/{oId}")
public Result<Object> getDetails(@PathVariable @NotBlank(message = "订单id不能为空") String oId) {
OrderVo order = ordersService.getDetails(oId);
if (order != null) {
return Result.success(order);
}
return Result.error(ResponseEnum.GET_DETAIL_ERROR);
}
}
第3步:添加拦截器,参数异常时返回统一结果
package com.gmgx.handle;
import com.gmgx.common.Result;
import com.gmgx.exceptions.BusinessException;
import com.gmgx.exceptions.enumeration.ResponseEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
//捕获异常
public class CommonExceptionHandler {
//校验参数
@ExceptionHandler
public Result<List<String>> handleParametersException(MethodArgumentNotValidException e, BindingResult errors) {
log.info("参数不合法");
e.printStackTrace();
List<String> collect = errors.getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return Result.error(ResponseEnum.INVALID_PARAMETERS, collect);
}
@ExceptionHandler(Exception.class)
public Result<Object> handleException(Exception e) {
e.printStackTrace();
return Result.error(e.getMessage());
}
@ExceptionHandler(BusinessException.class)
public Result<Object> handleBusinessException(BusinessException e) {
e.printStackTrace();
ResponseEnum response = e.getResponse();
// return Result.error(e.getMessage());
return Result.error(response);
}
}
第4步:clean service 删掉.idea
因为idea有缓存,会导致校验规则不生效,所以要做这两步
测试
单个字段的参数校验可以在test里面测
@Operation(summary = "派送订单", description = "需要传入id")
@PutMapping("delivery/{id}")
public Result<String> delivery(@PathVariable @NotBlank(message = "订单id不能为空") String id) {
Boolean flag = ordersService.delivery(id);
if (flag) {
return Result.success("修改订单的状态为已派送,请求成功");
}
return Result.error(ResponseEnum.INVALID_ORDER_STATUS_CHANGED);
}
@Test
void testDelivery_noOId(){
ordersController.delivery("");
}
结果如下