Validation不是只能用注解,还可以通过编程方式实现参数校验

开发 前端
本文中,我们讨论了以编程方式为实体类添加验证的方式。可以修改源码时,我们可以使用注解,不能修改时,我们使用编程式校验,几乎可以覆盖绝大部分场景了。

在项目中集成Hibernate-Validation,定义注解,实现参数的校验,相信大家都会。

但如果我们需要校验的类是第三方提供的,由于种种原因无法替换参数类。根据业务逻辑,我们又需要对参数执行特定的校验规则,应该怎么做呢?当注解没有办法使用时,我们就可以使用编程式约束了。

接下来,我们一起看下如何实现。

一、用例描述

我们先引入一个User实体类,假设这个类是由第三方提供的:

import lombok.Data;

@Data
public class User {
    private Long id;
    private String name;
}

我们需要验证User类的id和name字段,id必须是正数、name不能为空。

如果能够修改User类,我们只需要@NotNull和@Range(min = 1)两个注解就解决问题了。现在,我们需要迂回一下。

为了验证User的id字段,我们创建了一个名为UserId的自定义注解:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.hibernate.validator.constraints.Range;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.NotNull;

@Documented
@NotNull
@Range(min = 1)
@ReportAsSingleViolation
@Constraint(validatedBy = {})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface UserId {

    String message() default "${validatedValue} must be a positive long";

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

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

有了注解,还需要有约束定义类:

import org.hibernate.validator.cfg.ConstraintDef;

public class UserIdDef extends ConstraintDef<UserIdDef, UserId> {

    public UserIdDef() {
        super(UserId.class);
    }
}

这里说个题外话,有朋友留言说我不写明引用的包,想想也是,Java栈同名类那么多,不写包名,很容易引起歧义。

此外,User的name字段不能为空,我们直接复用NotNull注解和NotNullDef约束定义。

二、验证器设置

现在,让我们来研究一下验证器配置:

import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.hibernate.validator.cfg.context.TypeConstraintMappingContext;
import org.hibernate.validator.cfg.defs.NotNullDef;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;

import cn.howardliu.effective.spring.constraint.UserIdDef;
import cn.howardliu.effective.spring.entity.User;
import jakarta.validation.Validation;
import jakarta.validation.Validator;

@Configuration
class ValidationConf {

    @Bean
    Validator validator(AutowireCapableBeanFactory autowireCapableBeanFactory) {
        final HibernateValidatorConfiguration conf = Validation.byProvider(HibernateValidator.class).configure();
        final ConstraintMapping constraintMapping = conf.createConstraintMapping();

        final TypeConstraintMappingContext<User> context = constraintMapping.type(User.class);
        context.field("id").constraint(new UserIdDef());
        final NotNullDef notNullDef = new NotNullDef();
        notNullDef.message("must not be null");
        context.field("name").constraint(notNullDef);

        return conf.allowOverridingMethodAlterParameterConstraint(true)
                .addMapping(constraintMapping)
                .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
                .buildValidatorFactory()
                .getValidator();
    }
}

我们使用TypeConstraintMappingContext,将必要的注解分配给User的id和name字段,以实现对应的约束定义。

为了保障测试用例的准确,这里有个小细节:

final NotNullDef notNullDef = new NotNullDef();
notNullDef.message("must not be null");

我们在定义NotNullDef时,设置了message属性。这是因为在当前的hibernate-validation版本中,内置了很多的错误信息,存储在 ValidationMessages*.properties文件族中,不同语言的错误信息不同。

为了实现测试用例的稳定性,统一设置为“must not be null”。

三、应用验证逻辑

接下来我们定义一个接收User参数的组件:

import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import cn.howardliu.effective.spring.entity.User;
import jakarta.validation.Valid;

@Validated
@Service
public class UserService {

    public void handleUser(@Valid User user) {
        System.out.println("Got validated user " + user);
    }
}

此时,如果向UserService的handleUser传入参数,就会执行校验逻辑。

四、测试约束

编写测试用例,看看执行效果:

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import cn.howardliu.effective.spring.entity.User;
import jakarta.validation.ConstraintViolationException;

@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void provideInvalidUser() {
        final User user = new User();
        user.setId(-1L);
        user.setName(null);

        Assertions.assertThatThrownBy(() -> userService.handleUser(user))
                .isInstanceOf(ConstraintViolationException.class)
                .hasMessageContaining("handleUser.arg0.id: -1 must be a positive long")
                .hasMessageContaining("handleUser.arg0.name: must not be null");
    }

    @Test
    void provideValidUser() {
        final User user = new User();
        user.setId(1L);
        user.setName("howardliu.cn");

        assertDoesNotThrow(() -> userService.handleUser(user));
    }
}

我们写了两个测试用例,一个是非法的参数、一个是合法参数。非法参数传入时,会被拦截,并返回定义好的异常信息。

五、总结

本文中,我们讨论了以编程方式为实体类添加验证的方式。可以修改源码时,我们可以使用注解,不能修改时,我们使用编程式校验,几乎可以覆盖绝大部分场景了。

责任编辑:武晓燕 来源: 看山的小屋
相关推荐

2015-04-02 11:25:39

2020-12-28 08:36:30

C语言编程泛型

2024-06-13 08:19:08

Controller接口参数

2024-08-06 09:51:21

SpringHTTPJSON

2021-03-03 08:05:53

C++项目函数

2009-12-09 17:37:36

PHP函数isset(

2023-10-18 18:38:44

数据校验业务

2021-02-01 13:35:28

微信Python技巧

2022-07-29 16:50:30

网络带宽

2022-12-06 17:30:04

2020-11-04 07:36:06

Redis二进制数据库

2022-07-30 23:45:09

内存泄漏检测工具工具

2022-10-09 10:02:09

Python3.12

2022-04-21 14:50:50

Python农历命令

2022-11-10 07:53:54

Spring参数校验

2022-04-21 09:59:53

Nest参数校验

2009-06-15 10:40:14

2015-02-11 09:41:02

土豪APP

2023-09-20 00:27:40

Linux系统

2022-04-24 14:13:00

快手互联网
点赞
收藏

51CTO技术栈公众号