jjzjj

Spring Boot中参数校验

咸鱼爱吃青椒 2024-05-24 原文

前言

为了保证数据的正确性、完整性,前后端都需要进行数据检验。作为一名后端开发工程师,不能仅仅依靠前端来校验数据,我们还需要对接口请求的参数进行后端的校验。最常见的做法就是通过if/else语句来对请求的每一个参数一一校验,当很多参数需要校验的时候,if/else语句就会比较长,写起来也比较麻烦,一点都不简洁、美观。所以,今天来和大家分享一下Spring Boot Validation。

spring-boot-starter-validation

Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上。

如下图所示,spring-boot-starter-web-2.2.6.RELEASE就包含了spring-boot-starter-validation,

而spring-boot-starter-web-2.5.7并没有spring-boot-starter-validation,需要自己手动加入依赖。

如下图所示,手动加入依赖spring-boot-starter-validation,实际上依赖了hibernate-validator。

@Valid和@Validated

@Valid所属包为javax.validation,不具备分组校验功能;可以用在方法、构造函数、方法参数和成员属性(field)上

说明:Java的JSR-303声明了@Valid这类接口,而hibernate-validator对其进行了实现。

JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。

@Validated所属包为org.springframework.validation.annotation,属于spring的校验机制,具有分组校验功能;用在类型、方法和方法参数上。但不能用于成员属性(field)。

@Valid和@Validated注解可以结合使用,来实现嵌套验证。

 常用的注解如下:

简单使用

添加依赖

本文使用的是Spring Boot 2.5.7,具体依赖如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

实体类

@Data
@ToString
public class User {

    private Integer id;

    @NotBlank(message = "姓名不能为空")
    private String name;

    @Max(value = 100, message = "最大不超过100")
    private Integer age;
}    

Controller

@RestController
public class TestController {

    /**
    * post请求 实体类User 加上注解@Valid 
    */
    @PostMapping("/test")
    public String test(@Valid @RequestBody User user) {
        System.out.println(user);
        return "Hello World";
    }

    /**
    * get请求 实体类User 加上注解@Valid 
    * 测试:http://localhost:8080/query2?name=&age=101
    */    
    @GetMapping("/query2")
    public String queryUserInfo(@Valid User user) {
        System.out.println("query2");
        return user.getName();
    }
}
/**
* 【反面教材】验证不生效
*/
@RestController
public class Test3Controller {

    @GetMapping("/test3")
    public String test3(@NotEmpty(message = "name不能为空") String name,
                        @Max(value = 100, message = "请输入数字") Integer age) {
        System.out.println("是啥啊3");
        return name;
    }

    @GetMapping("/test4")
    @Validated
    public String test4(@NotEmpty(message = "name不能为空") String name,
                        @Max(value = 100, message = "请输入数字") Integer age) {
        System.out.println("是啥啊4");
        return name;
    }

    @GetMapping("/test5")
    public String test5(@Validated @NotEmpty(message = "name不能为空") String name,
                        @Valid @Max(value = 100, message = "请输入数字") Integer age) {
        System.out.println("是啥啊5");
        return name;
    }
}
/**
* 【正面教材】类上加注解@Validated,下面的验证生效
*/
@RestController
@Validated 
public class Test2Controller {

    @GetMapping("/query")
    public String queryUserInfo(@NotEmpty(message = "name不能为空") String name,
                                @Max(value = 100, message = "请输入数字") Integer age) {
        System.out.println("是啥啊");
        return name;
    }
}

全局异常处理类

/**
* 参数校验异常包括MethodArgumentNotValidException、BindException、ConstraintViolationException
*/
@RestControllerAdvice
public class ExceptionHandler {

  @org.springframework.web.bind.annotation.ExceptionHandler(MethodArgumentNotValidException.class)
    public String handle(MethodArgumentNotValidException e) {
        e.printStackTrace();
        return e.getBindingResult().getFieldError().getDefaultMessage();
    }

    @org.springframework.web.bind.annotation.ExceptionHandler(BindException.class)
    public String handle(BindException e) {
        e.printStackTrace();
        return e.getBindingResult().getFieldError().getDefaultMessage();
    }

    @org.springframework.web.bind.annotation.ExceptionHandler(ConstraintViolationException.class)
    public String handle(ConstraintViolationException e) {
        e.printStackTrace();
        StringBuffer sb = new StringBuffer();
        for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
            sb.append(violation.getMessage());
        }
        return sb.toString();
    }
}

进阶使用

分组校验

  • 约束注解上声明适用的分组信息groups
@Data
public class UserDTO {
 
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;
 
    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;
 
    // 对于添加、修改都需要操作的公共属性,也可以不加groups标签,这时候,controller中入参需要写成@Validated({UserDTO.Update.class, Default.class}) 即可
    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;
 
    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;
 
    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }
 
    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}
  • @Validated注解上指定校验分组
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}
 
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

嵌套校验

前面的示例中,DTO类里面的字段都是基本数据类型String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况下,可以使用嵌套校验

比如,上面保存User信息的时候同时还带有Job信息。需要注意的是,此时DTO类的对应字段必须标记@Valid注解

@Data  
public class UserDTO {  
  
    @Min(value = 10000000000000000L, groups = Update.class)  
    private Long userId;  
  
    @NotNull(groups = {Save.class, Update.class})  
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})  
    private String userName;  
  
    @NotNull(groups = {Save.class, Update.class})  
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})  
    private String account;  
  
    @NotNull(groups = {Save.class, Update.class})  
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})  
    private String password;  
  
    // job属性上添加@Valid注解
    @NotNull(groups = {Save.class, Update.class})  
    @Valid  
    private Job job;  
  
    @Data  
    public static class Job {  
  
        @Min(value = 1, groups = Update.class)  
        private Long jobId;  
  
        @NotNull(groups = {Save.class, Update.class})  
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})  
        private String jobName;  
  
        @NotNull(groups = {Save.class, Update.class})  
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})  
        private String position;  
    }  
  
    public interface Save {  
    }  
  
    public interface Update {  
    }  
}

自定义校验

  • 自定义注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
// 指明使用那个校验器(类) 去校验使用了此标注的元素。
@Constraint(validatedBy = CaseUpperValidator.class)
@Documented
public @interface CaseUpper {

    String message() default "必须大写";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
  • 实现ConstraintValidator接口编写约束校验器

public class CaseUpperValidator implements ConstraintValidator<CaseUpper, String> {

    // 提示信息
    private String message;

    // 实现校验逻辑的方法。value为当前需要进行校验的值,context可以给约束验证器时提供上下文数据和操作,比如设置错误信息等。
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 为空,返回失败
        boolean isValid = false;
        // value为空不校验;另外,value都是大写校验通过
        if (Objects.isNull(value) || value.equals(value.toUpperCase())) {
            isValid = true;
        }
        // 校验不通过,实现自定义错误信息
        if (!isValid) {
            //禁止默认消息返回
            context.disableDefaultConstraintViolation();
            //自定义返回消息
            context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
        }
        return isValid;
    }

    // caseUpper为当前校验注解的实例,可以获取当前注解的属性值
    @Override
    public void initialize(CaseUpper caseUpper) {
        this.message = caseUpper.message();
    }
}
  • 使用注解@CaseUpper

@Data
@ToString
public class User {

    private Integer id;

    @NotBlank(message = "姓名不能为空")
    private String name;

    @Max(value = 100, message = "最大不超过100")
    private Integer age;

    @NotBlank(message = "englishName不能为空")
    @CaseUpper(message = "englishName必须大写")
    private String englishName;
}

参考链接:

SpringBoot 实现各种参数校验,非常实用!_枫哥和java的博客-CSDN博客

Spring Validation最佳实践及其实现原理,参数校验没那么简单! - 掘金

有关Spring Boot中参数校验的更多相关文章

  1. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  2. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  3. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  4. ruby - 检查方法参数的类型 - 2

    我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)

  5. ruby-on-rails - 在默认方法参数中使用 .reverse_merge 或 .merge - 2

    两者都可以defsetup(options={})options.reverse_merge:size=>25,:velocity=>10end和defsetup(options={}){:size=>25,:velocity=>10}.merge(options)end在方法的参数中分配默认值。问题是:哪个更好?您更愿意使用哪一个?在性能、代码可读性或其他方面有什么不同吗?编辑:我无意中添加了bang(!)...并不是要询问nobang方法与bang方法之间的区别 最佳答案 我倾向于使用reverse_merge方法:option

  6. ruby - 定义方法参数的条件 - 2

    我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano

  7. ruby - rails 3 redirect_to 将参数传递给命名路由 - 2

    我没有找到太多关于如何执行此操作的信息,尽管有很多关于如何使用像这样的redirect_to将参数传递给重定向的建议:action=>'something',:controller=>'something'在我的应用程序中,我在路由文件中有以下内容match'profile'=>'User#show'我的表演Action是这样的defshow@user=User.find(params[:user])@title=@user.first_nameend重定向发生在同一个用户Controller中,就像这样defregister@title="Registration"@user=Use

  8. ruby - 字符串文字中的转义状态作为 `String#tr` 的参数 - 2

    对于作为String#tr参数的单引号字符串文字中反斜杠的转义状态,我觉得有些神秘。你能解释一下下面三个例子之间的对比吗?我特别不明白第二个。为了避免复杂化,我在这里使用了'd',在双引号中转义时不会改变含义("\d"="d")。'\\'.tr('\\','x')#=>"x"'\\'.tr('\\d','x')#=>"\\"'\\'.tr('\\\d','x')#=>"x" 最佳答案 在tr中转义tr的第一个参数非常类似于正则表达式中的括号字符分组。您可以在表达式的开头使用^来否定匹配(替换任何不匹配的内容)并使用例如a-f来匹配一

  9. ruby-on-rails - 如何生成传递一些自定义参数的 `link_to` URL? - 2

    我正在使用RubyonRails3.0.9,我想生成一个传递一些自定义参数的link_toURL。也就是说,有一个articles_path(www.my_web_site_name.com/articles)我想生成如下内容:link_to'Samplelinktitle',...#HereIshouldimplementthecode#=>'http://www.my_web_site_name.com/articles?param1=value1¶m2=value2&...我如何编写link_to语句“alàRubyonRailsWay”以实现该目的?如果我想通过传递一些

  10. ruby - rbenv 安装 ruby​​ 校验和不匹配 osx - 2

    我已经在mountainlion上成功安装了rbenv和ruby​​build。运行rbenvinstall1.9.3-p392结束于:校验和不匹配:ruby-1.9.3-p392.tar.gz(文件已损坏)预期f689a7b61379f83cbbed3c7077d83859,得到1cfc2ff433dbe80f8ff1a9dba2fd5636它正在下载的文件看起来没问题,如果我使用curl手动下载文件,我会得到同样不正确的校验和。有没有人遇到过这个?他们是如何解决的? 最佳答案 tl:博士;使用浏览器从http://ftp.rub

随机推荐