项目实战五-考试模块-考场、后端入参校验

考场模块

查询列表

  1. 考场页面列表展示功能
  2. 通过EasyCode-MybatisCodeHepler生成对应层的类,exam_place表
  3. 各个层代码如下;
    1. 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;
       }
      
    2. query

       @EqualsAndHashCode(callSuper = true)
       @Data
       public class ExamPlaceQuery extends KeywordQuery {
           private Integer provinceId;
           private Integer cityId;
       }
      
    3. mapper

       public interface ExamPlaceMapper extends BaseMapper<ExamPlace> {}
      
    4. 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();
           }
       }
      
    5. 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);
           }
       }
      

二级联动

  1. 在考场页面中有二级联动搜索,如下图

    图1

  2. 如果要实现二级联动,那么后台返回的数据格式应该如下

     {
         "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"
                     }
                 ]
             }
         ]
     }
    
  3. 分析:
    1. 这是一个树状结构的数据,但是目前po中的PlateRegion类并不是这样的结构模型,直接按照这个修改?
    2. 如果修改就破坏了po与数据库表的一致性,因此就出现了dto层
    3. 在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;
       }
      
  4. controller改造
    1. 由于该接口请求的是省份、城市,因此在PlateRegionController中新增接口

       /*
       * 返回所有的省份+城市,树状结构
       * */
       @GetMapping("/regions")
       public R listRegions(){
           return Rs.ok(service.listRegions());
       }
      
  5. service新增方法

     //PlateRegionService
     List<ProvinceDto> listRegions();
     //PlateRegionServiceImpl
     @Override
     public List<ProvinceDto> listRegions() {
         return baseMapper.selectRegions();
     }
    
    1. 注意: baseMapper是BaseMapper类,但是该类并没有selectRegions方法,该方法是自定义的,因此需要在BaseMapper类的基础上新增方法
  6. mapper层,PlateRegionMapper新增方法
    1. PlateRegionMapper仅仅是一个接口interface,那么如何新增方法实现呢?—通过xml
    2. PlateRegionMapper新增自定义方法

       public interface PlateRegionMapper extends BaseMapper<PlateRegion> {
           List<ProvinceDto> selectRegions();
       }
      
    3. 在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>
      

后端入参校验

  1. 引入:之前写的项目接口,存在下面问题
    1. 别人拿到这个接口可以直接通过postman传参调用
    2. web端在传参的时候做的有非空校验等逻辑,但是如果用postman直接调用,可以直接传空值,没有校验逻辑
  2. 解决办法:在后台进行校验
    1. 方法一:在具体接口的方法(比如save接口)里面针对前端传递过来的数据进行一一校验
    2. 方法二:通过hibernate-validator
  3. hibernate-validator简介
    1. 是Java中常用的一款后端校验框架
    2. 参考文档
      1. https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/
      2. https://docs.jboss.org/hibernate/stable/validator/reference/en-US/pdf/hibernate_validator_reference.pdf
  4. 使用步骤
    1. 添加pom依赖

       <!-- 后端校验 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-validation</artifactId>
       </dependency>
      

方法的Model参数校验

  1. 就是一个接口方法传递的参数是Model
  2. 步骤如下
    1. 在Model的getter或成员变量上加相关的校验注解
    2. 在Model参数上加@Valid注解
  3. 代码举例
    1. 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);
           }
       }
      
    2. po类(Model)属性添加@NotBlank注解

       @Data
       public class DictType {
           private Integer id;
           /**
           * 名称
           */
           @NotBlank(message = "名称不能为空")
           private String name;
           /**
           * 值
           */
           @NotBlank(message = "值不能为空")
           private String value;
           /**
           * 简介
           */
           private String intro;
       }
      
  4. 校验失败时,会抛出异常
    1. rg.springframework.validation.BindException:
    2. 可以通过BindException.getBindingResult().getAllErrors()拿到所有的错误信息
  5. 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参数校验

  1. 接口方法的参数不是Model类型
    1. 在Controller上加@Validated注解
    2. 在非Model参数上加相关的校验注解
  2. 校验失败时,会抛出异常
    1. javax.validation.ConstraintViolationException
    2. 可以通过ConstraintViolationException.getConstraintViolations()拿到所有的错误信息
  3. 代码举例:
    1. 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:必须符合指定的正则表达式

自定义校验

  1. 某些场景下,某些属性必须为0、1,这样可以自定义一个注解
  2. 自定义注解类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;
             }
         }
     }
    
  3. 使用

     DictItem类的disabled属性
        
     /**
     * 是否禁用,0代表不禁用(启用),1代表禁用
     */
     //只能传递0、1
     @BoolNumber(message = "disabled只能是0和1")
     private Short disabled;
    

快速失败

  1. 默认情况下是检测完所有的错误后再统一抛出异常
    1. 比如一个model下有n个限制,正常的是n个异常都检查完才会统一抛出
  2. 也可以设置快速失败:只要检测到一个错误,就直接抛出异常,不再往下检查
  3. 创建一个配置类,直接放入容器即可

     //com.zh.jk.common.cfg
     //放入容器
     @Configuration
     public class ValidatorCfg {
         @Bean
         public Validator validator() {
             return Validation
                     .byProvider(HibernateValidator.class)
                     .configure()
                     .failFast(true)
                     .buildValidatorFactory().getValidator();
         }
     }
    
Table of Contents