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的文档,我推荐Spring Framework 中文文档 - 9. 验证,数据绑定和类型转换 | Docs4dev

 下面是借鉴过来的描述:

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(具体怎么扩展,不在这里提了)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值