写程序三个步骤:参数校验,程序执行,回写结果(返回结果)。应该有不少人像我一样不喜欢将一大堆的参数校验与业务代码混杂在一起。这个问题和另一个问题不可靠的数据来源是相关联的。

在实际开发中,每开始执行一段逻辑之前你都应该校验参数的合法性,只有通过校验才可以往下执行逻辑,否则抛出相应的异常(或者其他处理方式)。参数校验但是无处不在的,前端参数你得校验,从数据库中select出来的数据你也得校验,外部系统API的返回你还是得校验。这是一个繁杂又啰嗦的过程,写多了确实十分恼人,但是你又不得不写,因为他人是不可靠的,在校验之前,你永远无法相信其他人传递给你的东西是否合法。这时候我们就应该想想方法如何减少校验的重复工作。
从前文你可以知道主要有俩个地方是需要校验的:外部参数、计算结果(也就是后续需要使用的返回值)。
1.外部参数校验:这个部分的校验一般可以使用现有的校验框架解决问题,一般使用hibernate-validator去处理这个问题。大概思路是:标记你要验证的方法及参数,验证失败抛出异常,全局异常接住异常从中解析出什么参数为什么不合法然后将这个信息返回给前端。
定义需要校验的参数类:

public class TestModel {
    @NotBlank(message = "name不能为空")
    private String name;
    @Range(max = 100,min=10,message = "age在10到100之间")
    private Integer age;
    
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

校验:

    @PostMapping("/test/tt1")
    public String test1(@RequestBody @Valid TestModel model,BindingResult result){
        if(result.hasErrors()) {
            return result.getAllErrors().get(0).getDefaultMessage();
        }
        return testService.test(null);
    }

注:
a.由于不同的方法可能使用同一个类型的参数,但是内部的校验条件可能是不一样的,所以每个校验注解都可以设置一个group属性用以区分不同的校验类型。
b.它不会使用在除Controller层的其他地方,当然你可以使用AOP然后在切面中自己使用Validator验证。
c.具体如何使用你可以查看文档

现在我们得到了校验参数基本步骤:在SpringMVC中Controller层的参数校验,由于请求是通过dispacherServlet分发到各个mapping的Controller方法的,在这个过程中可以很容易的将参数通过参数校验框架验证通过后再调用,所以spring自己已经解决了这些校验问题;而在其他的地方使用这样的参数校验的最好的方法是使用AOP将参数校验与业务逻辑分开来,或者你可以直接在调用方应用该Validator做校验,校验通过则继续往下走,不通过则抛异常。

为什么我主张抛异常而不是直接返回错误呢?程序只有俩个流程,正常流程与异常流程,抛异常可以直接将异常流程与正常流程区别开来,虽然他会更加耗时,但是职责更加明确。当然,这不是一个放之天下皆准的道理,你需要结合实际的业务场景提出最合理的方案。

2.计算结果校验:在你做了一次运算(调用方法、API、查库等等)之后,如果其影响的数据在后续是需要使用的,你就需要对该数据做参数校验。典型的情况是查库之后对返回值做判空操作(null或者size为0)。大多数情况下这种校验是可以被接受的,让人无法接受的是不按照约定赋值的字段,这使得所有的数据都不再可靠。
典型的返回后数据校验:

    public String test(TestModel name) {
        String res = restTemplate.getForObject(URI.create("http://www.baidu.com/"), String.class);
        if(res == null) {
            //处理数据不合法的情况
            log.info("res is empty");
            throw new RuntimeException("baidu未返回合法参数");
        }
        log.info(res);
        return res;
    }

正常的校验操作我主张使用Spring Assert工具区判断:

    public String test(TestModel name) {
        String res = restTemplate.getForObject(URI.create("http://www.baidu.com/"), String.class);
        Assert.hasText(res, "res is empty");//处理参数不合法情况
        
        log.info(res);
        return res;
    }

或者你也可以使用Spring或者Apache Commons组件的各种Utils去判断数据正确性。像这样正常的数据校验直接校验即可。

有这样一种情况,数据没有按约定或者常理正确的赋值是最恶心的。比如,我们定义了一张user表,里面有一个modify_time字段,存最后一次数据的修改时间。按常理他是一定有值的(即使你不想为其赋值你也可以给一个NOW()默认值)。但是当你使用该字段的时候却发现该字段是null,于是你得到教训表中除了ID是可靠的其他字段比如name,age在你需要使用的时候你都需要做数据校验才能够让你的程序往下走。
现在你需要为每一个使用了该数据的程序添加校验代码,并且你埋下了一颗怀疑的种子,你知道这些数据都不再可靠。这是个非常愚蠢的事情,就因为这么一个小小的赋值操作,让后续的所有的使用到该数据的代码都需要添加上校验。我在对接抖店API的时候就遇到了这么恶心人的情况,抖店的消息文档上写的清清楚楚写着消息体里面会带有的字段及其含义,到你接受到的时候却完全不是这么回事。这就意味着我得每一种情况都测试一遍才能知道它到底是什么格式。这也意味着每一个对接该消息的程序员都得这么做一遍,这是个非常非常脑残的错误,就只因为字节的程序员不按照约定返回。

总之,校验有俩个部分,对入参的校验与对调用后返回数据的校验。正常的数据校验已经有一套比较成熟的套路可以很好的完成这些校验工作。我们重点是需要规范那些不可靠的数据,这些问题在你对接一个外部系统的时候和兼容数据的时候会特别常见,这可能会让你每做一步操作就不得不做数据校验以保证你的程序可以正确执行。
一个比较好的技巧是在返回值的对应类上定义好对应的静态校验方法或者在数据的setter或者getter方法里面做参数校验(和Assert做类似样的操作),如果不合法则抛出异常。当然这样解决依然无法彻底解决问题,因为外部数据你是无法确定也无法控制的,而正是因为如此所以参数校验才会无处不在,才会成为开发噩梦。

注:如果你有什么意见或建议,可以评论留言。转载请标明作者与出处。

标签: 业务开发, 开发难点, 解决方案

已有 2 条评论

  1. 1. 表字段的默认值太重要。wsf 中order_base 应该都有默认值。我曾经在获取的order_base 是也会怀疑,然后我看了表结构每个字段都有默认值,后续就不对返回的字段属性做空判断
    2. 我有个疑问啊,Assert.hasText 这些断言是可以可以应用到生产环境的么?

    1. 我个人认为是可以的,Assert.hasText本身是抛出的就是IllegalArgumentException,语义和类型都是合适的。如果业务上强制要求参数校验阶段应该抛出业务异常那就得和规定保持一致了,我倾向与将参数校验作为业务外的一部分,可以通过技术手段将参数校验与业务逻辑向剥离。

添加新评论