• 微服务之路(三)-springboot验证


    前言

    主要议题

    • Bean Validation(JSR-303):介绍Java Bean验证、核心API、实现框架Hibernate Validator
    • Apache commons-validator:介绍最传统Apache通用验证器框架,如:长度、邮件等方式。
    • Spring Validator:介绍Spring内置验证器API、以及自定义实现。

    主体内容

    一、Bean Validation

    JSR-303

    1.Maven依赖

    <dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

    2.命名规则(Since Spring Boot 1.4)

    SpringBoot大多数情况采用starter(启动器,包含一些自动装配的Spring组件),官方的命名规则:spring-boot-starter-{name},业界或者民间:{name}-spring-boot-starter

    3.举例

    (1)老样子,我们去https://start.spring.io/构建一个validation的springboot项目。

    (2)然后Idea导入该项目,创建domain下的User.java模型(不难理解,@Max注解设置最大值,@NotNull不为空)。

    import javax.validation.constraints.Max;
    import javax.validation.constraints.NotNull;
    
    /**
     * @ClassName User
     * @Describe 用户模型
     * @Author 66477
     * @Date 2020/5/1321:50
     * @Version 1.0
     */
    public class User {
    
        @Max(value=10000)
        private long id;
    
        @NotNull
        private String name;
    
        private String cardNumber;
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getCardNumber() {
            return cardNumber;
        }
    
        public void setCardNumber(String cardNumber) {
            this.cardNumber = cardNumber;
        }
    }
    

    (3)创建controller下的UserController.java,解释已经在注释里了,不做过多赘述。

    import com.gupao.springbootbeanvalidation.domain.User;
    import org.springframework.util.Assert;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    import javax.validation.Valid;
    
    /**
     * @ClassName
     * @Describe 控制层直接进行验证,这里只是为了举例
     * @Author 66477
     * @Date 2020/5/1321:59
     * @Version 1.0
     */
    @RestController
    public class UserController {
        /**
         * 经过Postman测试,json格式 POST请求方式时,如果加上@Valid,User模型设置了@Max(value=10000),@NotNull等,如果参数不符合条件,直接会返回400错误
         * @param user
         * @return
         */
        @PostMapping("/user/save")
        public User save(@Valid @RequestBody User user){
    
            return user;
        }
    
        @PostMapping("/user/save2")
        public User save2(@Valid @RequestBody User user){
            //API调用的方式
            Assert.hasText(user.getName(),"名称不能为空!");
            //JVM断言
            assert user.getId()<=10000;
            return user;
        }
    
    }
    

    (4)Postman测试localhost:8080/user/save接口,post请求方式,json格式,传入不符合条件的数据直接就是400错误返回。

    Postman测试localhost:8080/user/save2接口,不符合的数据传入直接500。

    如果是name不符合条件,控制台会打出“名称不能为空!”信息。而采用JVM断言的id不符合条件,返回400。

    java.lang.IllegalArgumentException: 名称不能为空!
    	at org.springframework.util.Assert.hasText(Assert.java:284) ~[spring-core-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
    ...
    

    Spring Assert API &&JVM/java assert断言这两种方式有缺点就是:耦合了业务逻辑。网上关于耦合和解耦有个比较形象的解释:

    耦合

    有一对热恋中的男女,水深火热的,谁离开谁都不行了,离开就得死,要是对方有一点风吹草动,这一方就得地动山摇。可以按照琼瑶阿姨的路子继续想象,想成什么样都不过分,他们之间的这种状态就应该叫做“耦合”。

    解耦

    他们这么下去,有人看不惯了,有一些掌握话语权的权利机构觉得有必要出面阻止了,这样下去不是个事吖,你得先爱祖国,爱社会,爱人民,爱这大好河山才行啊,于是棒打鸳鸯,让他们之间对对方的需要,抽象成一种生理需要,这就好办了,把她抽象成女人,他抽象成男人,当他需要女人时,就把她当做女人送来,反之亦然,看上去他们仍在一起,没什么变化,实质上呢,他们已经被成功的拆散了,当有一天他需要女人时,来了另外一个女人,嘿嘿 他不会反对的。对方怎么变他也不会关心了。这就是“解耦”。

    虽然可以通过实现HandlerInterceptor做拦截或者Filter做拦截,但是也是较为恶心的。

    还可以通过AOP的方式,也可以提升代码的可读性。

    以上方式方法都有一个问题,那就是不是统一的标准。

    4.自定义Bean Validation

    我们以一个需求例子来演示自定义Bean Validation。

    需求:通过员工的卡号来校验,需要通过工号的前缀和后缀来判断。前缀必须以“GUPAO-”开头,后缀必须是数字。需要通过Bean Validator来校验。

    这里介绍一下Apahce的验证。可以在http://commons.apache.org/proper/commons-validator/apidocs/org/apache/commons/validator/package-summary.html#package_description可以找到各种验证。

    (1)首先我们仿造@Max内部实现来写一个Annotation:ValidCardNumber(为了保持统一,看看@Max导包package,我们也仿造它一波,即创建package#validation.constraints)

    import com.gupao.springbootbeanvalidation.validation.ValidCardNumberConstraintValidator;
    import javax.validation.Constraint;
    import java.lang.annotation.*;
    
    /**
     * @ClassName
     * @Describe 合法 卡号校验
     * @Need 需求:通过员工的卡号来校验,需要通过工号的前缀和后缀来判断。前缀必须以“GUPAO-”开头,后缀必须是数字。需要通过Bean Validator来校验。
     * @Author 66477
     * @Date 2020/5/1420:47
     * @Version 1.0
     */
    @Target({ ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(
            validatedBy = {ValidCardNumberConstraintValidator.class}
    )
    public @interface ValidCardNumber {
    }
    

    (2)然后呢,我们发现@Max注解定义上还有个@Constraint注解,点进去看发现它是继承了ConstraintValidator,那么再点进ConstraintValidator看一下,发现了一个叫做“ConstraintValidator<A extends Annotation, T>”的即可。那么接下来,我们编写一个自定义类(这里我取名叫做ValidCardNumberConstraintValidator)来实现ConstraintValidator<A extends Annotation, T>。

    在此之前需要用到一个依赖,我们需要用到里面判断是否为数字的方法StringUtils.isNumeric()。

    <dependency>
    			<groupId>org.apache.commons</groupId>
    			<artifactId>commons-lang3</artifactId>
    			<version>3.6</version>
    </dependency>
    

    附上代码:

    import com.gupao.springbootbeanvalidation.validation.constraints.ValidCardNumber;
    import org.apache.commons.lang3.ArrayUtils;
    import org.apache.commons.lang3.StringUtils;
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.util.Objects;
    
    /**
     * @ClassName
     * @Describe 自定义一个类实现ConstraintValidator<A extends Annotation, T>
     * @Author 66477
     * @Date 2020/5/1420:55
     * @Version 1.0
     */
    public class ValidCardNumberConstraintValidator implements ConstraintValidator<ValidCardNumber,String> {
    
        @Override
        public void initialize(ValidCardNumber constraintAnnotation) {
    
        }
    
        /**
         * @Need 需求:通过员工的卡号来校验,需要通过工号的前缀和后缀来判断。前缀必须以“GUPAO-”开头,后缀必须是数字。需要通过Bean Validator来校验。
         * @param value
         * @param constraintValidatorContext
         * @return
         */
        @Override
        public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
            //前半部分和后半部分
            //String[] parts = StringUtils.delimitedListToStringArray(value,"-");
            String[] parts = StringUtils.split(value,"-");
            //为什么一般不用String#split方法?原因在于该方法使用了正则表达式 这里因为StringUtils用了两处,只能选择一种
            //其次是NPE保护不够
            //如果在依赖中没没有StringUtils.delimitedListToStringArray API的话呢,可以使用
            //Apache commons-lang StringUtils
            // jdk里的StringTokenizer(不足之处在于它类似于Enumeration API)
           /* if(parts.length!=2){
                return false;
            }*/
            if(ArrayUtils.getLength(parts)!=2){
                return false;
            }
    
            String prefix = parts[0];
            String suffix = parts[1];
    
            //boolean isValidPrefix = "GUPAO".equals(prefix);
            boolean isValidPrefix = Objects.equals(prefix,"GUPAO");
            boolean isValidInteger = StringUtils.isNumeric(suffix);
    
            return isValidPrefix&&isValidInteger;
        }
    }
    

    (3)好了,万事俱备,只欠注解。我们去User模型加上刚定义好还热乎的注解。首先别忘了还是要给cardNumber字段加上@NotNull,判断前提它不为空嘛,然后加上自己的@ValidCardNumbe。如下:

     @NotNull
     @ValidCardNumber
     private String cardNumber;
    

    (4)重启项目,掏出Postman,还是访问之前写的http://localhost:8080/user/save接口。

    先故意来个错的

    这里发现控制台出现了这个错误,意思大概是不包含一个message参数:

    javax.validation.ConstraintDefinitionException: HV000074: com.gupao.springbootbeanvalidation.validation.constraints.ValidCardNumber contains Constraint annotation, but does not contain a message parameter.
    	at org.hibernate.validator.internal.metadata.core.ConstraintHelper.assertMessageParameterExists(ConstraintHelper.java:1054) ~[hibernate-validator-6.1.4.Final.jar:6.1.4.Final]
    

    那么回去看看代码,发现人家@Max好像是有个message的东东。

    public @interface Max {
        String message() default "{javax.validation.constraints.Max.message}";//就是它
    
        Class<?>[] groups() default {};
    

    我们把它加到ValidCardNumber自定义注解中,default就不要了,我们另外在模型User中定义它的属性即可。剩余几个属性也一起复制过来,因为它必然还会发生这种少参数的错误。

    public @interface ValidCardNumber {
        String message() ;
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    

    User模型的cardNumber这么搞就对了。

    @NotNull
    @ValidCardNumber(message = "卡号必须以"GUPAO" 开头,以数字结尾")
    private String cardNumber;
    

    再次测试一波无法通过验证的数据,结果终于是400了,但是控制台却没有返回我刚刚设置的消息,这个还有待于研究:

    然后来个对的,Ok,正常,通过验证!

    那么补充一点,信息提示的国际化该如何实现。

    (1)我们先在ValidCardNumber重新定义message默认值

        String message() default "{com.gupao.bean.validation.invalid.card.number.message}";
    

    (2)然后在resource下分别创建两个文件,文件名就用这个不要更改。

    a.ValidationMessages.properties

    com.gupao.bean.validation.invalid.card.number.message=The card number must start with "GUPAO",and its suffix must be a number!
    

    b.ValidationMessages_zh_CN.properties

    com.gupao.bean.validation.invalid.card.number.message=卡号必须以"GUPAO" 开头,以数字结尾
    

    (3)去掉User模型上cardNumber注解的message定义。

     @NotNull
     @ValidCardNumber
     private String cardNumber;
    

    注意注意注意,这里pom文件中一定要切换成SpringMVC,WebFlux可能导致无结果返回,他两的实现方式有差异,至于什么导致WebFlux控制台不输出错误信息,过于深入,这里暂时不做研究了。

    切换成SpringMVC结果:

    2020-05-14 22:47:09.743  WARN 177284 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.gupao.springbootbeanvalidation.domain.User com.gupao.springbootbeanvalidation.web.controller.UserController.save(com.gupao.springbootbeanvalidation.domain.User): [Field error in object 'user' on field 'cardNumber': rejected value [GUPAO]; codes [ValidCardNumber.user.cardNumber,ValidCardNumber.cardNumber,ValidCardNumber.java.lang.String,ValidCardNumber]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.cardNumber,cardNumber]; arguments []; default message [cardNumber]]; default message [¿¨ºÅ±ØÐëÒÔ"GUPAO" ¿ªÍ·£¬ÒÔÊý×Ö½áβ]] ]
    

    貌似有乱码,我们就处理一下吧。

    首先,打开cmd,cd到jdk的bin目录下,执行以下语句,用jdk工具相当于给这个文件重新编码一波。(笨方法,要么直接百度在线工具也行)

    C:Program FilesJavajdk1.6.0_45in>native2ascii.exe E:WorkplacesIDEAWorkplacewk-microservicespring-boot-bean-validationsrcmain
    esourcesValidationMessages_zh_CN.properties E:WorkplacesValidationMessages_zh_CN.properties
    

    生成文件到E:WorkplacesValidationMessages_zh_CN.properties,替换了就ok。这就编码后的文件内容,白嫖这个也行:

    com.gupao.bean.validation.invalid.card.number.message=u5361u53f7u5fc5u987bu4ee5"GUPAO" u5f00u5934uff0cu4ee5u6570u5b57u7ed3u5c3e
    

    再次测试,ok,控制台返回值正常了。

    Validation failed for argument [0] in public com.gupao.springbootbeanvalidation.domain.User com.gupao.springbootbeanvalidation.web.controller.UserController.save(com.gupao.springbootbeanvalidation.domain.User): [Field error in object 'user' on field 'cardNumber': rejected value [GUPAO]; codes [ValidCardNumber.user.cardNumber,ValidCardNumber.cardNumber,ValidCardNumber.java.lang.String,ValidCardNumber]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.cardNumber,cardNumber]; arguments []; default message [cardNumber]]; default message [卡号必须以"GUPAO" 开头,以数字结尾]] ]
    

    二、问题总结

    1.Json校验如何搞?

    解答:尝试让它变成Bean的方式。

    2.实际中很多参数都要校验,那时候怎么写这样写会增加很多类?

    解答:确实会增加部分工作量,大多数场景,不需要自定义,除非很特殊的情况。Bean Validation的主要缺点就是单元测试不方便。

    3.如何将400错误变成200?(这个有问题,先不要看,等后面研究后补充上来)

    (1)编写一个拦截器UserControllerInterceptor(或者使用过滤器Filter也可以)

    import org.springframework.http.HttpStatus;
    import org.springframework.lang.Nullable;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * @ClassName
     * @Describe TODO
     * @Author 66477
     * @Date 2020/5/1322:50
     * @Version 1.0
     */
    public class UserControllerInterceptor implements HandlerInterceptor {
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //把校验逻辑存放在这里
            return true;
        }
    
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
            Integer status = response.getStatus();
            if(status == HttpStatus.BAD_REQUEST.value()){
                response.setStatus(HttpStatus.OK.value());
            }
        }
    
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        }
    }
    

    (2)启动类add这个拦截器。

    @SpringBootApplication
    public class SpringBootBeanValidationApplication implements WebMvcConfigurer {
    
    	public static void main(String[] args) {
    		SpringApplication.run(SpringBootBeanValidationApplication.class, args);
    	}
    
    	public void addInterceptors(InterceptorRegistry registry) {
    		registry.addInterceptor(new UserControllerInterceptor);
    	}
    
    }
    

    4.如果前端固定表单的话,这种校验方式很好,但是灵活性不够,如果表单是动态的话,如何校验呢?

    解答:表单字段与Form对象绑定即可,再走Bean Validation逻辑。

    <form action="" method="POST" command="form">
    	<input value = "${form.name}"/>
    	...
    	<input value = "${form.age}"/>
    </form>
    

    或者就是采用普通的一个接着一个验证,责任链模式(Pipeline):

    filed1->filed2->filed3->compute->result

    5.如何自定义返回格式?如何最佳实现?

    解答:可以通过REST来实现,比如XML或者JSON的格式(视图)。

  • 相关阅读:
    死信队列消息原因排查
    MQ中间件死信队列深度不断增加问题解决案例
    DB2 57016报错的解决办法(表状态不正常,导致表无法操作)
    万门大学--童哲
    eclipse jvm配置
    weblogic threadpool has stuck threads
    8-10 ObserveableCommand演示
    8-9 四种执行方式区别讲解
    8-8 toObserve两种形态演示
    8-7 Observe两种形态演示
  • 原文地址:https://www.cnblogs.com/xusp/p/12892243.html
Copyright © 2020-2023  润新知