-
如何基于 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验证的常用注解
补充资料:元注解
元注解就是定义注解的注解,是 Java 提供的用于定义注解的基本注解。
元注解:
-
@Retention
是注解类,实现声明类 Class ,声明类别 Category ,声明扩展 Extension -
@Target
放在自定义注解的上边,表明该注解可以使用的范围 -
@Inherited
允许子类继承父类的注解,在子类中可以获取使用父类注解 -
@Documented
表明这个注解是由 Javadoc 记录的 -
@interface
用来自定义注解类型 -
@Repeatable
表示这个声明的注解是可重复的
@Target
标注作用范围
@Target
该注解的作用是告诉 Java 将自定义的注解放在什么地方,比如类、方法、构造器、变量上等。它是一个枚举类型,有如下值:
-
@Target(ElementType.CONSTRUCTOR)
用于描述构造器 -
@Target(ElementType.FIELD)
用于描述成员变量、对象、属性、枚举的常量 -
@Target(ElementType.LOCAL_VARIABLE)
用于描述局部变量 -
@Target(ElementType.METHOD)
用于描述方法 -
@Target(ElementType.PACKAGE)
用于描述包 -
@Target(ElementType.PARAMETER)
用于描述方法参数 -
@Target(ElementType.TYPE)
用于描述接口、类、枚举、注解 -
@Target(ElementType.ANNOTATION_TYPE)
用于描述注解
使用多个作用范围 @Target({ElementType.METHOD,ElementType.TYPE})
。
@Retention
标注生命周期
该注解用于说明自定义注解的生命周期,在注解中有三个生命周期:
-
@Retention(RetentionPolicy.RUNTIME)
始终不会丢弃,运行期也保留该注解,可以使用反射机制读取该注解的信息。自定义的注解通常使用这种方式。 -
@Retention(RetentionPolicy.CLASS)
类加载时丢弃,默认使用这种方式。 -
@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