摘要:Web 框架,全局异常、API 日志等。整合 Hibernate Validator 加强了参数验证,用
@Validated等注解实现参数验证,极大简化了代码,验证更简洁方便。通用返回和统一异常处理。
目录
[TOC]
分为 config、core 两部分。
统一配置
- configurePathMatch:设置 API 前缀,仅仅匹配 controller 包下的
- 配置API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀。
- 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
- 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
- GlobalExceptionHandler
- GlobalResponseBodyHandler
- 创建 CorsFilter Bean,解决跨域问题
- 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
- 创建 RestTemplate 实例
1 | |
参数校验
参考:Java 参数校验validation和validator区别;
使用场景
对 RESTful API 接口(Controller 接收的参数)、Service 接口的形参赋值等进行统一参数校验,以保证最终数据入库的正确性。
- 例如说,用户注册时,会校验手机格式的正确性,密码非弱密码。
- 如果参数校验不通过,会抛出 ConstraintViolationException 异常,被全局的异常处理捕获,返回“请求参数不正确”的响应。
绝大多数情况下,选用
Hibernate Validator注解。
方式有:
-
一般对于复杂的业务参数校验:可通过校验类单独的校验方法进行处理;
- 常规做法:业务逻辑中要(手动)加数据校验后,再把数据写入数据库;校验失败抛出
IllegalArgumentException; - 缺点:代码冗余臃肿复杂,每个人抛出的异常都不同。
- 常规做法:业务逻辑中要(手动)加数据校验后,再把数据写入数据库;校验失败抛出
-
通常对于与业务无关、简单的参数校验:可采用
javax.validation和hibernate-validator验证框架,通过注解的方式实现校验。-
Bean Validation 规范:是 Java 定义的一套基于注解的参数验证规范,属于 JSR(Java Specification Requests 规范)。
- 只是一项标准,规定了一些校验注解的规范,但没有实现,如
@Null、@NotNull、@Pattern等, - 位于
javax.validation.constraints包下;
- 只是一项标准,规定了一些校验注解的规范,但没有实现,如
-
hibernate validator框架:是对这个规范的实现。-
并增加了一些其他校验注解,如
@NotBlank、@NotEmpty、@Length等, -
位于
org.hibernate.validator.constraints包下。 -
不要以为 Hibernate 仅仅是一个 ORM 框架,这只是它的 Hibernate ORM 所提供的。
-
-
1 | |
1 | |
1 | |
Spring Validation
在使用 Spring 的项目中,因为 Spring Validation 提供了对 Bean Validation 的内置封装支持(底层调用 Hibernate Validator),可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。
- 在实现原理上,也是基于 Spring AOP 拦截,实现校验相关的操作。
友情提示:这一点,类似 Spring Transaction 事务,通过
@Transactional注解,实现声明式事务。
Hibernate 校验模式
- 普通模式(默认):校验所有属性,返回所有的验证失败信息;
- 快速失败返回模式:只要有一个校验失败就返回。
1 | |
@Valid VS @Validated
作用:用于验证注解是否符合要求、统一参数校验。
- 直接加在变量、参数之前,在变量中添加验证信息的要求(用
@NotNull等字段验证注解),当不符合要求时就会在方法中返回 message 中的错误提示信息。
用法:绝大多数场景下,使用 @Validated 注解即可。在有嵌套校验的场景,使用 @Valid 注解添加到成员属性上。
@Valid:
- 所属包:属于
javax.validation包下,是 Bean Validation 所定义的,由 JDK 提供; - 可标注在:构造器、方法返回、方法和方法参数、成员属性上;多了成员变量,这就导致,如果有嵌套对象时,只能使用
@Valid注解。 - 不支持分组验证;
- 级联校验/
嵌套验证:用于标注嵌套对象(包含其他对象的数组、集合、map、对象的引用)或其内部(另一对象)的成员属性,进行属性验证;- 用于内部时,外部可配合
@Valid或@Validated进行嵌套验证; - 在检查当前对象的同时也会检查该字段所引用的对象;
- 用于内部时,外部可配合
- 异常:如果验证失败,将抛出
MethodArgumentNotValidException。
@Validated:
比
@Valid更强大;
- 所属包:属于
org.springframework.validation.annotation包下,是 Spring Validation 所定义的,由 Spring 提供; - 可标注在:类、方法和方法参数上,不能用在成员属性上;
- 额外支持分组验证:在方法参数验证时,根据不同的分组采用不同的验证机制。
- 不同 Controller 对于同一 POJO 实体类的属性约束可能不一样。如,
- 当新增数据时,因为 id 在数据库里是自增字段,不支持手动赋值,需要 id 属性为空(
@Null); - 当更新数据时,需要 id 属性不为空(
@NotNull),才能确定更新哪一条记录。
- 当新增数据时,因为 id 在数据库里是自增字段,不支持手动赋值,需要 id 属性为空(
- 首先在实体类中定义组别接口进行区分,然后在注解中加上对应的组别接口,不加则是默认组别;如
@Validated({Item.add.class}) Item item? - 因为
@Valid不支持分组,所以用@Validated标注 Controller 方法的形参;并在@Validated中指定分组,不在指定组中的其他校验不会生效,还需把其他属性的校验一并加入到组中;
- 不同 Controller 对于同一 POJO 实体类的属性约束可能不一样。如,
- 嵌套验证:用于标注嵌套对象属性;内部需配合
@Valid进行嵌套验证; - Spring Validation 仅对
@Validated注解,实现声明式校验。
嵌套验证
1 | |
注解
字段验证注解:用于校验 Controller 方法中用
@Valid、@Validated修饰的参数(实体类对象)。
从定义位置分为:
javax.validation内置基础校验注解,由 JDK 提供。hibernate validator支持的注解,由 Spring 提供。
@NotNull、@NotEmpty、@NotBlank
@NotNull(message = "name不能为空"):元素不能为 null(必须有定义);不检查长度 size ;
@Null:元素必须为 null;JDK 提供的。:用于字符串、集合、Map、数组。不能为 null ** 或 长度@NotEmptysize != 0(String、Collection、Map 的isEmpty()方法);如username。Hibernate Validator 提供**的。:只用于 String,不仅不能为 null,而且至少包含一个非空白字符(@NotBlanktrim()后 size>0)。Hibernate Validator 提供的。如手机号。
1 | |
| @NotNull | @NotEmpty | @NotBlank | |
|---|---|---|---|
| 定义 | 元素不能为 null;不检查长度 size | 不能为 **null ** 或 长度size != 0 |
不仅不能为 null,而且至少包含一个非空白字符 |
| 实质 | 必须有定义 | String、Collection、Map 的 isEmpty() 方法 |
trim() 后 size>0 |
| 用途 | 用于字符串、集合、Map、数组 | 只用于 String | |
| 场景 | 结果判空 | username,手机验证码 | 手机号 |
| 提供 | **JDK ** | Hibernate Validator | Hibernate Validator |
String name1 = null; |
false |
false |
false |
String name3 = ""; |
true |
false |
false |
String name3 = " "; ` |
true |
true |
false |
常用注解
@Size(min=4, max=15):元素大小必须在指定范围内;可是字符串、数组、集合、map 等;
@DecimalMin(value)/@DecimalMax(value):必须是一个(BigDecimal 的字符串表示形式的)>= / <=指定值的数字,可以是小数。
| 分类 | 注解 | 来源 | 功能 |
|---|---|---|---|
| 判空 | @NotBlank |
Hibernate | 只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 |
| 判空 | @NotEmpty |
Hibernate | 集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null |
| 判空 | @NotNull |
JDK | 不能为 null |
@Pattern(value) |
Hibernate | 被注释的元素必须符合指定的正则表达式 | |
| 大小 | @Max(value) |
JDK | 必须是一个 Long 类型的数字,该字段的值只能 <= 该值 |
| 大小 | @Min(value) |
JDK | 该字段的值只能大于或等于该值 |
| 大小 | @Range(min=, max=) |
Hibernate | 检被注释的元素必须在合适的范围内 |
| 大小 | @Size(max, min) |
JDK | 检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。 |
| 大小 | @Length(max, min) |
Hibernate | 被注释的字符串的大小必须在指定的范围内。 |
@AssertFalse |
JDK | 被注释的元素必须为 false |
|
@AssertTrue |
JDK | 被注释的元素必须为 true |
|
@Email |
Hibernate | 被注释的元素必须是电子邮箱地址 | |
@URL(protocol=,host=,port=,regexp=,flags=) |
Hibernate | 被注释的字符串必须是一个有效的 URL | |
@CreditCardNumber |
Hibernate | 对信用卡进行一个大致的校验; |
@Pattern
1 | |
不常用注解
| 分类 | 注解 | 来源 | 功能 |
|---|---|---|---|
@Null |
JDK | 必须为 null |
|
| 数字 | @DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | |
| 数字 | @DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | |
| 数字 | @Digits(integer, fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 | |
| 数字 | @Positive |
JDK | 判断正数 |
| 数字 | @PositiveOrZero |
JDK | 判断正数或 0 |
| 数字 | @Negative |
JDK | 判断负数 |
| 数字 | @NegativeOrZero |
JDK | 判断负数或 0 |
| 日期 | @Future |
JDK | 被注释的元素必须是一个将来的日期 |
| 日期 | @FutureOrPresent |
JDK | 判断日期是否是将来或现在日期 |
| 日期 | @Past |
JDK | 检查该字段的日期是在过去 |
@PastOrPresent |
JDK | 判断日期是否是过去或现在日期 | |
@SafeHtml |
判断提交的 HTML 是否安全。例如说,不能包含 JavaScript 脚本等等 |
@InEnum
1 | |
@DateTimeFormat 时间传参
对于日期格式化,针对 Date 类型字段:
- 接收前端校验注解,
@DateTimeFormat接受前台的时间格式传到后台的格式; - 后端返回给前端日期格式化,使用场景数据库存储的是
yyyy-MM-dd HH:mm:ss,但前端需要yyyy-MM-dd时可用@JsonFormat。
1 | |
Query 时间传参
Query 时间传参,指的是 GET 请求、或者 POST 的 form-data 请求。比如,由前端的日期时间控件传的参数。
① 后端接收时间参数时,需要添加 SpringMVC 的 @DateTimeFormat 注解,并设置时间格式。例如说:
1 | |
② 前端传递时间参数时,需要时间格式为 yyyy-MM-dd HH:mm:ss,和上面的 FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND 对应。例如说前端 yudao-ui-admin-vue3 项目:
views/infra/job/logger/index.vue的beginTime或endTime参数
Request Body 时间传参
Request Body 时间传参,指的是 Post、PUT 等请求,通过 JSON 格式。
① 后端接收时间参数时,需要添加 SpringMVC 的 @RequestBody 注解,使用 LocalDateTime 属性进行接收。
② 前端传递时间参数时,需要时间格式为 Long 时间戳。例如说:
views/system/tenant/TenantForm.vue的expireTime参数

Response Body 时间响应
JSON 返回的时间,使用 LocalDateTime 定义属性,会被序列化为 Long 时间戳进行相应。
例如说 TenantRespVO 的 createTime 属性。
如何自定义 JSON 时间格式?
为什么使用 Long 时间戳呢?
-
每个项目希望展示的时间格式可能不同,有希望
yyyy-MM-dd HH:mm:ss,也有希望yyyy/MM/dd HH:mm:ss,又或者是其它。 -
而 Long 时间戳是比较标准的,没有任何“产品需求”的味道,所以使用它。 至于业务希望展示成什么样子,可以通过前端封装统一的 format 方法去实现,更加规范。
它是通过 LocalDateTime 自定义的 TimestampLocalDateTimeSerializer 和 TimestampLocalDateTimeDeserializer 实现,之后进行如下配置:

全局配置时间格式
如果你想 JSON 全局配置成 yyyy-MM-dd HH:mm:ss 或其它时间格式,通过使用 Jackson 内置的 LocalDateTimeSerializer 和 LocalDateTimeDeserializer 即可,如下图所示:

局部配置时间格式
如果只是部分 VO 的字段想自定义 yyyy-MM-dd HH:mm:ss 或其它时间格式,可通过 Jackson 内置的 @JsonFormat 注解,如下所示:
1 | |
自定义校验注解
如果 Validator 内置的参数校验注解不满足需求时,也可以自定义参数校验的注解。
开发自定义约束一共只要两步:
- 编写自定义约束的注解;
- 编写自定义的校验器 ConstraintValidator 。
可以按照 @NotNull 等基础校验注解源码的写法。用 @Retention。
1 | |
以手机号校验为例。
编写自定义注解
① 第一步,新建 @Mobile 注解,并设置自定义校验器为 MobileValidator (opens new window)接口(加 @Target 等注解)。
1 | |
编写自定义校验器
新建 MobileValidator (opens new window)校验器,实现 ConstraintValidator 接口。
1 | |
参数上添加注解
在需要手机格式校验的参数上添加 @Mobile 注解。示例代码如下:
1 | |
使用步骤
更多关于 Validator 的使用,可以系统阅读 《芋道 Spring Boot 参数校验 Validation 入门 》 (opens new window)文章。
- 例如说,手动参数校验、分组校验、国际化 i18n 等等。
引入依赖
-
引入参数校验的
spring-boot-starter-validation依赖。一般不需要做,项目默认已经引入。- 如果项目使用了 Spring Boot,
spring-boot-starter-web包中已集成了hibernate-validator框架,无需再添加hibernate validator依赖。
1
2
3
4<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> - 如果项目使用了 Spring Boot,
-
如果不是 Spring Boot 项目,需添加
hibernate.validator依赖。1
2
3
4
5<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.13.Final</version> </dependency>
在类、方法参数上加注解
- 在需要参数校验的类上,添加
@Validated注解,表示类中所有接口都需要进行参数校验。例如说 Controller、Service 类。- 疑问:Controller 做了参数校验后,Service 是否需要做参数校验?
- 是需要的。Service 可能会被别的 Service 进行调用,也会存在参数不正确的情况,所以必须进行参数校验。
- 在方法参数上添加注解:
- 如果方法的参数是 Bean 类型,则添加
@Valid注解,并在 Bean(POJO) 类上添加参数校验的注解。 - 如果方法的参数是普通类型,则直接添加参数校验的注解。
- 如果方法的参数是 Bean 类型,则添加
Controller 类
1 | |
Service 接口
相比在 Controller 添加参数校验来说,在 Service 进行参数校验,会更加安全可靠。
-
个人建议的话,Controller 的参数校验可以不做,Service 的参数校验一定要做。
-
和 UserController 的方法是一致的,包括注解。
1 | |
接口
1 | |
Bean 类
1 | |
POJO 类
如用于 DTO 类。
在 POJO 实体类上加上具体的约束注解。
1 | |
分组校验
暂时没有这方面的诉求。即使有,也是拆分不同的 Bean 类。
在一些业务场景下,需要使用分组校验,即相同的 Bean 对象(在同一个字段上加校验注解),根据校验分组,使用不同的校验规则。
- 使用分组校验,核心在于添加上
@Validated注解,并设置对应的校验分组。
UserUpdateStatusDTO
创建 UserUpdateStatusDTO 类,为用户更新状态 DTO 。
- 创建了 Group01 和 Group02 接口,作为两个校验分组。不一定要定义在 UserUpdateStatusDTO 类中,这里仅仅是为了方便。
status字段,在 Group01 校验分组时,必须为true;在 Group02 校验分组时,必须为false。
1 | |
UserController
修改 UserController 类,增加两个修改状态的 API 接口。
- 对于
#updateStatusTrue(updateStatusDTO)方法,在updateStatusDTO参数上,添加了@Validated注解,并且设置校验分组为 Group01 。校验不通过报错 “请求参数不合法:状态必须为 true”。 - 对于
#updateStatusFalse(updateStatusDTO)方法,我们在updateStatusDTO参数上,添加了@Validated注解,并且设置校验分组为 Group02 。校验不通过报错 “请求参数不合法:状态必须为 false“。
1 | |
Bean Validation 手动校验
示例代码对应仓库:lab-22-validation-01 。
在上面的示例中,使用的主要是 Spring Validation 的声明式注解。然而在少数业务场景下,可能需要手动使用 Bean Validation API 进行参数校验。
修改 UserServiceTest 测试类,增加手动参数校验的示例。
1 | |
-
<1.2>处,打印validator的类型。输出如下:1
org.springframework.validation.beanvalidation.LocalValidatorFactoryBean@48c3205avalidator的类型为 LocalValidatorFactoryBean 。LocalValidatorFactoryBean 提供 JSR-303、JSR-349 的支持,同时兼容 Hibernate Validator 。- 在 Spring Boot 体系中,使用 ValidationAutoConfiguration 自动化配置类,默认创建 LocalValidatorFactoryBean 作为 Validator Bean 。
-
<3>处,调用Validator#validate(T object, Class<?>... groups)方法,进行参数校验。 -
<4>处,打印校验结果。如果校验通过,则返回的Set<ConstraintViolation<?>>集合为空。- 输出如下:
1
2username:登录账号不能为空 password:密码不能为空
处理参数校验异常
因为 UserController 使用了 @Validated 注解,那么 Spring Validation 就会使用 AOP 切面进行参数校验。而该切面的拦截器,使用的是 MethodValidationInterceptor 。
如果直接将校验的结果返回给前端,提示内容的可阅读性是比较差的,所以需要对校验抛出的异常进行处理。
- 在
GlobalExceptionHandler中,使用@ExceptionHandler注解,实现自定义的异常处理。
#get(id) 方法的返回的结果是 status = 500 ,而 #add(addDTO) 方法的返回的结果是 status = 400 。
- 对于
#get(id)方法,在 MethodValidationInterceptor 拦截器中,校验到参数不正确,会抛出 ConstraintViolationException 异常。 - 对于
#add(addDTO)方法,因为addDTO是个 POJO 对象,所以会走 SpringMVC 的 DataBinder 机制,它会调用DataBinder#validate(Object... validationHints)方法,进行校验。在校验不通过时,会抛出 BindException 。
在 SpringMVC 中,默认使用 DefaultHandlerExceptionResolver 处理异常。
- 对于 BindException 异常,处理成 400 的状态码。
- 对于 ConstraintViolationException 异常,没有特殊处理,所以处理成 500 的状态码。

校验参数错误码
- 校验参数不通过的错误码用:
- 也可以定义为枚举类。
1 | |
处理 Validator 校验不通过异常
- 将每个约束的错误内容提示,拼接起来,使用
;分隔。
1 | |
重新请求 UserController#get(id) 对应的接口,响应结果如下:

处理 BindException 异常
修改 GlobalExceptionHandler 类,增加 #bindExceptionHandler(...) 方法,处理 BindException 异常。
- 将每个约束的错误内容提示,拼接起来,使用
;分隔。
1 | |
- 重新请求
UserController#add(addDTO)对应的接口,响应结果如下:

加入自定义校验,因为传入的请求参数 gender 的值为 null ,显然不在 GenderEnum 范围内,所以校验不通过,输出 "性别必须是 [1, 2]" 。

通用返回
CommonResult
项目在实践时,将状态码放在 Response Body 响应内容中返回。一共有 3 个字段,通过 CommonResult (opens new window)定义如下:
- 在 RESTful API 成功时,定义(Controller 对应方法的)返回类型为 CommonResult,并调用
#success(T data)(opens new window)方法来返回。- CommonResult 的
data字段是泛型,建议定义对应的 VO 类,而不是使用 Map 类。
- CommonResult 的
- 在 RESTful API 失败时,通过抛出 Exception 异常,具体在 「2. 异常处理」 小节。
- 失败时的
code字段,使用全局的错误码。
- 失败时的

1 | |
可以增加 success 字段吗?
- 有些团队在实践时,会增加了
success字段,通过true和false表示成功还是失败。 这个看每个团队的习惯吧。 - 还是偏好基于约定,返回
0时表示成功。
1 | |
使用 @ControllerAdvice
@ControllerAdvice在 Spring MVC 中,可以使用 @ControllerAdvice 注解,通过 Spring AOP 拦截修改 Controller 方法的返回结果,从而实现全局的统一返回。
- 可以阅读 《芋道 Spring Boot SpringMVC 入门 》 (opens new window)文章的「4. 全局统一返回 」小节。
为什么项目不采用这种方式呢?
- 主要原因是,这样的方式“破坏”了方法的定义,导致一些隐性的问题。例如说,Swagger 接口定义错误,展示的响应结果不是 CommonResult。
- 还有个原因,部分 RESTful API 不需要自动包装 CommonResult 结果。例如说,第三方支付回调只需要返回
"success"字符串。
ErrorCode 错误码
错误码,对应 ErrorCode (opens new window)类,枚举项目中的错误,全局唯一,方便定位是谁的错、错在哪。
分成两类:
- 全局的系统错误码、
- 模块的业务错误码。
全局的系统错误码
定义在 GlobalErrorCodeConstants 接口中
全局的系统错误码,使用 0-999 错误码段,和 HTTP 响应状态码 (opens new window)对应。
- 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的。
新增的有:
- 423:请求失败,请稍后重试。比如并发请求,不允许。
- 429:请求过于频繁,请稍后重试。
- 9XX:自定义错误段
- 900:重复请求。

1 | |
模块的业务错误码
模块的业务错误码,按照模块分配错误码的区间,避免模块之间的错误码冲突。
- ① 业务错误码一共 10 位,分成 4 段,在
ServiceErrorCodeRange类中分配。 - ② 每个业务模块,定义自己的 ErrorCodeConstants 错误码枚举类。
规则与代码如下:
1 | |
以 yudao-module-system 模块举例子,代码如下:

ServiceException
定义 ServiceException (opens new window)异常类,继承 RuntimeException 异常类(非受检),用于定义业务异常。

ServerException
todo
统一响应和异常处理
异常相关的统一响应、异常处理、业务异常、错误码这 4 块的内容。
统一响应
后端提供 RESTful API 给前端时,需要响应前端 API 调用是否成功。
- 因此,需要有统一响应,而不能是每个接口定义自己的风格。
一般来说,统一响应返回信息如下:
- 成功时,返回成功的状态码 + 数据。后续,前端会将数据渲染到页面上。
- 失败时,返回失败的状态码 + 错误提示。一般,前端会将原因弹出提示给用户。
在标准的 RESTful API 的定义,是推荐使用 HTTP 响应状态码 (opens new window)作为状态码。一般来说,我们实践很少这么去做,主要原因如下:
- 业务返回的错误状态码很多,HTTP 响应状态码无法很好的映射。例如说,活动还未开始、订单已取消等等
- 学习成本高,开发者对 HTTP 响应状态码不是很了解。例如说,可能只知道 200、403、404、500 几种常见的
GlobalResponseBodyHandler
全局响应结果处理器
- 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult},
- 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。
- 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构
- 目前,主要作用是,记录 Controller 的返回结果,
- 方便 {@link cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志
1 | |
统一异常处理
Global 全局异常
分为两类:
- RESTful API 发生异常时:需要拦截 Exception 异常,转换成统一响应的格式
- Spring MVC 的异常:通过
@ControllerAdvice+@ExceptionHandler注解,指定异常的类型,转换成对应的 CommonResult 响应。 - Filter 的异常:通过
try catch的方式,手动调用globalExceptionHandler.allExceptionHandler(request, ex);,返回 CommonResult<?>。
- Spring MVC 的异常:通过
- Service 发生业务异常时:封装 ServiceException 统一业务异常,里面有错误码和错误提示,然后进行
throw抛出。 - RPC 服务 API异常:
- Gateway 全局异常处理器:
GlobalExceptionHandler
全局异常处理器
1 | |
RESTfuk API 异常处理
RESTful API 发生异常时,需要拦截 Exception 异常,转换成统一响应的格式,否则前端无法处理。
Spring MVC 的异常
在 Spring MVC 中,通过 @ControllerAdvice + @ExceptionHandler 注解,声明将指定类型的异常,转换成对应的 CommonResult 响应。
@ControllerAdvice注解:@ExceptionHandler注解:指定异常的类型
实现的代码,可见 GlobalExceptionHandler (opens new window)类。
@SuppressWarnings:允许选择性地取消特定代码段(即,类或方法)中的警告。- 作用是给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默。批注J2SE 提供的

defaultExceptionHandler
1 | |
Filter 的异常
在请求被 Spring MVC 处理之前,是先经过 Filter 处理的,此时发生异常时,是无法通过 @ExceptionHandler 注解来处理的。
- 只能通过
try catch的方式来实现,代码如下:
1 | |

业务异常处理
在 Service 发生业务异常时,如果进行返回呢?例如说,用户名已经存在,商品库存不足等。常用的方案选择,主要有两种:
方案一,直接使用 CommonResult 统一响应结果,里面有错误码和错误提示,然后进行return返回- 方案二,封装 ServiceException 统一业务异常,里面有错误码和错误提示,然后进行
throw抛出
选择方案一 CommonResult 会存在两个问题:
- 因为 Spring
@Transactional声明式事务,是基于异常进行回滚的,如果使用 CommonResult 返回,则事务回滚会非常麻烦。 - 当调用别的方法时,如果别人返回的是 CommonResult 对象,还需要不断的进行判断,写起来挺麻烦的。
因此,项目采用方案二 ServiceException 异常。
serviceExceptionHandler
定义 ServiceException (opens new window)异常类,继承 RuntimeException 异常类(非受检),用于定义业务异常。
为什么继承 RuntimeException 异常?
- 大多数业务场景下,无需处理 ServiceException 业务异常,而是通过 GlobalExceptionHandler 统一处理,转换成对应的 CommonResult 对象,进而提示给前端即可。
- 如果真的需要处理 ServiceException 时,通过
try catch的方式进行主动捕获。
1 | |
ServiceExceptionUtil
在 Service 需抛出业务异常时,通过调用 ServiceExceptionUtil (opens new window)的 #exception(ErrorCode errorCode, Object... params) 方法来构建 ServiceException 异常,然后使用 throw 进行抛出。
为什么使用 ServiceExceptionUtil 来构建 ServiceException 异常?
- 目的在于,格式化异常信息提示。
- 错误提示的内容,支持使用管理后台进行动态配置,所以通过 ServiceExceptionUtil 获取内容的配置与格式化。
1 | |

业务异常
1 | |
Assert
1 | |
RPC 服务 - API 异常日志
1 | |
- @Async:用来声明一个异步方法,
- @Await:用来等待异步方法执行
1 | |
Gateway 全局异常处理器
Gateway 的全局异常处理器:将 Exception 翻译成 CommonResult + 对应的异常编号。
ErrorWebExceptionHandlerMonoServerWebExchange:是一个HTTP请求-响应交互的契约。提供对HTTP请求和响应的访问,并公开额外的服务器端处理相关属性和特性,如请求属性。- 服务网络交换器:存放着重要的请求-响应属性、请求实例和响应实例等等,有点像
Context的角色。 - 注意到过滤器(包括
GatewayFilter、GlobalFilter和过滤器链GatewayFilterChain),都依赖到ServerWebExchange
- 服务网络交换器:存放着重要的请求-响应属性、请求实例和响应实例等等,有点像
ServerHttpResponse:用于表示服务器端的HTTP响应ServerHttpResponse response = serverWebExchange .getResponse();
WebFrameworkUtils.writeJSON(exchange, result)ResponseStatusException
1 | |
Filter 过滤器
Filter对web服务器管理的所有资源进行拦截,例如实现URL级别的权限访问控制、过滤敏感词汇等。
大致流程:Filter对用户请求进行预处理,接着将请求交给Servlet进行处理并生成响应,最后Filter再对服务器响应进行后处理。
- Filter接口中有一个doFilter方法,里面编写我们的业务逻辑,配置对哪个资源进行拦截,在调用service方法之前,都会先调用一下filter的doFilter方法,
- 也可以编写多个Filter,这些Filter组合起来称之为一个Filter链,也可以控制先调用哪个Filter。
- web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。
- 在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。
Filter的创建和销毁由WEB服务器负责。 web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。
doFilter
doFilter是整个过滤器最底层的概念Filter接口中的方法。- Spring的
OncePerRequestFilter类:实际上是一个实现了Filter接口的抽象类。spring对Filter进行了一些封装处理,能够确保在一次请求只通过一次filter,而不需要重复执行,因为在执行完后会在request设置标识符。 - 而
doFilterInternal是OncePerRequestFilter 中的一个抽象方法。 abstract class ApiRequestFilter extends OncePerRequestFilter:过滤 /admin-api、/app-api 等 API 请求的过滤器CacheRequestBodyFilter extends OncePerRequestFilter:Request Body 缓存 Filter,实现它的可重复读取
拦截器
在SpringBoot中可以使用HandlerInterceptorAdapter这个适配器来实现自己的拦截器。这样就可以拦截所有的请求并做相应的处理。
- 应用场景:请求日志,权限,JVM性能输出等。
在HandlerInterceptorAdapter中主要提供了以下的方法:
- preHandle:在方法被调用前执行。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行。
- postHandle:在方法执行后调用。
- afterCompletion:在整个请求处理完毕后进行回调,也就是调用方已经拿到响应时。
拦截器和过滤器的区别
- 拦截器是基于java的反射机制的,而过滤器是基于函数回调。
- 拦截器不依赖与servlet容器,过滤器依赖于servlet容器。
- 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
- 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
- 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
- 拦截器可以获取ioc中的service bean实现业务逻辑,拦截器可以获取ioc中的service bean实现业务逻辑,在拦截器里注入一个service,可以调用业务逻辑。
总结:过滤器包裹住servlet,servlet包裹住拦截器
触发时机:过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前。
- 过滤器的触发时机是容器后,servlet之前,所以过滤器的 doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 的入参是ServletRequest ,而不是httpservletrequest。因为过滤器是在httpservlet之前。
chain.doFilter(request, response);这个方法的调用作为分水岭。事实上调用Servlet的doService()方法是在chain.doFilter(request, response);这个方法中进行的。
拦截器是被包裹在过滤器之中的。
- 拦截器的preHandle()方法是在过滤器的chain.doFilter(request, response)方法的前一步执行,不是doFilter(request,response,chain)之前。
- postHandle()方法在return ModelAndView之前,可以操控Controller的内容。 afterCompletion()方法是在过滤器返回给前端前一步执行。也就是doFilter(request,response,chain)方法return之前执行。
- 在我们项目中用的最多是preHandle这个方法,而未用其他的,框架提供了一个已经实现了拦截器接口的适配器类HandlerInterceptorAdapter,继承这个类重写一下需要用到的方法就行了,可以少几行代码,这种方式Java中很多地方都有体现。
API 日志
API 日志:包含两类
- API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
- 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。