热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

微服务之路(三)springboot验证

前言主要议题BeanValidation(JSR-303):介绍JavaBean验证、核心API、实现框架HibernateValidatorApachecommons-valid

前言

主要议题

  • Bean Validation(JSR-303):介绍Java Bean验证、核心API、实现框架Hibernate Validator

  • Apache commons-validator:介绍最传统Apache通用验证器框架,如:长度、邮件等方式。

  • Spring Validator:介绍Spring内置验证器API、以及自定义实现。


主体内容

一、Bean Validation


JSR-303


1.Maven依赖


org.springframework.boot
spring-boot-starter-validation


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”的即可。那么接下来,我们编写一个自定义类(这里我取名叫做ValidCardNumberConstraintValidator)来实现ConstraintValidator

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


org.apache.commons
commons-lang3
3.6

附上代码:

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

* @Author 66477
* @Date 2020/5/1420:55
* @Version 1.0
*/
public class ValidCardNumberConstraintValidator implements ConstraintValidator {
@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[] 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 Files\Java\jdk1.6.0_45\bin>native2ascii.exe E:\Workplaces\IDEAWorkplace\wk-microservice\spring-boot-bean-validation\src\main\resources\ValidationMessages_zh_CN.properties E:\Workplaces\ValidationMessages_zh_CN.properties

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

com.gupao.bean.validation.invalid.card.number.message=\u5361\u53f7\u5fc5\u987b\u4ee5"GUPAO" \u5f00\u5934\uff0c\u4ee5\u6570\u5b57\u7ed3\u5c3e

再次测试,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逻辑。



...


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

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

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

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



推荐阅读
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 本文深入探讨了Java多线程环境下的同步机制及其应用,重点介绍了`synchronized`关键字的使用方法和原理。`synchronized`关键字主要用于确保多个线程在访问共享资源时的互斥性和原子性。通过具体示例,如在一个类中使用`synchronized`修饰方法,展示了如何实现线程安全的代码块。此外,文章还讨论了`ReentrantLock`等其他同步工具的优缺点,并提供了实际应用场景中的最佳实践。 ... [详细]
  • 本文深入解析了Java面向对象编程的核心概念及其应用,重点探讨了面向对象的三大特性:封装、继承和多态。封装确保了数据的安全性和代码的可维护性;继承支持代码的重用和扩展;多态则增强了程序的灵活性和可扩展性。通过具体示例,文章详细阐述了这些特性在实际开发中的应用和优势。 ... [详细]
  • 提升Android开发效率:Clean Code的最佳实践与应用
    在Android开发中,提高代码质量和开发效率是至关重要的。本文介绍了如何通过Clean Code的最佳实践来优化Android应用的开发流程。以SQLite数据库操作为例,详细探讨了如何编写高效、可维护的SQL查询语句,并将其结果封装为Java对象。通过遵循这些最佳实践,开发者可以显著提升代码的可读性和可维护性,从而加快开发速度并减少错误。 ... [详细]
  • 在本文中,我们将为 HelloWorld 项目添加视图组件,以确保控制器返回的视图路径能够正确映射到指定页面。这一步骤将为后续的测试和开发奠定基础。首先,我们将介绍如何配置视图解析器,以便 SpringMVC 能够识别并渲染相应的视图文件。 ... [详细]
  • 在List和Set集合中存储Object类型的数据元素 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • 在Java Web服务开发中,Apache CXF 和 Axis2 是两个广泛使用的框架。CXF 由于其与 Spring 框架的无缝集成能力,以及更简便的部署方式,成为了许多开发者的首选。本文将详细介绍如何使用 CXF 框架进行 Web 服务的开发,包括环境搭建、服务发布和客户端调用等关键步骤,为开发者提供一个全面的实践指南。 ... [详细]
  • 本文介绍了如何利用ObjectMapper实现JSON与JavaBean之间的高效转换。ObjectMapper是Jackson库的核心组件,能够便捷地将Java对象序列化为JSON格式,并支持从JSON、XML以及文件等多种数据源反序列化为Java对象。此外,还探讨了在实际应用中如何优化转换性能,以提升系统整体效率。 ... [详细]
  • 使用 Vuex 管理表单状态:当输入框失去焦点时自动恢复初始值 ... [详细]
  • 本文总结了JavaScript的核心知识点和实用技巧,涵盖了变量声明、DOM操作、事件处理等重要方面。例如,通过`event.srcElement`获取触发事件的元素,并使用`alert`显示其HTML结构;利用`innerText`和`innerHTML`属性分别设置和获取文本内容及HTML内容。此外,还介绍了如何在表单中动态生成和操作``元素,以便更好地处理用户输入。这些技巧对于提升前端开发效率和代码质量具有重要意义。 ... [详细]
  • DRF框架中Serializer反序列化验证机制详解:深入探讨Validators的应用与优化
    在DRF框架的反序列化验证机制中,除了基本的字段类型和长度校验外,还常常需要进行更为复杂的条件限制校验。通过引入`validators`模块,可以实现自定义校验逻辑,如唯一字段校验等。本文将详细探讨`validators`的使用方法及其优化策略,帮助开发者更好地理解和应用这一重要功能。 ... [详细]
  • 如何在页面底部添加倾斜样式效果? ... [详细]
  • 【图像分类实战】利用DenseNet在PyTorch中实现秃头识别
    本文详细介绍了如何使用DenseNet模型在PyTorch框架下实现秃头识别。首先,文章概述了项目所需的库和全局参数设置。接着,对图像进行预处理并读取数据集。随后,构建并配置DenseNet模型,设置训练和验证流程。最后,通过测试阶段验证模型性能,并提供了完整的代码实现。本文不仅涵盖了技术细节,还提供了实用的操作指南,适合初学者和有经验的研究人员参考。 ... [详细]
author-avatar
mobiledu2402851377
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有