优雅的处理参数校验以及异常

1、前言

 编写控制层时,我们可能会自己去校验请求参数,就会出现这样的代码:

if (StringUtils.isEmpty(memberSid)) {
	return new JsonResult(false, "参数memberSid为空");
}
if (null == test) {
	return new JsonResult(false, "参数test为空");
}

相同的参数在不同的地方运用就是出现类型的冗余的代码。如果我们要快速开发要么把参数校验复制一份出来,要么抽成公用的方法 。重复的代码本来就是代码重构和优化的一个表象,所以这种方式本身不可取。抽成公用方法确实是一个不错的方法,但是未能从本质像解决。

       由此,看能不能使用框架的技术去解决这样问题。试着使用【Spring Validation】能否解决问题。

2、引入依赖

非springboot 项目需要引入一下依赖

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>
 
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.1.Final</version>
</dependency>

springboot 项目需要区分版本:

springboot2.3之前,只需要引入一下依赖即可:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.3之前的版本</version>
</dependency>

springboot2.3之后(含2.3),取消了【hibernate-validator】相关的依赖,需要手动引入一下依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

以下为官方的的说明:

3、了解【Spring Validation】的相关注解

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。

针对参数的注解:

 

@Validated和@Valid的区别:

@Validated

  • 提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,可参考高效使用hibernate-validator校验框架。
  • 可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上。

@Valid

  • 作为标准JSR-303规范,不支持分组的功能。
  • 可以用在方法、构造函数、方法参数和成员属性(字段)上。

* 因为@Valid可以用来成员属性上,该注解也可以用来校验嵌套的校验。

4、最佳实践

4.1 请求参数的实体

      实体中针对需要校验的字段使用相应的注解,指定输出的message,以及分组。分组是为了区分不同业务场景实体的公用。

@Data
public class BookDTO implements Serializable {

    private static final long serialVersionUID = 6833185176640090531L;

    private Integer id;
    @Max(value = 30L, message = "最高价格不超过30!", groups = {SaveAction.class})
    private Double price;
    @NotNull(message = "作者信息不能为空!", groups = {UpdateAction.class})
    private String author;
    @Length(min = 5, max = 10, message = "书名的长度必须在5~10之间")
    private String name;
    private Date buyDate;

    // 开启嵌套校验
    @Valid
    private BookContent bookContent;
}

4.2 嵌套类

@Data
public class BookContent implements Serializable {

    private static final long serialVersionUID = -3018013509162052516L;

    @NotBlank(message = "书的内容不能为空!")
    private String content;
    private Integer pageCount;
}

4.3 分组接口

分组接口只是为了标识使用的场景,接口中没有任何的实现。

public interface SaveAction {}

public interface UpdateAction {}

4.4 控制层的实现

      @RequestBody是为了测试方便,使用的json参数提交,也可以使用表单提交。

      参数中没有加分组的都属于默认分组,使用Default.class即可。

@RestController
@RequestMapping("/foo")
public class FooController {

    @PostMapping ("/test01")
    public String test01(@Validated({SaveAction.class, Default.class}) @RequestBody BookDTO bookDTO){
        System.out.println(JSON.toJSONString(bookDTO));
        Assert.notNull(bookDTO.getId(), "ID信息不能为空!");
        return "success";
    }

    @GetMapping("/test02")
    public String test02(@RequestParam String a, Date date){
        if (date != null) {
            System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(date));
        }
        return "success:" + a;
    }
}

4.5 统一参数处理

     参数的校验不通过,如果控制层不处理的话,都会抛出异常,只需要针对异常处理,获取异常的报错信息响应即可。

     @ControllerAdvice是针对所有的控制Controller。因为前端传递的参数有可能是日期。那我们需要将对应的字符串转成日期,可以使用@InitBinder完成对日期的解析。

     @InitBinder对日期的解析只是针对form-data结构有效,如果是JSON数据的话则无效。

@ControllerAdvice
@Slf4j
public class GlobalHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> globalExceptionHandler(Exception e){
        log.warn("异常信息:{}", e);
        String msg = e.getMessage();
        if (e instanceof BindException) {
            msg = ((BindException)e).getBindingResult().getFieldError().getDefaultMessage();
        }else if (e instanceof MissingServletRequestParameterException) {
            MissingServletRequestParameterException me = (MissingServletRequestParameterException)e;
            msg = "参数缺失,缺失的参数:" + me.getParameterName();
        }

        return ResponseEntity.ok(msg);
    }

    @InitBinder
    public void globalWebDataBinder(WebDataBinder binder){
        DateFormatter dateFormatter = new DateFormatter("yyyy-MM-dd");
        String[] patterns = {"yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"};
        dateFormatter.setFallbackPatterns(patterns);
        binder.addCustomFormatter(dateFormatter, Date.class);
    }
}

5、异常的处理

       对于绑定参数的处理,归根到底是对异常的处理。对于异常的处理,我们最终需要获取到异常的信息,那我们只需要处理有差异的异常即可。

      曾看到很多文档这对异常逐个处理,但是最终的结果都是 e.getMessage() 。个人觉得没有必要。

      异常的处理包括框架的异常,以及自定义异常。最终包装成用户可以识别的错误信息返回。这也是异常处理的目的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值