为了保证数据的正确性、完整性,前后端都需要进行数据检验。作为一名后端开发工程师,不能仅仅依靠前端来校验数据,我们还需要对接口请求的参数进行后端的校验。最常见的做法就是通过if/else语句来对请求的每一个参数一一校验,当很多参数需要校验的时候,if/else语句就会比较长,写起来也比较麻烦,一点都不简洁、美观。所以,今天来和大家分享一下Spring Boot 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;
}
@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;
}
参考链接:
exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby中使用两个参数异步运行exe吗?我已经尝试过ruby命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何rubygems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除
我有一些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
我正在为一个项目制作一个简单的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"
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)
两者都可以defsetup(options={})options.reverse_merge:size=>25,:velocity=>10end和defsetup(options={}){:size=>25,:velocity=>10}.merge(options)end在方法的参数中分配默认值。问题是:哪个更好?您更愿意使用哪一个?在性能、代码可读性或其他方面有什么不同吗?编辑:我无意中添加了bang(!)...并不是要询问nobang方法与bang方法之间的区别 最佳答案 我倾向于使用reverse_merge方法:option
我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano
我没有找到太多关于如何执行此操作的信息,尽管有很多关于如何使用像这样的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
对于作为String#tr参数的单引号字符串文字中反斜杠的转义状态,我觉得有些神秘。你能解释一下下面三个例子之间的对比吗?我特别不明白第二个。为了避免复杂化,我在这里使用了'd',在双引号中转义时不会改变含义("\d"="d")。'\\'.tr('\\','x')#=>"x"'\\'.tr('\\d','x')#=>"\\"'\\'.tr('\\\d','x')#=>"x" 最佳答案 在tr中转义tr的第一个参数非常类似于正则表达式中的括号字符分组。您可以在表达式的开头使用^来否定匹配(替换任何不匹配的内容)并使用例如a-f来匹配一
我正在使用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”以实现该目的?如果我想通过传递一些
我已经在mountainlion上成功安装了rbenv和rubybuild。运行rbenvinstall1.9.3-p392结束于:校验和不匹配:ruby-1.9.3-p392.tar.gz(文件已损坏)预期f689a7b61379f83cbbed3c7077d83859,得到1cfc2ff433dbe80f8ff1a9dba2fd5636它正在下载的文件看起来没问题,如果我使用curl手动下载文件,我会得到同样不正确的校验和。有没有人遇到过这个?他们是如何解决的? 最佳答案 tl:博士;使用浏览器从http://ftp.rub