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

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

问题

如何基于 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验证的常用注解

9.Validator验证的常用注解

补充资料:元注解

元注解就是定义注解的注解,是 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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值