如何基于 Spring Boot 实现接口参数验证及全局异常处理

  • 如何基于 Spring Boot 实现接口参数验证及全局异常处理

    • 问题

    • 方案

    • 步骤:构建一个Spring Boot项目

    • 步骤:集成 Knife4j 提供 Swagger 接口文档服务

    • 步骤:实现全局统一异常处理

    • 步骤:Hibernate Validator 自定义验证注解

    • 步骤:创建控制器使用参数验证

    • 步骤:启动服务验证参数验证及全局异常处理

    • 问题处理

    • 补充资料:使用控制器实现全局统一异常处理

    • 补充资料:使用 Hibernate Validator 验证数据

    • 补充资料:元注解

    • 代码仓库

问题

如何基于 Spring Boot 实现接口参数校验及全局异常处理。

包括:接口参数校验、自定义注解及数据验证功能、全局异常处理、自定义异常处理、异常返回规范整理。

方案

  • 使用 Knife4j 提供 Swagger 接口文档服务。

  • 基于 Spring 的控制器通知( @ControllerAdvice 或 @RestControllerAdvice )实现全局统一异常处理。

  • 使用 hibernate-validator 实现接口数据验证;

  • 通过自定义注解类、自定义验证业务逻辑实现自定义验证功能;

步骤:构建一个Spring Boot项目

可参考之前的文章《如何快速构建一个Spring Boot项目》 快速构建一个 Spring Boot 项目。

使用 IDEA 的 Spring Initializr 构建 Spring Boot 项目。

选择 Spring Boot 版本 2.1.18 。

项目构建工具选择 Maven 。

步骤:集成 Knife4j 提供 Swagger 接口文档服务

添加依赖

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.5</version>
</dependency>

添加 Swagger 配置类

@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
//是否开启 (true 开启 false隐藏。生产环境建议隐藏)
//.enable(false)
.select()
//扫描的路径包,设置basePackage会将包下的所有被@Api标记类的所有方法作为api
.apis(RequestHandlerSelectors.basePackage("com.chen.solution.validator.demo.controller"))
//指定路径处理PathSelectors.any()代表所有的路径
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//设置文档标题(API名称)
.title("SpringBoot中使用Swagger2接口规范")
//文档描述
.description("接口说明")
//服务条款URL
.termsOfServiceUrl("")
//版本号
.version("1.0.0")
.build();
}
}

步骤:实现全局统一异常处理

Spring 提供了一个非常方便的异常处理方案–控制器通知@ControllerAdvice 或 @RestControllerAdvice),它将所有控制器作为一个切面,利用切面技术来实现。

定义统一响应编码

public enum ResultCode {
SUCCESS(HttpServletResponse.SC_OK, "Operation is Successful"),

FAILURE(HttpServletResponse.SC_BAD_REQUEST, "Biz Exception"),

UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "Request Unauthorized"),

NOT_FOUND(HttpServletResponse.SC_NOT_FOUND, "404 Not Found"),

//...

final int code;

final String msg;
}

定义统一返回格式

public class BaseResponse {

/** 返回结果码 */
@Builder.Default
private Integer code = ResultCode.SUCCESS.code;

/** 返回结果码描述 */
@Builder.Default
private String desc = ResultCode.SUCCESS.msg;

/** 返回错误描述 */
private String message;

/** 时间戳 */
@Builder.Default
private long timestamp = System.currentTimeMillis();

public boolean checkIsSuccess() {
return code == ResultCode.SUCCESS.code;
}
}

创建全局统一异常处理类

@RestControllerAdvice
public class GlobalExceptionTranslator {

@ExceptionHandler(MissingServletRequestParameterException.class)
public BaseResponse handleError(MissingServletRequestParameterException e) {
log.warn("Missing Request Parameter", e);
String message = String.format("Missing Request Parameter: %s", e.getParameterName());
return BaseResponse
.builder()
.code(ResultCode.PARAM_MISS.getCode())
.desc(ResultCode.PARAM_MISS.getMsg())
.message(message)
.build();
}

//...

@ExceptionHandler(Throwable.class)
public BaseResponse handleError(Throwable e) {
log.error("Internal Server Error", e);
return BaseResponse
.builder()
.code(ResultCode.INTERNAL_SERVER_ERROR.getCode())
.desc(ResultCode.INTERNAL_SERVER_ERROR.getMsg())
.message(e.getMessage())
.build();
}
}

步骤:Hibernate Validator 自定义验证注解

创建一个约束注解

@Documented
@Constraint(validatedBy = DayOfWeekValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DayOfWeek {
String message() default "Unknown day of week";
Class[] groups() default {};
Class[] payload() default {};
}
  • message :代表着约束默认的信息,当违反约束产生错误消息的时候,返回默认值。在声明约束时,可以给 message 赋值,以此覆盖默认的信息。

  • groups :分组约束,分组可以在验证期间限制应用一组约束。

  • payload :Bean Validation API的客户端程序使用该属性去给约束指派自定义的payload,API本身不使用该属性。

实现一个约束注解对应的Validator

public class DayOfWeekValidator implements ConstraintValidator<DayOfWeek, String>  {

private List<String> daysOfWeek =
Arrays.asList("sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday");

@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
// can be null
if (value == null) {
return true;
}
String input = value.trim().toLowerCase();
if (daysOfWeek.contains(input)) {
return true;
}
return false;
}
}

步骤:创建控制器使用参数验证

创建实体类,添加验证注解

public class CompanyDto {
@NotBlank(groups = {Group1.class})
private String id;

@NotBlank(groups = {Group1.class, Group2.class})
private String name;

@Email(message = "Invalid email")
private String email;

@Timezone(groups = {Group1.class, Group2.class})
@NotBlank(groups = {Group1.class, Group2.class})
private String defaultTimezone;

@DayOfWeek(groups = {Group1.class, Group2.class})
@NotBlank(groups = {Group1.class, Group2.class})
private String defaultDayWeekStarts;
}

在控制器类上添加注解 @Validated 开启参数验证,使用 @Valid 指定要校验的参数对象,使用@Validated 实现分组校验。

@Slf4j
@RestController
@RequestMapping("/v1/company")
@Validated
public class CompanyController {

@PostMapping(path = "/create")
public GenericCompanyResponse createCompany(@RequestBody @Validated({Group2.class}) CompanyDto companyDto) {
log.info("name:{}", companyDto.getName());
companyDto.setId(UUID.randomUUID().toString());
return new GenericCompanyResponse(companyDto);
}

//...

@PutMapping(path= "/update")
public GenericCompanyResponse updateCompany(@RequestBody @Validated({Group1.class}) CompanyDto companyDto) {
log.info("id:{},name:{}", companyDto.getId(), companyDto.getName());
CompanyDto updatedCompanyDto = companyDto;
return new GenericCompanyResponse(updatedCompanyDto);
}
}

步骤:启动服务验证参数验证及全局异常处理

启动服务后,使用浏览器访问 http://localhost:8083/doc.html ,使用 Swagger 接口文档进行测试。

测试数据

/v1/account/getOrCreate
{
"phoneNumber": "13845325678",
"name": "chen",
"email": "xxx@sina.com"
}

/v1/company/create
{
"name": "chen",
"defaultDayWeekStarts": "sunday",
"defaultTimezone": "CTT"
}

/v1/company/update
{
"id": "123",
"name": "chen",
"defaultDayWeekStarts": "sunday",
"defaultTimezone": "CTT"
}

问题处理

全局异常注解 @RestControllerAdvice 处理类未生效

全局异常注解 @RestControllerAdvice 处理类,一般放到 common 模块中,以便各微服务共同使用,有可能未被 Spring 扫描到,导致全局异常处理未生效。

检查异常处理类是否被Spring管理,@SpringbootApplication 默认扫描本包和子包;如果未扫描到,使用 @SpringbootApplication(scanBasePackages="xxx.xxx") 。


Spring Boot 集成 Knife4j 报错

使用 Knife4j 2.0.6及以上的版本,Spring Boot的版本必须大于等于2.2.x 。

详见:https://xiaoym.oschina.io/knife4j/documentation/changelog.html

本项目实践采用的是 Spring Boot 2.1.18 ,故选择 Knife4j 2.0.5 版本。

补充资料:使用控制器实现全局统一异常处理

Spring 提供了一个非常方便的异常处理方案–控制器通知(@ControllerAdvice 或 @RestControllerAdvice),它将所有控制器作为一个切面,利用切面技术来实现。

通过其可以对异常进行全局统一处理,默认对所有 Controller 有效。如果限定生效范围,则可以使用 @ControllerAdvice 支持的限定范围方式。

  • 按注解: @ControllerAdvice(annotations=RestController.class) 。

  • 按包名: @ControllerAdvice("com.chen.controller") 。

  • 按类型: @ControllerAdvice(assignableTypes={ControllerInterface.class,AbstractController.class}) 。

这是 @ControllerAdvice 进行统一异常处理的优点,它能够细粒度地控制该异常处理器针对哪些 Controller、包或类型有效。

可以利用这一特性在一个系统实现多个异常处理器,然后 Controller 可以有选择地决定使用哪个,使得异常处理更加灵活、降低侵入性。

异常处理类会包含以下一个或多个注解标注的方法:

  • @ExceptionHandler:定义控制器发生异常后的操作,可以拦截所有控制器发生的异常。

  • @InitBinder:对表单数据进行绑定,用于定义控制器参数绑定规则。如转换规则、格式化等。可以通过这个注解的方法得到 WebDataBinder 对象,它在参数转换之前被执行。

  • @ModelAttribute:在控制器方法被执行前,对所有 Controller 的 Model 添加属性进行操作。

补充资料:使用 Hibernate Validator 验证数据

Hibernate-validator 可实现对数据的验证,它是对 JSR(Java Specification Requests) 标准的实现。

Validator验证的常用注解

如何基于 Spring Boot 实现接口参数验证及全局异常处理

补充资料:元注解

元注解就是定义注解的注解,是 Java 提供的用于定义注解的基本注解

元注解:

  • @Retention 是注解类,实现声明类 Class ,声明类别 Category ,声明扩展 Extension

  • @Target 放在自定义注解的上边,表明该注解可以使用的范围

  • @Inherited 允许子类继承父类的注解,在子类中可以获取使用父类注解

  • @Documented 表明这个注解是由 Javadoc 记录的

  • @interface 用来自定义注解类型

  • @Repeatable 表示这个声明的注解是可重复的


@Target标注作用范围

@Target 该注解的作用是告诉 Java 将自定义的注解放在什么地方,比如类、方法、构造器、变量上等。它是一个枚举类型,有如下值:

  1. @Target(ElementType.CONSTRUCTOR) 用于描述构造器

  2. @Target(ElementType.FIELD) 用于描述成员变量、对象、属性、枚举的常量

  3. @Target(ElementType.LOCAL_VARIABLE) 用于描述局部变量

  4. @Target(ElementType.METHOD) 用于描述方法

  5. @Target(ElementType.PACKAGE) 用于描述包

  6. @Target(ElementType.PARAMETER) 用于描述方法参数

  7. @Target(ElementType.TYPE) 用于描述接口、类、枚举、注解

  8. @Target(ElementType.ANNOTATION_TYPE) 用于描述注解

使用多个作用范围 @Target({ElementType.METHOD,ElementType.TYPE}) 。


@Retention 标注生命周期

该注解用于说明自定义注解的生命周期,在注解中有三个生命周期:

  1. @Retention(RetentionPolicy.RUNTIME) 始终不会丢弃,运行期也保留该注解,可以使用反射机制读取该注解的信息。自定义的注解通常使用这种方式。

  2. @Retention(RetentionPolicy.CLASS) 类加载时丢弃,默认使用这种方式。

  3. @Retention(RetentionPolicy.SOURCE) 编译阶段丢弃,注解在编译结束之后就不再有意义。所以它们不会写入字节码。 @Override 、 @SuppressWarnnings都属于这类注解。


@Inherited

该注解是一个标记注解,表明被标注的类型是可以被继承的。如果一个使用了 @Inherited 修饰的 Annotation 类型被用于一个 Class ,则这个 Annotation 将被用于该 Class 的子类。

subClazz.getAnnotations()) 可以获取到自身和其父类的注解。

subClazz.getDeclaredAnnotations() 只获取自身的注解。


@Documented

表示将注解信息添加到 Java 文档中。


@Repeatable

表示这个声明的注解是可重复的。 @Repeatable 的值是另一个注解,其可以通过这个另一个注解的值来包含这个可重复的注解。

约束:

  • @Repeatable 声明的注解,其元注解 @Target的使用范围要比 @Repeatable 的值声明的注解中的 @Target 的范围要大或相同,否则编译器错误

  • @Repeatable 声明的注解,其元注解 @Retention 的生命周期要比 @Repeatable 的值声明的注解中的 @Retention 的要小或相同。生命周期:SOURCE(源码) < CLASS (字节码) < RUNTIME(运行) 。


@interface

该注解用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。

方法名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。

可以通过 default 来声明参数的默认值。

代码仓库

https://gitee.com/chentian114/solution-springboot

公众号

原文始发于微信公众号(知行chen):如何基于 Spring Boot 实现接口参数验证及全局异常处理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/45909.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!