java数据校验spring数据校验hibernate-validator一篇文章搞懂
1、为什么后台也需要校验呢?
虽然我们在前台js进行了拦截,比如submit总体校验一遍,或者每个form控件blur失去焦点的时候进行了校验,但是
我们服务器接口可能被服务器通过代码(http-client)访问,或者其他的方式跳过浏览器js的校验逻辑,如果后台不进行
校验,那么可能会带来严重的安全问题:比如sql注入,XXS攻击等等安全漏洞。
2、使用Hibernate-validator校验。
这个校验框架可不是我们通常所说的Hibernate数据访问层(dao)框架,它只是一个实现JSR-303标准的一个校验框架。
所谓JSR-303其实就是一个校验api定义,而Hibernate-validator是其标准的实现。就像jdbc是java访问数据库的标准api,
而具体的实现由数据库厂商自己去实现。
废话不多说,直接写个demo:
(1)引入相应的jar包
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.3.4.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.4</version>
</dependency>
关于el的jar包引入是因为Hibernate-validator需要使用到el表达式的功能,至少上述版本是这样的,否则运行时会报错。
如果是在web环境,上述el表达式可以设置<scope>provided<scope>。
(2)写好我们需要校验的javaBean
PersonDto.java和Address.java
package normal.test.spring.bootstrap.validator; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import javax.validation.Valid; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; public class PersonDto { @NotNull @Length(max = 10,min = 1,message = "姓名必须在1-10个字符之间") private String name; @Min(value = 18,message = "年龄不能小于18") @Max(value = 100,message = "年龄不能大于100") private Integer age; @Valid private Address address; @Email private String email; public void setEmail(String email) { this.email = email; } public String getEmail() { return email; } public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } public void setAddress(Address address) { this.address = address; } public Address getAddress() { return address; } }
package normal.test.spring.bootstrap.validator; import javax.validation.constraints.NotNull; public class Address { @NotNull private String country; @NotNull private String province; private String city; private String cityDetail; public void setCity(String city) { this.city = city; } public String getCity() { return city; } public void setCountry(String country) { this.country = country; } public String getCountry() { return country; } public void setProvince(String province) { this.province = province; } public String getProvince() { return province; } public void setCityDetail(String cityDetail) { this.cityDetail = cityDetail; } public String getCityDetail() { return cityDetail; } }
其实上述类就是简单java类,只是将JSR-303定义的一些约束类(Constraint)的注解加入到了各个属性而已。
(3)使用Validator
package normal.test.spring.bootstrap.validator; import org.junit.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.util.Set; public class HibernateValidatorTest { @Test public void test01(){ // 首先获取ValidatorFactory ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); // 然后获取validator实例 Validator validator = validatorFactory.getValidator(); // 进行校验 PersonDto personDto = new PersonDto(); personDto.setName("1111111111111111111111111"); personDto.setAge(111); Address address = new Address(); address.setCountry("中国"); personDto.setAddress(address); personDto.setEmail("111111@com"); Set<ConstraintViolation<PersonDto>> constraintViolationSet = validator.validate(personDto); // 如果constraintViolationSet为空说明没有任务错误 for (ConstraintViolation<PersonDto> personDtoConstraintViolation : constraintViolationSet) { System.out.println(personDtoConstraintViolation.toString()); System.out.println(personDtoConstraintViolation.getMessage()); } // 关闭factory validatorFactory.close(); } }
运行截图如下:
由此可见,我们@Valid注解提供了递归校验,这样我们只要在对应的javaBean中写上注解,那么校验起来是非常有效的。
(4)总结各个注解的作用:
这些注解有些是Hibernate-validator自定义的,当然上述描述自己要去实践才行。
3、上述两点只是描述了java开发进行数据校验的标准方式,但是我们开发中往往都会使用spring,那么spring其实也是有自己的校验接口的。
org.springframework.validation.Validator就是spring自己提供的接口,换句话说我们可以使用实现该接口来对某个bean进行校验。
关于spring的文档,我推荐https://www.docs4dev.com/docs/zh/spring-framework/4.3.21.RELEASE/reference/validation.html#validator
下面是借鉴过来的描述:
Spring 具有Validator
接口,可用于验证对象。 Validator
接口使用Errors
对象工作,因此验证器可以将验证失败报告给Errors
对象。
让我们考虑一个小的数据对象:
public class Person { private String name; private int age; // the usual getters and setters... }
我们将通过实现org.springframework.validation.Validator
接口的以下两种方法来提供Person
类的验证行为:
-
supports(Class)
-此Validator
可以验证提供的Class
的实例吗? -
validate(Object, org.springframework.validation.Errors)
-验证给定的对象,如果发生验证错误,请向给定的Errors
对象注册
实现Validator
非常简单,尤其是当您知道 Spring 框架还提供的ValidationUtils
类时。
public class PersonValidator implements Validator { /** * This Validator validates *just* Person instances */ public boolean supports(Class clazz) { return Person.class.equals(clazz); } public void validate(Object obj, Errors e) { ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); Person p = (Person) obj; if (p.getAge() < 0) { e.rejectValue("age", "negativevalue"); } else if (p.getAge() > 110) { e.rejectValue("age", "too.darn.old"); } } }
如您所见,ValidationUtils
类上的static
rejectIfEmpty(..)
方法用于拒绝'name'
属性(如果它是null
或空字符串)。看看ValidationUtils
javadocs,看看它提供了什么功能,除了前面显示的示例。
虽然可以实现单个Validator
类来验证丰富对象中的每个嵌套对象,但是最好将每个嵌套类的验证逻辑封装在自己的Validator
实现中。 *'rich'*对象的一个简单示例是Customer
,它由两个String
属性(名字和名字)和一个复杂的Address
对象组成。 Address
对象可以独立于Customer
对象使用,因此已实现了不同的AddressValidator
。如果您希望CustomerValidator
重用AddressValidator
类中包含的逻辑而不求助于复制粘贴,则可以在CustomerValidator
中依赖注入或实例化AddressValidator
,并按如下方式使用它:
public class CustomerValidator implements Validator { private final Validator addressValidator; public CustomerValidator(Validator addressValidator) { if (addressValidator == null) { throw new IllegalArgumentException("The supplied [Validator] is " + "required and must not be null."); } if (!addressValidator.supports(Address.class)) { throw new IllegalArgumentException("The supplied [Validator] must " + "support the validation of [Address] instances."); } this.addressValidator = addressValidator; } /** * This Validator validates Customer instances, and any subclasses of Customer too */ public boolean supports(Class clazz) { return Customer.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required"); Customer customer = (Customer) target; try { errors.pushNestedPath("address"); ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors); } finally { errors.popNestedPath(); } } }
其实我们经常写在Controller中的方法参数BindingResult就是Errors的扩展。
下面是我的观点了哈:
上述这种spring的校验接口,相当于每个javaBean都要去写对应的XxxValidator接口,其实是非常不方便的。
于是spring中有一个类就整合了第1点和第2点描述的内容,这个类是LocalValidatorFactoryBean
4、重点了,重点了,重点了 。。。。LocalValidatorFactoryBean
该类其实虽然叫FactoryBean但却不是FactoryBean<T>接口的实例。
它准确来讲是javax.validator.Validator的装饰器,同时又将校验功能适配到org.springframework.validation.Validator接口。
所以,我们在代码中可以@Autowire上述两种校验接口。
但是为何我们会对LocalValidatorFactoryBean如何陌生呢?
是因为:
(1)如果在基于xml配置容器元数据时,xml中配置了<mvc:annotation-driven>,那么spring会默认给我们将LocalValidatorFactoryBean注入到容器中。
而且该实例会跟WebDataBinder关联起来,至于WebDataBinder是啥,这里就不多说了。
(2)基于javaCode的方式配置元数据时。
@Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public Validator getValidator(); { // return "global" validator } }
也就是说往往都是容器替我们配置了LocalValidatorFactoryBean,当然我们也可以自行配置。
那么在具体的controller中我们需要怎么做呢?
如下copy的图所示:
我们需要在入参中对javaBean标上@Valid注解,然后紧接其后加入BindingResult参数,这样WebDataBinder会将
校验的错误结果放入到上图的result中。
当然,往往我们都是通过异常解析器统一处理BindingException。
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BindException.class) @ResponseBody public ResultBean validationErrorHandler(BindException ex) throws JsonProcessingException { //1.此处先获取BindingResult BindingResult bindingResult = ex.getBindingResult(); //2.获取错误信息 List<FieldError> fieldErrors = bindingResult.getFieldErrors(); //3.组装异常信息 Map<String,String> message = new HashMap<>(); for (FieldError fieldError : fieldErrors) { message.put(fieldError.getField(),fieldError.getDefaultMessage()); } //4.将map转换为JSON ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(message); //5.返回错误信息 return new ResultBean("400",json); } }
写到这里,我不知道你搞懂了吗?
总结一下:
在我们项目中,往往需要引入Hibernate-validator的jar包,当然spring-boot由starter引入。
目的是为了引入JSR-303的实现。
我们往往不需要配置自己的org.springframework.validation.Validator实例,是因为我们
通过<mvc:annotation-driven>或者@EnableWebMvc的配置告诉spring做了默认注册,当然
该配置,不止是验证那么简单,还有其他的东西。
如果,标准的注解满足不了我们,可以自己实现对应的org.springframework.validation.Validator或者
扩展@Constraint(具体怎么扩展,不在这里提了)。
最后,如果有啥说的不到的,欢迎指出 。。。。。