结合
@ControllerAdvice
穷举@ExceptionHandler
对异常分类处理是当前企业使用 Spring 框架开发 Web 项目的常见做法,但这么做却偏向于硬编码,本文结合官方使用手册予以探究并应用。
一、前言
(一)手册说明
Spring 5.2.10 使用手册 中 1.3.6. Exceptions REST API exceptions 部分内容如图:

其中提到:
Applications that implement global exception handling with error details
in the response body should consider extending ResponseEntityExceptionHandler,
which provides handling for exceptions that Spring MVC raises and provides hooks
to customize the response body. To make use of this, create a subclass of
ResponseEntityExceptionHandler, annotate it with @ControllerAdvice,
override the necessary methods, and declare it as a Spring bean.
直译如下:
在响应主体中实现错误详细信息的全局异常处理的应用程序应该考虑扩展 ResponseEntityExceptionHandler, 它为 Spring MVC 引发的异常提供处理,并提供钩子来自定义响应主体。 要利用这一点,请创建 ResponseEntityExceptionHandler 的子类,用 @ControllerAdvice 对其进行注释, 覆盖必要的方法,并将其声明为 Spring Bean。
综上所述,官网手册推荐的是通过继承ResponseEntityExceptionHandler
,并用@ControllerAdvice
对其进行注释 来完成对 Web 应用中 RESTFUL
风格接口的全局异常处理。
(二)注释内容
在org.springframework:spring-webmvc:5.2.10.RELEASE
中我们找到ResponseEntityExceptionHandler
类,如下:

其中提到:
This base class provides an @ExceptionHandler method for handling internal Spring MVC exceptions.
直译如下:
该基类提供了一个 @ExceptionHandler 方法,用于处理内部 Spring MVC 异常。
这个@ExceptionHandler
方法,便是在其方法handleException()
上修饰,其中还专门以final
字段修饰,且在最后留下了注释:
Unknown exception, typically a wrapper with a common MVC exception as cause (since @ExceptionHandler type declarations also match first-level causes): We only deal with top-level MVC exceptions here, so let's rethrow the given exception for further processing through the HandlerExceptionResolver chain.
直译如下:
未知异常,通常是一个以常见 MVC 异常为原因的包装(因为 @ExceptionHandler 类型声明也与一级原因匹配): 我们在这里只处理顶级 MVC 异常,所以让我们重新抛出给定的异常,以便通过 HandlerExceptionResolver 链进一步处理。
综上所述,我们可以得到如下信息:
- ResponseEntityExceptionHandler 专用于 Spring MVC 自身的顶级异常,如需变更请重写;
- 处理方法不能被继承等方式修改
所以开发的应用未来在继承ResponseEntityExceptionHandler
时需要像它一样专用于捕捉应用的内部异常, 换句话说,需要定义好这个应用的顶级异常有哪些,且不能被轻易修改。
二、思想与设计
(一)异常定义
1. 兼容 Spring MVC 内部异常
因为应用就是基于 Spring MVC 进行开发的,所以需要兼容,而兼容方式也非常简单,继承并用 @ControllerAdvice 对其进行注释再声明为Bean
即可自定义处理,正如手册中提到的一样, ResponseEntityExceptionHandler
提供了勾子(继承重写)帮助我们来实现。
2. 应用的顶级异常
对于 Web 应用而言,本质上就是建立在"请求-响应协议"之上进行交互的应用,因此我们可以直接参考HTTP
状态码, 直接定义出如下异常种类:
- 应用异常(WebApplicationException)
- 客户端异常(ClientException)
- 非法操作异常(IllegalOperationException)
- 请求参数异常(RequestParamsException)
- 重复请求异常(RequestRepeatException)
- 更多可扩展(More······)
- 服务端异常(ServerException)
- 业务异常(BusinessException)
- 业务数据异常(BusinessDataException)
- 没有这样的业务数据异常(NoSuchBusinessDataException)
- 业务数据异常(BusinessDataException)
- 配置异常(ConfigurationException)
- 第三方异常(ThirdPartyException)
- 内部API异常(InternalApiException)
- 更多可扩展(More······)
- 业务异常(BusinessException)
- 客户端异常(ClientException)
上述异常中,定义了应用异常(WebApplicationException
)作为应用的顶级异常,在继承ResponseEntityExceptionHandler
时 需要专门为其处理。
3. 非预期异常
应用内部中,往往存在开发人员未注意到代码细节而抛出异常,以除 0 为例:
java
@Override
public void unexpected() {
BigDecimal paidAmount = new BigDecimal(100);
for (int i = 0; i < 100; i++) {
paidAmount.divide(BigDecimal.valueOf(i));
}
}
像这种情况,我们需要和ResponseEntityExceptionHandler
一样,在最后一个 else 处理这些异常, 以便通过 HandlerExceptionResolver 链进一步处理。

因此我们必须规定,开发人员抛出的异常只能是应用异常(WebApplicationException)的子类
(二)继承实现
综上(一)所述,继承ResponseEntityExceptionHandler
,内容如下:
java
@ControllerAdvice
public class ApplicationResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({
Throwable.class
})
public final ResponseEntity<Object> handleApplicationException(Throwable ex) {
logger.error("An exception is caught, the detailed content is as follows:", ex);
if (ex instanceof WebApplicationException) {
return handleWebApplicationException((WebApplicationException) wae);
} else {
return handleUnexpectedException(ex);
}
}
// Spring MVC internal exceptions
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
// Compatibility processing
}
// more
}
特别地,与ResponseEntityExceptionHandler
不同的是,ApplicationResponseEntityExceptionHandler
中@ExceptionHandler
捕捉的是Throwable.class
,是因为可能出现3.非预期异常
部分提到的异常种类是多且不确定的。
三、配置化异常处理
在上述内容中,仍然存在以下问题:
- 发生异常时对外响应的内容仍然是硬编码,例如在测试环境中,希望直接将异常的详细原因响应出来方便查找, 而在生产环境中,却不希望将这些堆栈展示于客户端中;
- 有些常见的非预期异常需要进行控制,如
NullPointerException
、IOException
、SQLException
等。
针对上述情况,希望将异常处理进行配置化,因此可以结合@ConfigurationProperties
定义配置属性,目的是 可以通过.yaml
或.properties
文件配置完成,如下:
yaml
spring:
application:
web:
expception:
response:
server:
exposure: true
unexpected:
http-status: 500
body-status: 5009999
exposure: true
configs:
"[java.sql.SQLException]":
http-status: 500
body-status: 5000000
body-message: 服务器开小差,请稍后再试...
因此,ApplicationResponseEntityExceptionHandler
的代码可以修改为:
java
@ControllerAdvice
public class ApplicationResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({
Throwable.class
})
public final ResponseEntity<Object> handleApplicationException(Throwable ex) {
logger.error("An exception is caught, the detailed content is as follows:", ex);
if (ex instanceof WebApplicationException) {
return handleWebApplicationException((WebApplicationException) wae);
} else if (isConfigured(ex)) {
return handleConfiguredException(ex);
} else {
return handleUnexpectedException(ex);
}
}
// Spring MVC internal exceptions
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
// Compatibility processing
}
// more
}
四、实践与应用
(一)顶层异常
参考内容 二、思想与设计(一)异常定义 2. 应用的顶级异常
定义如下异常种类:

(二)属性配置
定义WebExceptionResponseProperties
如下:
java
package io.github.rovingsea.boot.web;
import io.github.rovingsea.boot.web.exception.ServerException;
import io.github.rovingsea.boot.web.exception.WebException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpStatus;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* <p>
* Web 项目异常响应配置
* </p>
*
* @author wuhaixin
* @version 2023-11-17
* @since 2023-11-17
*/
@Data
@Slf4j
@ConfigurationProperties(prefix = WebExceptionResponseProperties.PREFIX)
public class WebExceptionResponseProperties {
public static final String PREFIX = "spring.application.web.exception.response";
public static final int SERVER_CODE = 5000000;
public static final int UNEXPECTED_CODE = 5009999;
public static final String UNEXPECTED_MESSAGE = "服务器开小差,请稍后再试...";
public WebExceptionResponseProperties() {
this.server = new Server();
this.unexpected = new Unexpected();
this.configs = new LinkedHashMap<>();
}
/**
* 关于 {@link ServerException} 的配置,当发生时,会根据配置的内容进行响应
*/
private Server server;
/**
* 关于非预期异常(不是手动抛出来的异常)的配置,当发生时,会根据配置的内容进行响应
*/
private Unexpected unexpected;
/**
* 手动配置哪些异常的响应内容
* <ul>
* <li>key: 类的全路径名</li>
* <li>value: {@link ResponseConfig}</li>
* </ul>
*/
private Map<String, ResponseConfig> configs;
/**
* 根据类全路径名获取对应的配置,如果没有这个配置则会默认返回设置服务异常的配置
*
* @param className 类的全路径名
* @return 配置
* @see ResponseConfig
*/
public ResponseConfig getConfigByClassName(String className) {
ResponseConfig responseConfig = configs.get(className);
if (responseConfig == null) {
log.warn("No exception class [" + className + "] configured.");
responseConfig = new ResponseConfig();
responseConfig.setHttpStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
responseConfig.setBodyStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
responseConfig.setBodyMessage(WebException.DEFAULT_SERVER_MESSAGE);
}
return responseConfig;
}
/**
* {@link ServerException} 响应配置
*/
public static class Server {
/**
* 发生内部服务异常时对外展示的内容
*/
private String displayMessage;
/**
* 是否将内部异常信息外曝,如果是,请结合 {@link #displayMessage} 一起使用
*/
private boolean exposure;
public Server() {
this.displayMessage = UNEXPECTED_MESSAGE;
this.exposure = false;
}
public Server(String bodyMessage, boolean exposure) {
this.displayMessage = bodyMessage;
this.exposure = exposure;
}
public String getDisplayMessage() {
return displayMessage;
}
public void setDisplayMessage(String displayMessage) {
this.displayMessage = displayMessage;
}
public boolean isExposure() {
return exposure;
}
public void setExposure(boolean exposure) {
this.exposure = exposure;
}
}
/**
* 非预期异常(可认为非手动抛出的异常)配置,
*/
public static class Unexpected {
/**
* 响应状态码
*
* @see HttpStatus
*/
private int httpStatus;
/**
* 响应体 code
*/
private int bodyStatus;
/**
* 响应体 message
*/
private String bodyMessage;
/**
* 是否将内部异常信息外曝
*/
private boolean exposure;
public Unexpected() {
this.httpStatus = HttpStatus.INTERNAL_SERVER_ERROR.value();
this.bodyStatus = UNEXPECTED_CODE;
this.bodyMessage = UNEXPECTED_MESSAGE;
this.exposure = false;
}
public Unexpected(int httpStatus, int bodyStatus, String bodyMessage, boolean exposure) {
this.httpStatus = httpStatus;
this.bodyStatus = bodyStatus;
this.bodyMessage = bodyMessage;
this.exposure = exposure;
}
public int getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(int httpStatus) {
this.httpStatus = httpStatus;
}
public int getBodyStatus() {
return bodyStatus;
}
public void setBodyStatus(int bodyStatus) {
this.bodyStatus = bodyStatus;
}
public String getBodyMessage() {
return bodyMessage;
}
public void setBodyMessage(String bodyMessage) {
this.bodyMessage = bodyMessage;
}
public boolean isExposure() {
return exposure;
}
public void setExposure(boolean exposure) {
this.exposure = exposure;
}
}
/**
* 指定异常的响应配置
*/
@Data
public static class ResponseConfig {
/**
* 响应的状态码
*/
private int httpStatus;
/**
* 响应体中的 status
*/
private int bodyStatus;
/**
* 响应体中的 message
*/
private String bodyMessage;
/**
* 是否将异常 message 暴露(即作为响应体中的 message)出去?
* <ul>
* <li>true 为暴露 </li>
* <li>false 为不暴露 </li>
* </ul>
* 默认为 false
*/
private boolean exposure;
}
}
(三)异常处理器
定义WebResponseEntityExceptionHandler
如下:
java
package io.github.rovingsea.boot.web.exception;
import io.github.rovingsea.boot.web.WebExceptionResponseProperties;
import io.github.rovingsea.common.util.StringUtils;
import io.github.rovingsea.boot.web.share.protocol.ResponseInteBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* <p>
* 异常处理器,捕获在系统中发生的所有异常,对异常有如下几种处理方式:
* <ol>
* <li>{@link WebException} 子类异常:如果是 {@link ClientException} 那么就直接将异常信息外曝;
* 如果是 {@link ServerException} 那么还会依据 {@link WebExceptionResponseProperties#getServer()} 中的配置进行处理。</li>
* <li>经 {@link WebExceptionResponseProperties#getConfigs()} 配置过的异常:那么会根据配置进行处理。 </li>
* <li>未经配置也未不属于 <i>WebException</i> 异常,那么将会归属为非预期异常,处理的方式可根据配置
* {@link WebExceptionResponseProperties#getUnexpected()} 进行处理。</li>
* </ol>
* <p>
* 本次升级主要有如下几点:
* <ol>
* <li>取消穷举异常和使用 <i>@ExceptionHandler</i> 的方式,转变为配置化。原因是随着项目扩展,定制化的异常的捕捉需要重新维护。</li>
* <li>结合状态码使用。原因是先前的响应状态码仅有 200 和 500,针对参数校验部分需要进行提示为 400,避免个别情况误判给后端开发。</li>
* </ol>
* </p>
* <p>
* <b>本处理器兼容了 Spring MVC 的顶层异常</b>(具体可看该方法 {@link ResponseEntityExceptionHandler#handleException}) ,
* 针对这些异常,默认在进行了配置,具体做法可见下述继承重写的方法,配置内容可参见案例模块中的<i> application.yaml </i>文件。<br>
* <b>特别地,</b> 对于 {@link MethodArgumentNotValidException} 作为参数校验异常,该异常需要将校验的信息外曝,
* 所以此处未进行配置而是通过重写方法,具体见 {@link #handleMethodArgumentNotValid(MethodArgumentNotValidException, HttpHeaders, HttpStatus, WebRequest)}
* </p>
* <p>
* 如下是配置 <i>java.sql.SQLException</i> 的案例:<pre>{@code
* spring:
* application:
* web:
* exception:
* response:
* configs:
* "[java.sql.SQLException]":
* http-status: 500
* body-status: 5000000
* body-message: 服务器开小差,请稍后再试...
* }</pre>
* 如再添加则按上述案例添加即可。
* </p>
*
* @author wuhaixin
* @version 2023-11-16
* @see WebException
* @see WebExceptionResponseProperties
* @since 2023-11-16
*/
@RestControllerAdvice
public class WebResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
protected final Log logger = LogFactory.getLog(getClass());
private final WebExceptionResponseProperties properties;
public WebResponseEntityExceptionHandler(WebExceptionResponseProperties webExceptionResponseProperties) {
this.properties = webExceptionResponseProperties;
logger.info("全局异常处理注册完毕!");
}
@ExceptionHandler({
Throwable.class
})
public final ResponseEntity<ResponseInteBean<Void>> wsjHandleException(Throwable ex) {
logger.error("An exception is caught, the detailed content is as follows:", ex);
if (ex instanceof WebException) {
return handleWebException((WebException) ex);
} else if (isConfigured(ex)) {
return handleConfiguredException(ex);
} else {
return handleUnexpectedException(ex);
}
}
private ResponseEntity<ResponseInteBean<Void>> handleWebException(WebException ex) {
ExceptionResponse response = buildWebException(ex);
ExceptionResponse.Body body = response.getBody();
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
private boolean isConfigured(Throwable ex) {
Map<String, WebExceptionResponseProperties.ResponseConfig> configs = properties.getConfigs();
String canonicalName = ex.getClass().getCanonicalName();
return configs.containsKey(canonicalName);
}
private ResponseEntity<ResponseInteBean<Void>> handleConfiguredException(Throwable ex) {
Map<String, WebExceptionResponseProperties.ResponseConfig> configs = properties.getConfigs();
String canonicalName = ex.getClass().getCanonicalName();
WebExceptionResponseProperties.ResponseConfig config = configs.get(canonicalName);
ExceptionResponse response = new ExceptionResponse(config, ex);
ExceptionResponse.Body body = response.getBody();
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
private ResponseEntity<ResponseInteBean<Void>> handleUnexpectedException(Throwable ex) {
ExceptionResponse response = buildUnexpectedException(ex);
ExceptionResponse.Body body = response.getBody();
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
private ExceptionResponse buildWebException(WebException webException) {
WebExceptionResponseProperties.Server server = properties.getServer();
ExceptionResponse response = new ExceptionResponse();
ExceptionResponse.Body body = new ExceptionResponse.Body();
body.setStatus(webException.getBodyStatus());
if (webException instanceof ClientException) {
body.setMessage(webException.getBodyMessage());
} else if (webException instanceof ServerException) {
if (server.isExposure()) {
body.setMessage(webException.getBodyMessage());
} else {
body.setMessage(server.getDisplayMessage());
}
}
response.setHttpStatus(webException.getHttpStatus());
response.setBody(body);
return response;
}
private ExceptionResponse buildUnexpectedException(Throwable ex) {
ExceptionResponse response = new ExceptionResponse();
ExceptionResponse.Body body = new ExceptionResponse.Body();
WebExceptionResponseProperties.Unexpected unexpected = properties.getUnexpected();
int httpStatus = unexpected.getHttpStatus();
int bodyStatus = unexpected.getBodyStatus();
String bodyMessage = unexpected.getBodyMessage();
boolean exposure = unexpected.isExposure();
body.setStatus(bodyStatus);
if (exposure) {
body.setMessage(ex.getMessage());
} else {
body.setMessage(bodyMessage);
}
response.setHttpStatus(httpStatus);
response.setBody(body);
return response;
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return super.handleHttpMessageNotReadable(ex, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ExceptionResponse.Body body;
List<String> messages;
BindingResult bindingResult = ex.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
if (allErrors.size() != 0) {
messages = new ArrayList<>();
for (ObjectError error : allErrors) {
messages.add(error.getDefaultMessage());
}
body = new ExceptionResponse.Body(400, String.join(";", messages));
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
WebExceptionResponseProperties.ResponseConfig config = properties.getConfigByClassName(ex.getClass().getCanonicalName());
ExceptionResponse response = new ExceptionResponse(config, ex);
body = response.getBody();
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
@Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ExceptionResponse.Body body;
List<String> messages;
BindingResult bindingResult = ex.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
if (allErrors.size() != 0) {
messages = new ArrayList<>();
for (ObjectError error : allErrors) {
messages.add(error.getDefaultMessage());
}
body = new ExceptionResponse.Body(400, String.join(";", messages));
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
WebExceptionResponseProperties.ResponseConfig config = properties.getConfigByClassName(ex.getClass().getCanonicalName());
ExceptionResponse response = new ExceptionResponse(config, ex);
body = response.getBody();
return ResponseEntity.ok(ResponseInteBean.build(body.getStatus(), StringUtils.defaultString(body.getMessage().toString()), Void.class));
}
}
(四)自动装配
定义自动装配器WebExceptionAutoConfiguration
,如下:
java
package io.github.rovingsea.boot.web.autoconfigure;
import io.github.rovingsea.boot.web.WebExceptionResponseProperties;
import io.github.rovingsea.boot.web.exception.WebResponseEntityExceptionHandler;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* <p>
* Web 异常处理自动装配
* </p>
*
* @author wuhaixin
* @version 2023-11-20
* @since 2023-11-20
* @see META-INF
*/
@Configuration
@EnableConfigurationProperties(WebExceptionResponseProperties.class)
@ConditionalOnProperty(name = "spring.application.web.exception.enabled", matchIfMissing = true)
public class WebExceptionAutoConfiguration {
@Bean
public WebResponseEntityExceptionHandler webResponseEntityExceptionHandler(WebExceptionResponseProperties properties) {
return new WebResponseEntityExceptionHandler(properties);
}
}
(五)SPI
在 resource
下新建文件夹META-INF
,新建文件spring.factories
内容如下:
text
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.github.rovingsea.boot.web.autoconfigure.WebExceptionAutoConfiguration
(六)最佳实践
1. 模拟生产环境配置
yaml
spring:
application:
web:
exception:
response:
# 服务异常
server:
body-message: 服务器开小差,请稍后再试...
# 不允许服务器异常信息外曝
exposure: false
# 非预期异常
unexpected:
http-status: 500
body-status: 5009999
body-message: 服务器开小差,请稍后再试...
# 不允许非预期异常信息外曝
exposure: true
configs:
# 1. 兼容 spring mvc 顶层异常
"[org.springframework.web.HttpRequestMethodNotSupportedException]":
http-status: 405
body-status: 405
body-message: 请求方式不接受
"[org.springframework.web.HttpMediaTypeNotSupportedException]":
http-status: 415
body-status: 415
body-message: 请求媒体类型不支持
"[org.springframework.web.HttpMediaTypeNotAcceptableException]":
http-status: 406
body-status: 406
body-message: 请求媒体类型不接受
"[org.springframework.web.bind.MissingPathVariableException]":
http-status: 500
body-status: 500
body-message: 服务器开小差,请稍后再试...
"[org.springframework.web.bind.MissingServletRequestParameterException]":
http-status: 400
body-status: 400
body-message: 请求中缺少必填参数
"[org.springframework.web.bind.ServletRequestBindingException]":
http-status: 400
body-status: 400
body-message: 请求参数无法绑定
"[org.springframework.beans.ConversionNotSupportedException]":
http-status: 500
body-status: 500
body-message: 服务器开小差,请稍后再试...
"[org.springframework.beans.TypeMismatchException]":
http-status: 400
body-status: 400
body-message: 请求参数类型不正确
"[org.springframework.http.converter.HttpMessageNotReadableException]":
http-status: 400
body-status: 400
body-message: 请求体不可读,请检查请求体是否符合格式
"[org.springframework.http.converter.HttpMessageNotWritableException]":
http-status: 500
body-status: 500
body-message: 服务器开小差,请稍后再试...
"[org.springframework.web.multipart.support.MissingServletRequestPartException]":
http-status: 400
body-status: 400
body-message: 请求中缺少必填部分
"[org.springframework.validation.BindException]":
http-status: 400
body-status: 400
body-message: 请求参数无法绑定
"[org.springframework.web.servlet.NoHandlerFoundException]":
http-status: 404
body-status: 404
body-message: 请求路径无法处理
"[org.springframework.web.context.request.async.AsyncRequestTimeoutException]":
http-status: 503
body-status: 503
body-message: 请求超时,请稍后再试...
# 2. 系统异常配置
"[java.lang.IllegalArgumentException]":
http-status: 400
body-status: 4000000
exposure: true
"[java.lang.IllegalStateException]":
http-status: 500
body-status: 5000000
body-message: 服务器开小差,请稍后再试...
## 2.2 SQL 异常
"[java.sql.SQLException]":
http-status: 500
body-status: 5000000
body-message: 服务器开小差,请稍后再试...
2. 发生客户端异常
客户端异常代表着服务器对客户端的呈堂证供,因此客户端异常信息均会外曝异常信息
如下代码:
java
throw new IllegalOperationException("存在未提交的临分询价,不允许再新增")
响应内容:
json
{
"status": 42804250,
"message": "存在未提交的临分询价,不允许再新增",
"data": null
}
3. 发生服务端异常
3.1 允许外曝
配置如下:
yaml
spring:
application:
web:
exception:
response:
server:
exposure: true
如下代码:
java
throw new NoSuchBizDataException("100001", Element.class);
服务器日志:
text
2024-02-03 14:55:28.311 ERROR 82586 --- [nio-8080-exec-5] .f.w.e.WebResponseEntityExceptionHandler : An exception is caught, the detailed content is as follows:
io.github.rovingsea.boot.web.exception.NoSuchBizDataException: No business data named 'Element' with key '100001'.
at io.github.rovingsea.boot.web.example.api.impl.TestApiImpl.noSuchBizException(TestApiImpl.java:66) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
响应内容:
json
{
"status": 5000001,
"message": "No business data named 'Element' with key '100001'.",
"data": null
}
3.2 不允许外曝
配置如下:
yaml
spring:
application:
web:
exception:
response:
server:
body-message: 服务器开小差,请稍后再试...
exposure: false
如下代码:
java
throw new NoSuchBizDataException("100001", Element.class);
服务器日志:
text
2024-02-03 14:58:34.471 ERROR 82588 --- [nio-8080-exec-5] .f.w.e.WebResponseEntityExceptionHandler : An exception is caught, the detailed content is as follows:
io.github.rovingsea.boot.web.exception.NoSuchBizDataException: No business data named 'Element' with key '100001'.
at io.github.rovingsea.boot.web.example.api.impl.TestApiImpl.noSuchBizException(TestApiImpl.java:66) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
响应内容:
json
{
"status": 5000001,
"message": "服务器开小差,请稍后再试...",
"data": null
}
4. 发生非预期异常
4.1 允许外曝
配置如下:
yaml
spring:
application:
web:
exception:
response:
unexpected:
exposure: true
代码如下:
java
public void unexpected() {
BigDecimal paidAmount = new BigDecimal(100);
for (int i = 0; i < 100; i++) {
paidAmount.divide(BigDecimal.valueOf(i));
}
}
服务器日志:
text
2024-02-03 15:00:19.332 ERROR 82870 --- [nio-8080-exec-5] .f.w.e.WebResponseEntityExceptionHandler : An exception is caught, the detailed content is as follows:
java.lang.ArithmeticException: Division by zero
at java.math.BigDecimal.divide(BigDecimal.java:1682) ~[na:1.8.0_372]
at io.github.rovingsea.boot.web.example.api.impl.TestApiImpl.unexpected(TestApiImpl.java:40) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
响应内容:
json
{
"status": 5009999,
"message": "Division by zero",
"data": null
}
4.2 不允许外曝
配置如下:
yaml
spring:
application:
web:
exception:
response:
unexpected:
body-message: 服务器开小差,请稍后再试...
exposure: false
代码如下:
java
public void unexpected() {
BigDecimal paidAmount = new BigDecimal(100);
for (int i = 0; i < 100; i++) {
paidAmount.divide(BigDecimal.valueOf(i));
}
}
服务器日志:
text
2024-02-03 15:01:38.113 ERROR 82871 --- [nio-8080-exec-2] .f.w.e.WebResponseEntityExceptionHandler : An exception is caught, the detailed content is as follows:
java.lang.ArithmeticException: Division by zero
at java.math.BigDecimal.divide(BigDecimal.java:1682) ~[na:1.8.0_372]
at io.github.rovingsea.boot.web.example.api.impl.TestApiImpl.unexpected(TestApiImpl.java:40) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]
响应内容:
json
{
"status": 5009999,
"message": "服务器开小差,请稍后再试...",
"data": null
}
5. 多链路异常返回
模拟存在客户端调用 A 服务,A 服务再调用内部 B 服务,当 B 服务发生客户端异常时(这里的客户端指的是 A 服务,而非实际的客户端) A 服务应当接收到时要抛出服务器异常,不允许将 A、B 两个服务内部的错误交互信息外曝。
确认 A 服务异常配置,不允许服务器和非预期异常外曝:
yaml
spring:
application:
web:
exception:
response:
server:
body-message: 服务器开小差,请稍后再试...
exposure: false
unexpected:
body-message: 服务器开小差,请稍后再试...
exposure: false
确认 B 服务异常配置,允许服务器和非预期异常外曝:
yaml
spring:
application:
web:
exception:
response:
server:
exposure: true
unexpected:
exposure: true
A 某接口代码内容如下:
java
Element element = elementService.getByKey(id);
if (element != null) {
ResponseInterEntity interEntity = internalApi.processElement(element);
if (!isSucess(interEntity)) {
throw new InternalApiException(interEntity, internalApi.class);
}
// more
B internalApi.processElement
接口部分代码内容如下:
java
if (StringUtils.isEmpty(element.getNo())) {
throw new RequestParamsException("The no of Element can not be empty.");
}
// more
A 服务器日志如下:
text
2024-02-03 15:05:51.914 ERROR 82871 --- [nio-8080-exec-1] .f.w.e.WebResponseEntityExceptionHandler : An exception is caught, the detailed content is as follows:
io.github.rovingsea.boot.web.exception.InternalApiException: Failed to request api named 'internalApi',
the response body status is '4000001' and message is 'The no of Element can not be empty.'.
at io.github.rovingsea.boot.web.example.api.impl.AApiImpl.textRpc(TestApiImpl.java:51) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]
B 服务器日志如下:
text
2024-02-03 15:05:51.918 ERROR 82872 --- [nio-8080-exec-2] .f.w.e.WebResponseEntityExceptionHandler : An exception is caught, the detailed content is as follows:
io.github.rovingsea.boot.web.exception.RequestParamsException: The no of Element can not be empty.
at io.github.rovingsea.boot.web.example.api.impl.BApiImpl.textRpc(TestApiImpl.java:36) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]
客户端接收内容:
json
{
"status": 5009999,
"message": "服务器开小差,请稍后再试...",
"data": null
}
五、总结
按照手册继承ResponseEntityExceptionHandler
实现RESTFUL
风格接口的全局异常处理的同时, 也挖掘出应用也需要定义出顶级异常的 思想,再将其结合并通过配置化处理进一步地使异常处理更加灵活, 也使抛出异常的种类更加丰富和多样,还使代码一语中的和语法化。 因此,选择配置化异常处理和遵循手册的做法是较于穷举 @ExceptionHandler 来异常处理的更优选择。