项目实战五-考试模块-考场、后端入参校验
考场模块
查询列表
- 考场页面列表展示功能
- 通过EasyCode-MybatisCodeHepler生成对应层的类,exam_place表
- 各个层代码如下;
-
po
@Data public class ExamPlace { /** * 主键 */ private Integer id; /** * 名称 */ private String name; /** * 考场是哪个省份的 */ @ForeignField(PlateRegion.class) private Integer provinceId; /** * 考场是哪个城市的 */ @ForeignField(PlateRegion.class) private Integer cityId; /** * 考场的具体地址 */ private String address; /** * 纬度 */ private Double latitude; /** * 经度 */ private Double longitude; }
-
query
@EqualsAndHashCode(callSuper = true) @Data public class ExamPlaceQuery extends KeywordQuery { private Integer provinceId; private Integer cityId; }
-
mapper
public interface ExamPlaceMapper extends BaseMapper<ExamPlace> {}
-
service
//ExamPlaceService public interface ExamPlaceService extends IService<ExamPlace> { void list(ExamPlaceQuery query); } //ExamPlaceServiceImpl @Service @Transactional public class ExamPlaceServiceImpl extends ServiceImpl<ExamPlaceMapper, ExamPlace> implements ExamPlaceService { @Override @Transactional(readOnly = true) public void list(ExamPlaceQuery query) { //查询条件 MpQueryWrapper<ExamPlace> wrapper = new MpQueryWrapper<>(); wrapper.like(query.getKeyword(),ExamPlace::getName,ExamPlace::getAddress); //城市 Integer cityId = query.getCityId(); Integer provinceId = query.getProvinceId(); if (cityId != null && cityId > 0) { wrapper.eq(ExamPlace::getCityId,cityId); }else if (provinceId != null && provinceId > 0) { wrapper.eq(ExamPlace::getProvinceId,provinceId); } //排序方式 wrapper.orderByDesc(ExamPlace::getId); //查询 baseMapper.selectPage(new MpPage<>(query),wrapper).updateQuery(); } }
-
controller
@RestController @RequestMapping("/examPlaces") public class ExamPlaceController extends BaseController<ExamPlace> { @Autowired private ExamPlaceService service; @Override protected IService<ExamPlace> getService() { return service; } @GetMapping public R list(ExamPlaceQuery query) { service.list(query); return Rs.ok(query); } }
-
二级联动
-
在考场页面中有二级联动搜索,如下图
-
如果要实现二级联动,那么后台返回的数据格式应该如下
{ "code": 0, "data": [ { "id": 3, "name": "广东", "plate": "粤", "children": [ { "id": 5, "name": "广州", "plate": "A" }, { "id": 14, "name": "汕头", "plate": "D" } ] }, { "id": 4, "name": "福建", "plate": "闽", "children": [ { "id": 16, "name": "厦门", "plate": "J" }, { "id": 15, "name": "福州", "plate": "A" }, { "id": 17, "name": "莆田", "plate": "B" } ] } ] }
- 分析:
- 这是一个树状结构的数据,但是目前po中的PlateRegion类并不是这样的结构模型,直接按照这个修改?
- 如果修改就破坏了po与数据库表的一致性,因此就出现了dto层
-
在pojo下新建dto包,下面新建数据模型类
//com.zh.jk.pojo.dto @Data public class ProvinceDto { private Integer id; private String name; private String plate; private List<CityDto> children; } @Data public class CityDto { private Integer id; private String name; private String plate; }
- controller改造
-
由于该接口请求的是省份、城市,因此在PlateRegionController中新增接口
/* * 返回所有的省份+城市,树状结构 * */ @GetMapping("/regions") public R listRegions(){ return Rs.ok(service.listRegions()); }
-
-
service新增方法
//PlateRegionService List<ProvinceDto> listRegions(); //PlateRegionServiceImpl @Override public List<ProvinceDto> listRegions() { return baseMapper.selectRegions(); }
- 注意: baseMapper是BaseMapper类,但是该类并没有selectRegions方法,该方法是自定义的,因此需要在BaseMapper类的基础上新增方法
- mapper层,PlateRegionMapper新增方法
- PlateRegionMapper仅仅是一个接口interface,那么如何新增方法实现呢?—通过xml
-
PlateRegionMapper新增自定义方法
public interface PlateRegionMapper extends BaseMapper<PlateRegion> { List<ProvinceDto> selectRegions(); }
-
在resources下新建与PlateRegionMapper相同的包名相同的类名的xml文件,如下
<!--resources.com.zh.jk.mapper.PlateRegionMapper.xml--> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zh.jk.mapper.PlateRegionMapper"> <resultMap id="rmSelectRegions" type="ProvinceDto"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="plate" property="plate"/> <collection property="children" ofType="CityDto"> <id column="city_id" property="id"/> <result column="city_name" property="name"/> <result column="city_plate" property="plate"/> </collection> </resultMap> <!-- 使用了数据库的交集、并集等集合方式 --> <select id="selectRegions" resultMap="rmSelectRegions"> SELECT p.id, p.name, p.plate, c.id city_id, c.name city_name, c.plate city_plate FROM plate_region p JOIN plate_region c ON c.parent_id = p.id WHERE p.parent_id = 0 ORDER BY p.pinyin, c.plate </select> </mapper>
后端入参校验
- 引入:之前写的项目接口,存在下面问题
- 别人拿到这个接口可以直接通过postman传参调用
- web端在传参的时候做的有非空校验等逻辑,但是如果用postman直接调用,可以直接传空值,没有校验逻辑
- 解决办法:在后台进行校验
- 方法一:在具体接口的方法(比如save接口)里面针对前端传递过来的数据进行一一校验
- 方法二:通过hibernate-validator
- hibernate-validator简介
- 使用步骤
-
添加pom依赖
<!-- 后端校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
-
方法的Model参数校验
- 就是一个接口方法传递的参数是Model
- 步骤如下
- 在Model的getter或成员变量上加相关的校验注解
- 在Model参数上加
@Valid
注解
- 代码举例
-
BaseController的save方法参数添加
@Valid
注解@PostMapping("/save") public R save(@Valid T entity){ if ( getService().saveOrUpdate(entity)){ return Rs.ok(CodeMsg.SAVE_OK); }else { return Rs.raise(CodeMsg.SAVE_ERROR); } }
-
po类(Model)属性添加@NotBlank注解
@Data public class DictType { private Integer id; /** * 名称 */ @NotBlank(message = "名称不能为空") private String name; /** * 值 */ @NotBlank(message = "值不能为空") private String value; /** * 简介 */ private String intro; }
-
- 校验失败时,会抛出异常
- rg.springframework.validation.BindException:
- 可以通过BindException.getBindingResult().getAllErrors()拿到所有的错误信息
-
Rs类封装错误处理信息
public static R error(Throwable t) { if (t instanceof CommonException){ CommonException ce = (CommonException)t; return new R(ce.getCode(),ce.getMessage()); }else if(t instanceof BindException){ BindException be = (BindException)t; List<ObjectError> errors = be.getBindingResult().getAllErrors(); List<String> defaultMsgs = new ArrayList<>(errors.size()); for (ObjectError error : errors) { defaultMsgs.add(error.getDefaultMessage()); } String msg = StringUtils.collectionToDelimitedString(defaultMsgs,","); return error(msg); }else if (t instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException)t; List<String> msgs = cve.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.toList()); String msg = StringUtils.collectionToDelimitedString(msgs,","); return error(msg); }else { return error(t.getMessage()); } }
方法的非Model参数校验
- 接口方法的参数不是Model类型
- 在Controller上加
@Validated
注解 - 在非Model参数上加相关的校验注解
- 在Controller上加
- 校验失败时,会抛出异常
- javax.validation.ConstraintViolationException
- 可以通过ConstraintViolationException.getConstraintViolations()拿到所有的错误信息
- 代码举例:
-
BaseController
//1. 在Controller上加`@Validated`注解 @Validated public abstract class BaseController<T> { protected abstract IService<T> getService(); @PostMapping("/remove") //在非Model参数上加相关的校验注解 public R remove(@NotBlank(message = "id不能为空") String id){ String[] idStrs = id.split(","); if (getService().removeByIds(Arrays.asList(idStrs))){ return Rs.ok(CodeMsg.REMOVE_OK); }else { return Rs.raise(CodeMsg.REMOVE_ERROR); } } }
-
常见注解
@NotNull:不能为null,但可以为empty
@NotEmpty:不能为null,而且长度必须大于0
@NotBlank:只能作用在String上,不能为null,且去除空格后长度必须大于0
@AssertFalse:必须为false
@AssertTrue:必须为true
@Max、@DecimalMax:必须为一个不大于指定值的数字
@Min、@DecimalMin:必须为一个不小于指定值的数字
@Digits:必须为一个小数,且整数部分的位数不能超过integer, 小数部分的位数不能超过fraction
@Email:必须是Email,也可以通过正则表达式和flag指定自定义的Email格式
@Future:必须是一个将来的日期
@Past:必须是一个过去的日期
@Range:必须在合适的范围内
@Size、@Length:长度必须在min到max之间
@Pattern:必须符合指定的正则表达式
自定义校验
- 某些场景下,某些属性必须为0、1,这样可以自定义一个注解
-
自定义注解类BoolNumber
//com.zh.jk.common.validato @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) //约束,通过BoolNumberValidator类来校验 @Constraint(validatedBy = BoolNumber.BoolNumberValidator.class) public @interface BoolNumber { //下面这三个必须有 String message() default "只能是0和1"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; class BoolNumberValidator implements ConstraintValidator<BoolNumber, Short> { @Override public boolean isValid(Short num, ConstraintValidatorContext constraintValidatorContext) { return num == null || num == 0 || num == 1; } } }
-
使用
DictItem类的disabled属性 /** * 是否禁用,0代表不禁用(启用),1代表禁用 */ //只能传递0、1 @BoolNumber(message = "disabled只能是0和1") private Short disabled;
快速失败
- 默认情况下是检测完所有的错误后再统一抛出异常
- 比如一个model下有n个限制,正常的是n个异常都检查完才会统一抛出
- 也可以设置快速失败:只要检测到一个错误,就直接抛出异常,不再往下检查
-
创建一个配置类,直接放入容器即可
//com.zh.jk.common.cfg //放入容器 @Configuration public class ValidatorCfg { @Bean public Validator validator() { return Validation .byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory().getValidator(); } }