【Java】参数校验与统一异常处理

您所在的位置:网站首页 java使用注解优雅地实现参数校验的方法有哪些 【Java】参数校验与统一异常处理

【Java】参数校验与统一异常处理

#【Java】参数校验与统一异常处理| 来源: 网络整理| 查看: 265

Java参数校验与统一异常处理

【前言】参数校验是接口开发不可或缺的环节,校验参数在以前基本上依靠大量的if/else控制语句来实现,后来可以使用反射+自定义注解的形式进行校验,但是复用性不是很好。其中,《阿里开发手册》关于参数校验的规约的描述: 在这里插入图片描述

如何优雅地校验接口参数的合法性?spring开发了validated框架用于注解校验,可以节省很多冗余的校验实现逻辑细节,为开发和代码维护提升效能。

PART1.基本概念

JSR-303 JSR是Java Specification Requests的缩写(Java 规范提案)。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。 JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,是Java定义的一套基于注解的数据校验规范。

Hibernate Validator

Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

其pom引用为:

javax.validation validation-api 2.0.1.Final jakarta.validation jakarta.validation-api 2.0.1 org.hibernate.validator hibernate-validator 6.1.5.Final

它们的关系是,Hibernate Validator 是 Bean Validation 的实现,除了JSR规范中的,还加入了它自己的一些constraint实现,所以点开pom发现Hibernate Validator依赖了validation-api。 因为18年Java EE改名Jakarta EE,所以jakarta.validation是javax.validation改名而来。 对于spring boot应用,可以直接引用它提供的starter

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

spring boot有它的版本号配置,继承了spring boot的pom,所以不需要自己指定版本号了。 这个starter它内部也依赖了Hibernate Validator。Hibernate Validator内部依赖了jakarta.validation-api。

PART2.Validator参数校验

实现方式和使用方式:一般使用较多的是两个注解:@Validated、@Valid

@valid和@validate的区别

区别@Valid@Validated提供者JSR-303规范Spring是否支持分组不支持支持标注位置METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USETYPE, METHOD, PARAMETER嵌套校验支持不支持

ElementType.TYPE:能修饰类、接口或枚举类型 ElementType.FIELD:能修饰成员变量 ElementType.METHOD:能修饰方法 ElementType.PARAMETER:能修饰参数 ElementType.CONSTRUCTOR:能修饰构造器 ElementType.LOCAL_VARIABLE:能修饰局部变量 ElementType.ANNOTATION_TYPE:能修饰注解 ElementType.PACKAGE:能修饰包

TYPE_USE则可以用于标注任意类型(不包括class)

具体校验功能常用注解如下:

Constraint说明支持的数据类型@AssertFalse被注释的元素必须为 falseBoolean@AssertTrue被注释的元素必须为 trueBoolean@DecimalMax被注释的元素必须是一个数字,其值必须小于等于指定的最大值BigDecimal, BigInteger, CharSequence, byte, short, int, long@DecimalMin被注释的元素必须是一个数字,其值必须大于等于指定的最小值BigDecimal, BigInteger, CharSequence, byte, short, int, long@Max被注释的元素必须是一个数字,其值必须小于等于指定的最大值BigDecimal, BigInteger, byte, short, int, long@Min被注释的元素必须是一个数字,其值必须大于等于指定的最小值BigDecimal, BigInteger, byte, short, int, long@Digits(integer=,fraction=)检查注释的值是否为最多为整数位(integer)和小数位(fraction)的数字BigDecimal, BigInteger, CharSequence, byte, short, int, long@Email被注释的元素必须是电子邮箱地址,可选参数 regexp和flag允许指定必须匹配的附加正则表达式(包括正则表达式标志)CharSequence@Future被注释的元素必须是一个将来的日期Date,Calendar,Instant,LocalDate等@FutureOrPresent被注释的元素必须是一个将来的日期或现在的日期Date,Calendar,Instant,LocalDate等@Past被注释的元素必须是一个过去的日期Date,Calendar,Instant,LocalDate等@PastOrPresent被注释的元素必须是一个过去的日期或现在的日期Date,Calendar,Instant,LocalDate等@NotBlank被注释的元素不为null,并且去除两边空白字符后长度大于0CharSequence@NotEmpty被注释的元素不为null,并且集合不为空CharSequence, Collection, Map, arrays@NotNull被注释的元素不为nullAny type@Null被注释的元素为nullAny type@Pattern(regex=, flags=)被注释的元素必须与正则表达式 regex 匹配CharSequence@Size(min=, max=)被注释的元素大小必须介于最小和最大(闭区间)之间CharSequence, Collection, Map,arrays

接下来对参数校验的实践进行一个简单的demo:

定义一个参数实体类,对需要校验的参数加上对应的注解 @Data public class UserInfo { @NotBlank(message = "姓名不能为空") public String name; @Min(value = 10, message = "年龄不得少于10岁") public int age; }

​ 2.定义一个Controller类,对入参加上@Validated注解进行参数校验

@RestController @Slf4j public class TestController { @PostMapping("getInfo") public String test(@RequestBody @Validated UserInfo userInfo) { log.info("入参:【{}】", userInfo); return "success"; } }

请求结果如下: 在这里插入图片描述

PART3.分组校验

有时候我们多个接口会复用一个参数对象,里面的参数在A接口中是必填的,在B接口中又是非必填的。

诸如此类的场景就需要使用分组校验的方法,分组校验的灵活搭配,足以应付大部分的场景。

Validator中有一个groups的概念,通过该参数可以帮助我们指定不同的校验分组。

Validated有自己默认的组 Default.class,当我们需要在默认校验的基础上,增加新的分组校验时,建议继承Default,因为默认的groups就是groups = {Default.class}。

在这里插入图片描述

Step1.设置分组接口

我们要建的组,就是不同业务使用字段分成的组,举例的业务是一个用户对象,用户有不同的角色,不同的接口会用到这个用户对象的不同字段。比如学生(Student),老师(Teacher):

public interface Student { } public interface Teacher { }

Step2.在需要分组校验的参数上加上groups

@Data public class UserInfo { @NotBlank(message = "姓名不能为空") public String name; @Min(value = 10, message = "年龄不得少于10岁", groups = {Default.class}) public int age; @NotBlank(message = "手机号不能为空", groups = {Teacher.class}) public String phone; @NotEmpty(message = "授课科目不能为空", groups = {Teacher.class}) @Size(min = 2, message = "必须至少两个科目", groups = {Teacher.class}) private List subjects; }

Step3.在@Validated上加入对应的分组

@RestController @Slf4j public class TestController { @PostMapping("getInfo") public String test(@RequestBody @Validated({Teacher.class}) UserInfo userInfo) { log.info("入参:【{}】", userInfo); return "success"; } }

ps.分组继承校验:

自定义的分组可以使用继承方式进行校验,比如我们将很多个分组封装到一个特定的分组里面,方便我们自由组合,多个自定义分组下面请看如下的案例:

public interface GroupsOpration extends GroupUpdate{ } public interface GroupUpdate extends Default { } public interface GroupDel extends Default { } public interface GroupAdd extends Default { } PART4.对象校验

有时候我们接口的入参里包含一个集合对象元素,这个对象的各个属性也是要分别进行校验的,针对这个场景,我们可以结合@Valid注解,来进行嵌套校验,示例如下:

Step1.重新定义一个入参UsersVo,里面包含由@Valid注解标注的userInfoList这个集合属性,其中每一个元素UserInfo对象的校验约束参考前文的定义:

@Data public class UsersVo { @NotBlank(message = "ID不能为空") public String id; @Valid @NotEmpty public List userInfoList; }

Step2.Controller层接口参数加@Validated注解,以实现嵌套校验的目的

@PostMapping("getInfos") public String test(@RequestBody @Validated UsersVo usersVo) { log.info("入参:【{}】", usersVo); return "success"; } PART5.自定义Validator

如果默认的注解规则无法满足业务需求,这时候validator提供了自定义注解的形式帮助开发者可以进行自定的规则校验。

Step1、定义自定义注解:

首先第一步是确定自己需要自定义的注解,比如我这里定义了一个检查姓名是否以Zake开头的注解

@Target({ElementType.FIELD}) // 可以注入的类型,字段和参数类型 @Retention(RUNTIME) // 运行时生效 @Constraint(validatedBy = {CheckNameValidator.class}) // 指定用于验证元素的验证器 public @interface CheckName { String message() default "姓名不正确"; //提示的信息 Class[] groups() default { }; //分组验证,例如只在新增时进行校验等 Class[] payload() default { }; }

三个属性message、groups、payload都是必须定义的,否则进行校验的时候,会抛出如下的错误

Step2、定义真实注解处理类:

需要实现接口ConstraintValidator,泛型的第一个参数为注解类,第二个参数为具体校验对象的类型

public class CheckNameValidator implements ConstraintValidator { // 初始化注解的校验内容 @Override public void initialize(CheckName constraintAnnotation) { ConstraintValidator.super.initialize(constraintAnnotation); } // 具体的校验逻辑 @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value.startsWith("zake"); } }

Step3、参数变量上加上注解

@Data public class UserInfo { @NotBlank(message = "姓名不能为空") @CheckName public String name; @Min(value = 10, message = "年龄不得少于10岁", groups = {Default.class}) public int age; @NotBlank(message = "手机号不能为空", groups = {Teacher.class}) public String phone; @NotEmpty(message = "授课科目不能为空", groups = {Teacher.class}) @Size(min = 2, message = "必须至少两个科目", groups = {Teacher.class}) private List subjects; } PART6.异常处理

上述实现了参数校验的目标,但是异常信息的提示是不友好的,关于异常处理还可以进一步进行优化。

如何处理validate异常信息?

BindingResult信息处理控制器进行特定异常处理统一异常处理 1、BindingResult信息处理

validate提供BindResult对象封装异常信息,需要将该对象 紧跟@Validated注解参数位置之后,注意一定要紧跟,否则是无法注入的,加入后,该对象在校验失败之后,BindResult对象里面封装的基本异常信息既可以由开发者自由处理了。

@PostMapping("getInfo") public String test(@RequestBody @Validated({Teacher.class}) UserInfo userInfo, BindingResult result) { log.info("入参:【{}】", userInfo); if (result.hasErrors()) { FieldError fieldError = result.getFieldError(); String field = fieldError.getField(); String msg = fieldError.getDefaultMessage(); return field + ":" + msg; // 异常信息的处理返回 } return "success"; } 2、控制器进行特定异常处理

一般这种方式使用的比较少,在有全局异常处理的情况下,很少在Controller层进行异常处理,某些特殊情况可以用到。

该方式和全局异常处理器类似,只不过定义方法修改到了对应的Controller控制器层。

@RestController @Slf4j public class TestController { @PostMapping("getInfo") public String test(@RequestBody @Validated({Teacher.class}) UserInfo userInfo) { log.info("入参:【{}】", userInfo); return "success"; } /** 在控制器层处理异常信息,仅仅适用于当前控制器 */ @ExceptionHandler(MethodArgumentNotValidException.class) public Object processException(MethodArgumentNotValidException e){ log.error(e.getMessage()); return e.getAllErrors().get(0).getDefaultMessage(); } } 3、统一异常处理(推荐)

全局统一异常处理是最常用的处理手段,该方法将异常信息组装自定义的结果,也可以使用用来做日志记录和处理。处理方式如下:

Step1、新建全局统一异常处理类,在类名标注:@ControllerAdvice或者@RestControllerAdvice,分别对应Controller层注解@Controller和@RestController。

@ControllerAdvice -> @Controller @RestControllerAdvice -> @RestController

Step2、在全局统一异常处理类中对应的方法内部,使用@ExceptionHandler进行方法标注,@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常。当异常发生时,Spring会选择最接近抛出异常的处理方法。

代码示例如下:

定义错误码枚举

@Getter public enum ResultCodeEnum { SUCCESS(200, "成功"), FAILED(1001,"失败"), ERROR(500, "系统内部错误"); private int code; private String message; ResultCodeEnum(int code, String message) { this.code = code; this.message = message; } }

定义统一的响应格式:

@Data public class ResultInfo { public int code; public String message; public T data; public ResultInfo(T data){ this.code = ResultCodeEnum.SUCCESS.getCode(); this.message = ResultCodeEnum.SUCCESS.getMessage(); this.data = data; } public ResultInfo(int code, String msg) { this.code = code; this.message = msg; } }

全局异常处理器增加对异常信息的拦截,并修改异常时返回的数据格式。

@RestControllerAdvice public class GlobalExceptionHandlerAdvice { @ExceptionHandler(MethodArgumentNotValidException.class) public ResultInfo methodArgumentNotValidException(MethodArgumentNotValidException e) { // 从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); // 然后提取错误提示信息进行返回 return new ResultInfo(ResultCodeEnum.FAILED.getCode(), objectError.getDefaultMessage()); } @ExceptionHandler(Exception.class) public ResultInfo ExceptionHandler(Exception e) { return new ResultInfo(ResultCodeEnum.ERROR.getCode(), ResultCodeEnum.ERROR.getMessage()); } }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3