错误码体系
[一]前言
错误体系是任何编程语言和应用系统不可或缺的组成部分。有人对之退避三舍,有人对之甘之如饴。
在软件开发的复杂世界中,错误是不可避免的。无论是因为外部系统的变化、用户输入的错误,还是内部逻辑的缺陷,错误都会出现。为了有效管理这些错误,并向用户和开发者提供清晰、有用的反馈,设计一套合理的错误码和错误提示系统变得至关重要。
在软件项目的早期阶段就预测和规划所有可能的错误情况是一项挑战。设计过程需要在全面性和灵活性之间找到平衡点。此外,设计的错误码和提示不仅要对开发者有用,还要能够为最终用户提供清晰、易懂的信息。
[1]设计最佳实践
系统化错误分类
创建一个系统化的错误分类体系是确保错误码和提示设计既灵活又全面的基础。这可以帮助组织和规划错误码,并提高代码的可读性和可维护性。
使用错误码模板
错误码模板可以帮助生成一致和规范的错误码。例如,模板可以基于错误的类型、来源和严重程度来生成错误码。
动态错误提示信息
实现根据错误上下文动态生成错误提示信息的机制可以提高错误信息的实用性,帮助开发人员和用户更快地定位和解决问题。
为未来的变化预留空间
在设计错误码时,预留一定范围的代码用于未来可能出现的新错误,可以最大限度地减少因添加新错误类型而导致的重构需求。
用户友好的错误提示
错误提示应清晰、易懂,避免使用技术性或模糊的语言,并提供解决问题的建议或行动指导,以提升用户体验。
[2]Dante Cloud 错误体系设计的初衷
Dante Cloud 在开发之初,也和大多数应用系统一样,并没有太多的考虑错误体系的设计问题。随着,Dante Cloud 的开源以及用户的增多,问题就慢慢地暴露了出来。
抛开能力经验和技术水平不谈,仅是在问题沟通方面就出现了很多问题。大多数用户提问的习惯就是要么只打一句话、要么只截一张图,具体什么环境、参数设置、是否修改了代码、改了哪些地方、怎么操作能够重现问题均不会提供,在这种情况下想要确定问题,除非自己遇到过一模一样的问题,否则就只能全靠猜。即使提供了更详细的错误信息,由于错误体系不够成熟,导致根据错误信息很难有效区分具体的问题。
因此,这也促使 Dante Cloud 认证对自身的错误体系进行了深入的设计和改造。
[二]Dante Cloud错误体系设计
[1]设计目标
为了可以更便捷地定位问题,Dante Cloud 错误体系的设计设定了以下几个目标:
- 设计一套方便定位区分问题类型的错误码体系。
- 错误码可以自动计算和自动生成
- 可以为前端提供更友好、更准确的错误信息。
- 可以兼容多种来源的 Exception 错误。
- 可以按照模块定义各自的错误信息,提升代码的内聚性。
[2]设计思考
(1) 方便定位区分问题类型的错误码
很多应用系统以及软件产品都会根据自身的需求,设计一套遵照一定规则的错误码
体系,通过错误码标识就可以很直观的看出一些常见的问题。错误码的设计到底采用什么规则设计,完全是由系统自身的需求而定,没有固定的标准和规范。
Dante Cloud 是一套微服务系统,核心内容就是 REST 接口,而 REST 接口与 HTTP 协议密不可分。HTTP 协议对于开发人员来说是再熟悉不过的知识点,特别是对于 HTTP 协议的状态码,大家都是耳熟能详的。仔细研究了 HTTP 协议的状态码,含义也非常清晰而且可以涵盖开发中大多数错误类型。
因此,Dante Cloud 错误码体系将 HTTP 状态码与错误码进行了融合。
例如:我们定义了一个传递参数错误
的自定义错误 Exception,通常传递参数是属于代码执行的前置操作错误,这个错误就与 HTTP 状态码 412
的解析非常符合。那么,这个 Exception 的错误码就定义为:412XX
。
错误码之所以定义为5位(前三位是 HTTP 状态码,剩余两位是自定义编码),是因为考虑到一个系统,同一个类型的错误如果太少,很难区分问题。如果太多就会干扰实际的开发使用。两位数意味着最大为99,应该足以满足一个系统对某一类型错误的定义。
(2) 错误码自动计算
因为错误码采用了数字形式,那么衍生的需求就是错误码可以自动计算和生成。如果错误码全靠手动计算,特别是在自定义错误分布在不同模块的情况下,就非常容易出错。
(3) 提供更友好、更准确的错误信息
我们可以通过定义丰富的自定义 Exception,来接解决错误不容易定位的问题,通过 Exception 名称就可以清晰错误的缘由以及大改的代码位置。但是,只有 Exception 错误名,仅能对后端人员定位问题提供方便,对于其它端甚至用户来说,是很难理解 Exception 错误名代表的含义的。
所以,需要实现一种方式,可以将 Exception 错误转换成用户或者其它端开发人员更容易理解的信息。
(4) 兼容多种来源的 Exception
将 Exception 错误转换成用户或者其它端开发人员更容易理解的信息,说起来容易,做起来却不简单。如果仅仅是自定义的 Exception,那么可以完全按照自己的思路实现即可。但是在实际开发中,我们的系统会依赖非常多的第三方组件,这些组件会定义和使用自己的 Exception,而这些 Exception 代码是很难修改的,即使修改了也不好与第三方组件融合。
目前,经常简单到的 Exception 类型,大概为以下三类:
- 自定义的 Exception,可能是 RuntimeException 也可能是 Exception
- 第三方模块代码抛出的 Exception,可能是 RuntimeException 也可能是 Exception
- 第三方模块代码抛出的继承类型 Exception。即,抛出的是第三方模块自定义 Exception 的基类,实际 Exception 信息被包含在基类 Exception
RuntimeException 和 Exception 在实际使用中还是有较大区别的
- RuntimeException:运行时错误。简单的说就是在操作或代码执行时抛出的错误,这种错误一般用于不会导致功能或系统无法运行或者崩溃的场景。最适合用于前后端传递错误信息或者根据不同类型错误做不同操作响应的场景。
- Exception:通常是用于会导致功能或者系统无法运行的错误,对于这种错误会强制要求开发人员在代码层面做出应对。最直接的表现,就是代码的方法后面会标注
throw XXXException
, 调用这个方法的代码必须要对这个错误做出处理,最常见的就是用try catch
包裹住这个方法或者选择继续抛出。
RuntimeException 和 Exception 最大的不同,就是前者是否抛出没有明确的说明,后者必须在代码方法中明确标识出会抛出。
(5) 按照模块分别定义错误信息
一想到错误信息,最简单的做法就是错误码、错误文字信息以及 Exception 放入到一个统一的类型进行定义。这种做法本身没有任何问题,但是如果涉及到多模块系统,就只能做到在一个模块中统一定义错误,其它模块都依赖这个模块,这就极大地增加了模块间的耦合度。这样做不是不可行,但是却不够优雅。
[三]Dante Cloud错误体系
[1]明确错误码范围
按照前文的设计思路,Dante Cloud 的错误码是与 HTTP 协议的状态码绑定,那么 HTTP 协议状态码的范围就是 Dante Cloud 可用的错误码的范围,如下所示:
1**
:信息,服务器收到请求,需要请求者继续执行操作2**
:成功,操作被成功接收并处理3**
:重定向,需要进一步的操作以完成请求4**
:客户端错误,请求包含语法错误或无法完成请求5**
:服务器错误,服务器在处理请求的过程中发生了错误
利用 HTTP 协议状态码本身的含义,再结合实际的应用场景以及错误信息定义,就可以描述大多数错误内容。
但是,HTTP 协议状态码毕竟范围有限,即使其含义表达的再准确,也很难涵盖所有的错误信息。为此,Dante Cloud 还额外增加了一个 Custom
错误类型,这种类型的自定义错误码,不局限于 HTTP 协议的状态可以随便定义,例如,可以是 600XX、701XX 等。
[2]定义统一的错误反馈类型
Dante Cloud 中,定义了一个 Feedback
类,作为错误体系的统一返回信息定义。
public class Feedback implements Serializable {
private static final int IS_NOT_CUSTOMIZED = 0;
private final String message;
private final int status;
/**
* 实际错误码如果与 HttpStatus 错误码对应,即开头数字为 1~5;自定义错误码,开头数字为 6~9。
* 为了方便区分错误码是与 HttpStatus 错误码对应的还是自定义的,增加了 custom 属性。如果 custom 为 0,即为与 HttpStatus 错误码对应;如果为 6~9 那么就代表是自定义错误码
*/
private final int custom;
public Feedback(String message, int status) {
this(message, status, IS_NOT_CUSTOMIZED);
}
public Feedback(String message, int status, int custom) {
Assert.checkBetween(custom, IS_NOT_CUSTOMIZED, 9);
this.message = message;
this.status = status;
this.custom = custom;
}
public String getMessage() {
return message;
}
public int getStatus() {
return status;
}
public boolean isCustom() {
return custom != IS_NOT_CUSTOMIZED;
}
public int getCustom() {
return custom;
}
public int getSequence() {
if (isCustom()) {
return custom * 10000;
} else {
return status * 100;
}
}
public int getSequence(int index) {
return getSequence() + index;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Feedback feedback = (Feedback) o;
return Objects.equal(message, feedback.message);
}
@Override
public int hashCode() {
return Objects.hashCode(message);
}
}
Feedback
类中,主要包含 message
、status
和 custom
三个属性。
message
:就是更容易理解的错误信息status
:HTTP 状态码,用于更好的区分错误状态custom
: 用于标记是错误能否与HTTP 状态码关联,是否为自定义错误
Feedback
类中还增加了 getSequence()
方法,用于标记某一类型错误的初始值以及自动计算错误码。例如,404
类型的错误,初始值就是 40400
[3]常用错误类型定义
为了方便实际代码的开发,减少配置过程产生不必要的错误,加之通用错误码与 HTTP 状态码绑定。Dante Cloud 以 Feedback
类为基础,扩展了常用错误类型反馈类。
- CustomizeFeedback
- ForbiddenFeedback
- InternalServerErrorFeedback
- MethodNotAllowedFeedback
- NoContentFeedback
- NotAcceptableFeedback
- NotFoundFeedback
- NotImplementedFeedback
- OkFeedback
- PreconditionFailedFeedback
- ServiceUnavailableFeedback
- UnauthorizedFeedback
- UnsupportedMediaTypeFeedback
有了这些错误类型,无需在手动设置错误的 status
,而且从字面意思就很容易判断错误类型,从而提升错误码定义的便捷性。部分错误代码定义如下:
public interface ErrorCodes {
/**
* 200
*/
OkFeedback OK = new OkFeedback("成功");
/**
* 204
*/
NoContentFeedback NO_CONTENT = new NoContentFeedback("无内容");
/**
* 401.** 未经授权 Unauthorized 请求要求用户的身份认证
*/
UnauthorizedFeedback UNAUTHORIZED = new UnauthorizedFeedback("未经授权");
UnauthorizedFeedback ACCESS_DENIED = new UnauthorizedFeedback("您没有权限,拒绝访问");
UnauthorizedFeedback ACCOUNT_DISABLED = new UnauthorizedFeedback("该账户已经被禁用");
UnauthorizedFeedback ACCOUNT_ENDPOINT_LIMITED = new UnauthorizedFeedback("您已经使用其它终端登录,请先退出其它终端");
UnauthorizedFeedback ACCOUNT_EXPIRED = new UnauthorizedFeedback("该账户已经过期");
UnauthorizedFeedback ACCOUNT_LOCKED = new UnauthorizedFeedback("该账户已经被锁定");
UnauthorizedFeedback BAD_CREDENTIALS = new UnauthorizedFeedback("用户名或密码错误");
UnauthorizedFeedback CREDENTIALS_EXPIRED = new UnauthorizedFeedback("该账户密码凭证已过期");
UnauthorizedFeedback INVALID_CLIENT = new UnauthorizedFeedback("客户端身份验证失败或数据库未初始化");
UnauthorizedFeedback INVALID_TOKEN = new UnauthorizedFeedback("提供的访问令牌已过期、吊销、格式错误或无效");
UnauthorizedFeedback INVALID_GRANT = new UnauthorizedFeedback("提供的授权授予或刷新令牌无效、已过期或已撤销");
UnauthorizedFeedback UNAUTHORIZED_CLIENT = new UnauthorizedFeedback("客户端无权使用此方法请求授权码或访问令牌");
UnauthorizedFeedback USERNAME_NOT_FOUND = new UnauthorizedFeedback("用户名或密码错误");
UnauthorizedFeedback SESSION_EXPIRED = new UnauthorizedFeedback("Session 已过期,请刷新页面后再使用");
UnauthorizedFeedback NOT_AUTHENTICATED = new UnauthorizedFeedback("请求的地址未通过身份认证");
······
[4]错误码构建和配置
定义好的统一的错误反馈对象,下一步就需要定义错误码的构建器 ErrorCodeMapperBuilder
。之所以构建 ErrorCodeMapperBuilder
,是因为新版的 Dante Cloud 的错误码体系实现,完全借鉴了 Spring Boot 中 Jackson
所使用的 Customizer
设计模式(想要了解详情,参见 Customizer
设计模式原理)
ErrorCodeMapperBuilder
主要用途:
- 接收自定义错误配置
- 根据不同的错误类型自动计算错误码
- 生成错误映射列表
参照 Spring Boot 中 Jackson
所使用的 Customizer
模式,Dante Cloud 还定义了一个 @FunctionalInterface
类型的接口 ErrorCodeMapperBuilderCustomizer
。配合ErrorCodeMapperBuilder
配置,最终实现支持跨模块配置的自定义错误体系
public class StandardErrorCodeMapperBuilderCustomizer implements ErrorCodeMapperBuilderCustomizer, Ordered {
@Override
public void customize(ErrorCodeMapperBuilder builder) {
builder
.unauthorized(ErrorCodes.ACCESS_DENIED,
ErrorCodes.ACCOUNT_DISABLED,
ErrorCodes.ACCOUNT_ENDPOINT_LIMITED,
ErrorCodes.ACCOUNT_EXPIRED,
ErrorCodes.ACCOUNT_LOCKED,
ErrorCodes.BAD_CREDENTIALS,
ErrorCodes.CREDENTIALS_EXPIRED,
ErrorCodes.INVALID_CLIENT,
ErrorCodes.INVALID_TOKEN,
ErrorCodes.INVALID_GRANT,
ErrorCodes.UNAUTHORIZED_CLIENT,
ErrorCodes.USERNAME_NOT_FOUND,
ErrorCodes.SESSION_EXPIRED,
ErrorCodes.NOT_AUTHENTICATED)
.forbidden(ErrorCodes.INSUFFICIENT_SCOPE, ErrorCodes.SQL_INJECTION_REQUEST)
.methodNotAllowed(ErrorCodes.HTTP_REQUEST_METHOD_NOT_SUPPORTED)
.notAcceptable(ErrorCodes.UNSUPPORTED_GRANT_TYPE,
ErrorCodes.UNSUPPORTED_RESPONSE_TYPE,
ErrorCodes.UNSUPPORTED_TOKEN_TYPE,
ErrorCodes.USERNAME_ALREADY_EXISTS,
ErrorCodes.FEIGN_DECODER_IO_EXCEPTION,
ErrorCodes.CAPTCHA_CATEGORY_IS_INCORRECT,
ErrorCodes.CAPTCHA_HANDLER_NOT_EXIST,
ErrorCodes.CAPTCHA_HAS_EXPIRED,
ErrorCodes.CAPTCHA_IS_EMPTY,
ErrorCodes.CAPTCHA_MISMATCH,
ErrorCodes.CAPTCHA_PARAMETER_ILLEGAL)
.preconditionFailed(ErrorCodes.INVALID_REDIRECT_URI, ErrorCodes.INVALID_REQUEST, ErrorCodes.INVALID_SCOPE, ErrorCodes.METHOD_ARGUMENT_NOT_VALID, ErrorCodes.ACCESS_IDENTITY_VERIFICATION_FAILED)
.unsupportedMediaType(ErrorCodes.HTTP_MEDIA_TYPE_NOT_ACCEPTABLE)
.internalServerError(ErrorCodes.SERVER_ERROR,
ErrorCodes.HTTP_MESSAGE_NOT_READABLE_EXCEPTION,
ErrorCodes.ILLEGAL_ARGUMENT_EXCEPTION,
ErrorCodes.IO_EXCEPTION,
ErrorCodes.MISSING_SERVLET_REQUEST_PARAMETER_EXCEPTION,
ErrorCodes.NULL_POINTER_EXCEPTION,
ErrorCodes.TYPE_MISMATCH_EXCEPTION,
ErrorCodes.BORROW_OBJECT_FROM_POOL_ERROR_EXCEPTION,
ErrorCodes.OPENAPI_INVOKING_FAILED)
.notImplemented(ErrorCodes.PROPERTY_VALUE_IS_NOT_SET_EXCEPTION, ErrorCodes.URL_FORMAT_INCORRECT_EXCEPTION, ErrorCodes.ILLEGAL_SYMMETRIC_KEY, ErrorCodes.DISCOVERED_UNRECORDED_ERROR_EXCEPTION)
.serviceUnavailable(ErrorCodes.COOKIE_THEFT, ErrorCodes.INVALID_COOKIE, ErrorCodes.PROVIDER_NOT_FOUND, ErrorCodes.TEMPORARILY_UNAVAILABLE, ErrorCodes.SEARCH_IP_LOCATION)
.customize(ErrorCodes.TRANSACTION_ROLLBACK,
ErrorCodes.BAD_SQL_GRAMMAR,
ErrorCodes.DATA_INTEGRITY_VIOLATION,
ErrorCodes.PIPELINE_INVALID_COMMANDS);
}
@Override
public int getOrder() {
return ErrorCodeMapperBuilderOrdered.STANDARD;
}
}
@AutoConfiguration
public class ErrorCodeAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(ErrorCodeAutoConfiguration.class);
@PostConstruct
public void postConstruct() {
log.info("[Herodotus] |- Starter [Error Code] Configure.");
}
@Bean
public ErrorCodeMapperBuilderCustomizer standardErrorCodeMapperBuilderCustomizer() {
StandardErrorCodeMapperBuilderCustomizer customizer = new StandardErrorCodeMapperBuilderCustomizer();
log.debug("[Herodotus] |- Strategy [Standard ErrorCodeMapper Builder Customizer] Configure.");
return customizer;
}
@Bean
public ErrorCodeMapperBuilder errorCodeMapperBuilder(List<ErrorCodeMapperBuilderCustomizer> customizers) {
ErrorCodeMapperBuilder builder = new ErrorCodeMapperBuilder();
customize(builder, customizers);
log.debug("[Herodotus] |- Bean [Error Code Mapper Builder] Configure.");
return builder;
}
private void customize(ErrorCodeMapperBuilder builder, List<ErrorCodeMapperBuilderCustomizer> customizers) {
for (ErrorCodeMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
@Bean
public ErrorCodeMapper errorCodeMapper(ErrorCodeMapperBuilder builder) {
ErrorCodeMapper mapper = builder.build();
log.debug("[Herodotus] |- Bean [Error Code Mapper] Configure.");
return mapper;
}
}
[四]错误体系的统一
前面章节讲解了 Dante Cloud 自定义错误体系的设计与实现,但是也仅仅是实现自定义错误体系以及相关的配置,还不足以让这个错误体系运转起来。而且这也只是实现了自定义错误部分,还无法与第三方组件的 Exception 以及第三方组件抛出的继承类型 Exception 融合。
[1]Spring Boot 中的错误捕获
想要将自定义错误与第三方组件的 Exception 融合在一起,首先要解决的问题就是如何捕获第三方组件抛出的Exception。
Spring Boot 框架为我们提供了这个捕获的机制,就是利用 @ExceptionHandler
注解来实现。同时,因为微服务系统核心还是面向 REST 接口,可以配合使用 Spring Boot 提供的注解 @RestControllerAdvice
来捕获更多的错误。
@RestControllerAdvice
public class ReactiveRestControllerAdvice {
private static Result<String> resolveException(Exception ex, ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();
ServerHttpResponse response = serverWebExchange.getResponse();
Result<String> result = SecurityGlobalExceptionHandler.resolveException(ex, request.getURI().getPath());
response.setStatusCode(HttpStatusCode.valueOf(result.getStatus()));
return result;
}
private static Result<String> resolveSecurityException(Exception ex, ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();
ServerHttpResponse response = serverWebExchange.getResponse();
Result<String> result = SecurityGlobalExceptionHandler.resolveSecurityException(ex, request.getURI().getPath());
response.setStatusCode(HttpStatusCode.valueOf(result.getStatus()));
return result;
}
@ExceptionHandler({MethodArgumentNotValidException.class})
public static Result<String> validationMethodArgumentException(MethodArgumentNotValidException ex, ServerWebExchange serverWebExchange) {
return validationBindException(ex, serverWebExchange);
}
@ExceptionHandler({BindException.class})
public static Result<String> validationBindException(BindException ex, ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();
ServerHttpResponse response = serverWebExchange.getResponse();
Result<String> result = SecurityGlobalExceptionHandler.resolveException(ex, request.getURI().getPath());
BindingResult bindingResult = ex.getBindingResult();
FieldError fieldError = bindingResult.getFieldError();
//返回第一个错误的信息
if (ObjectUtils.isNotEmpty(fieldError)) {
result.validation(fieldError.getDefaultMessage(), fieldError.getCode(), fieldError.getField());
}
response.setStatusCode(HttpStatusCode.valueOf(result.getStatus()));
return result;
}
······
[2]第三方组件错误的定义
捕获了第三方组件的错误后,就需要考虑如何将这些错误信息转化为更友好的错误信息。
通过 @ExceptionHandler
捕获的 Exception,只是纯纯的 Exception 类,可以通过这个 Exception 拿到相关的信息,包括 Exception 的类信息。既然,在前文中我们已经定义了 Feedback
信息反馈类,那么想办法将 Exception 与 Feedback
进行关联,就可以解决第三方组件错误信息的转化。
在 Dante Cloud 中,定义了一个 GlobalExceptionHandler
类。在其中,定义了一个 Exception 与 Feedback
映射的字典。利用 Exception 的类名,查找对应的 Feedback
信息来实现错误信息的转化。
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final Map<String, Feedback> EXCEPTION_DICTIONARY = new HashMap<>();
static {
EXCEPTION_DICTIONARY.put("AccessDeniedException", ErrorCodes.ACCESS_DENIED);
EXCEPTION_DICTIONARY.put("InsufficientAuthenticationException", ErrorCodes.ACCESS_DENIED);
EXCEPTION_DICTIONARY.put("HttpRequestMethodNotSupportedException", ErrorCodes.HTTP_REQUEST_METHOD_NOT_SUPPORTED);
EXCEPTION_DICTIONARY.put("HttpMediaTypeNotAcceptableException", ErrorCodes.HTTP_MEDIA_TYPE_NOT_ACCEPTABLE);
EXCEPTION_DICTIONARY.put("IllegalArgumentException", ErrorCodes.ILLEGAL_ARGUMENT_EXCEPTION);
EXCEPTION_DICTIONARY.put("NullPointerException", ErrorCodes.NULL_POINTER_EXCEPTION);
EXCEPTION_DICTIONARY.put("IOException", ErrorCodes.IO_EXCEPTION);
EXCEPTION_DICTIONARY.put("HttpMessageNotReadableException", ErrorCodes.HTTP_MESSAGE_NOT_READABLE_EXCEPTION);
EXCEPTION_DICTIONARY.put("TypeMismatchException", ErrorCodes.TYPE_MISMATCH_EXCEPTION);
EXCEPTION_DICTIONARY.put("MissingServletRequestParameterException", ErrorCodes.MISSING_SERVLET_REQUEST_PARAMETER_EXCEPTION);
EXCEPTION_DICTIONARY.put("ProviderNotFoundException", ErrorCodes.PROVIDER_NOT_FOUND);
EXCEPTION_DICTIONARY.put("CookieTheftException", ErrorCodes.COOKIE_THEFT);
EXCEPTION_DICTIONARY.put("InvalidCookieException", ErrorCodes.INVALID_COOKIE);
EXCEPTION_DICTIONARY.put("BadSqlGrammarException", ErrorCodes.BAD_SQL_GRAMMAR);
EXCEPTION_DICTIONARY.put("DataIntegrityViolationException", ErrorCodes.DATA_INTEGRITY_VIOLATION);
EXCEPTION_DICTIONARY.put("TransactionRollbackException", ErrorCodes.TRANSACTION_ROLLBACK);
EXCEPTION_DICTIONARY.put("BindException", ErrorCodes.METHOD_ARGUMENT_NOT_VALID);
EXCEPTION_DICTIONARY.put("MethodArgumentNotValidException", ErrorCodes.METHOD_ARGUMENT_NOT_VALID);
EXCEPTION_DICTIONARY.put("RedisPipelineException", ErrorCodes.PIPELINE_INVALID_COMMANDS);
}
public static Result<String> resolveException(Exception ex, String path) {
log.trace("[Herodotus] |- Global Exception Handler, Path : [{}], Exception:", path, ex);
if (ex instanceof HerodotusException exception) {
Result<String> result = exception.getResult();
result.path(path);
log.error("[Herodotus] |- Global Exception Handler, Error is : {}", result);
return result;
} else {
Result<String> result = Result.failure();
String exceptionName = ex.getClass().getSimpleName();
if (StringUtils.isNotEmpty(exceptionName) && EXCEPTION_DICTIONARY.containsKey(exceptionName)) {
Feedback feedback = EXCEPTION_DICTIONARY.get(exceptionName);
result = Result.failure(feedback, exceptionName);
} else {
log.warn("[Herodotus] |- Global Exception Handler, Can not find the exception name [{}] in dictionary, please do optimize ", exceptionName);
}
result.path(path);
result.stackTrace(ex.getStackTrace());
result.detail(ex.getMessage());
log.error("[Herodotus] |- Global Exception Handler, Error is : {}", result);
return result;
}
}
}
将 @ExceptionHandler
捕获的 Exception 传递给 GlobalExceptionHandler
类,就实现了整体错误体系的统一。
提示
第三方组件到底会抛出哪些 Exception,以及会在什么场景下抛出哪个 Exception,是很难做到穷举的。所以, GlobalExceptionHandler
中的字典,仅是记录了已经遇到过的 Exception。对于新的、未曾遇到过的错误,在代码中会以日志的形式输出告警,发现这种类型的日志在手动补充进来
[3]特殊组件错误的定义
GlobalExceptionHandler
中定义的错误,只是 Spring Boot 基础组件中会遇到的 Exception,对于像 OAuth2 和 Spring Security 这类特殊应用的组件,又会抛出不同的错误类型,特别是还会存在集成关系错误。
所以,Dante Cloud 在 GlobalExceptionHandler
之上,又定义了一层 SecurityGlobalExceptionHandler
,专门用于处理 OAuth2 和 Spring Security 相关的错误。
之所以,没有将 GlobalExceptionHandler
和 SecurityGlobalExceptionHandler
合并在一个类中,主要考虑的是 GlobalExceptionHandler
和 SecurityGlobalExceptionHandler
涉及的底层依赖不同,划分成两个类型,可以更方便的划分模块,减少不必要的依赖。