添加异常转换
概述
Java 中 Exception 是很重要的一个组成部分。Exception 也并不是洪水猛兽
,在可预期的代码逻辑中合理的使用 Exception 可以增强代码的健壮性,包含丰富信息的以及合理的 Exception 可以帮助开发人员快速定位问题。
日常开发中,我们常见的异常类型主要有两种:编译时异常(Exception)和运行时异常(RuntimeException)。
- 编译时异常:Exception及其子类(除了RuntimeException),在编译时期抛出的异常,在编译期间检查程序是否可能会出现问题。如果可能会有,则预先防范。这一类异常如果不加处理,通常会导致组件或者应用无法启动
- 运行时异常(Runtime exception) 是用 RuntimeException 类表示的,它描述的是程序设计错误。RuntimeException 的任何子类都无需在 throws 子句中进行声明,指的就是这些问题不需要提前被预防(本质上也可以的,只不过没必要),因为只有在真正运行的时候才能发现是否发生问题,一旦在运行期间发生了问题,则一般不会修正。这一类异常更多的用于“提示”和“提醒”
本文中所述的自定义错误代码,主要是面向于 RuntimeException,用于在程序运行期间将操作产生的异常,用更人性化的表述方式展现给使用方,以便更容易、更快捷地定位问题。就像下图所示,显示更人性化的信息比抛出一大堆让人不知所云的错误信息更好。

目前,经常提到的 Exception 大概分为以下三类:
- 系统业务代码自定义的 Exception,可能是 RuntimeException 也可能是 Exception
- 第三方模块代码抛出的 Exception,可能是 RuntimeException 也可能是 Exception
- 第三方模块代码抛出的继承类型 Exception。即,抛出的是第三方模块自定义 Exception 的基类,实际 Exception 信息被包含在基类 Exception
正因为 Java Exception 体系的灵活性以及第三方组件的丰富,使得想要统一处理异常变得越发复杂,这也是自定义错误码系统是 Dante Cloud 重要功能的原因。
注意
Dante Cloud 的自定义错误码体系,主要面向的错误是运行时异常(Runtime exception)。这样在调用系统 REST 接口出现错误时,可以将更人性化的错误信息,在接口的 REST Response 中显示。让定位问题更加容易便捷,同时减少不同端开发人员的沟通成本。
下面就来介绍如何在 Dante Cloud 统一的错误码体系中添加和处理各种组件、各种类型的错误。
[一]常规类型异常
这里所说的“常规类型异常”,可以是 Java 自带的、大多数组件都会使用的通用类型异常,例如:NullPointerException。也可以是第三方组件自定义的异常。
这一类异常通常是 Exception 及其子类(除了RuntimeException),比较好处理。利用 Spring Boot 自身 @RestControllerAdvice
的机制,就可以进行拦截。
具体的添加方法如下:
[1]定义错误信息
在 core-definition
模块中,找到 ErrorCodes
常量定义接口。在其中定义具体的 Exception 错误描述。例如:
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("请求的地址未通过身份认证");
......
}
提示
Dante Cloud 中,错误码体系与 HTTP 请求状态码绑定。可根据错误码的描述,对实际的错误进行分类,并使用具体的 XXXFeedback
类进行描述。例如:认证授权方面的错误,就可以使用 UnauthorizedFeedback
进行描述,使用该描述类的错误,会自动生成 401
开头的错误码。
[2]添加错误字典
定义完错误码后,在 core-foundation
模块中,找到 GlobalExceptionHandler
类。在其中将之前定义的错误码,添加至 EXCEPTION_DICTIONARY
Map 中。
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("BadSqlGrammarException", ErrorCodes.BAD_SQL_GRAMMAR);
EXCEPTION_DICTIONARY.put("BindException", ErrorCodes.METHOD_ARGUMENT_NOT_VALID);
EXCEPTION_DICTIONARY.put("CookieTheftException", ErrorCodes.COOKIE_THEFT);
EXCEPTION_DICTIONARY.put("DataIntegrityViolationException", ErrorCodes.DATA_INTEGRITY_VIOLATION);
EXCEPTION_DICTIONARY.put("HttpMediaTypeNotAcceptableException", ErrorCodes.HTTP_MEDIA_TYPE_NOT_ACCEPTABLE);
EXCEPTION_DICTIONARY.put("HttpMessageNotReadableException", ErrorCodes.HTTP_MESSAGE_NOT_READABLE_EXCEPTION);
EXCEPTION_DICTIONARY.put("HttpRequestMethodNotSupportedException", ErrorCodes.HTTP_REQUEST_METHOD_NOT_SUPPORTED);
EXCEPTION_DICTIONARY.put("IllegalArgumentException", ErrorCodes.ILLEGAL_ARGUMENT_EXCEPTION);
EXCEPTION_DICTIONARY.put("InsufficientAuthenticationException", ErrorCodes.ACCESS_DENIED);
EXCEPTION_DICTIONARY.put("InvalidCookieException", ErrorCodes.INVALID_COOKIE);
EXCEPTION_DICTIONARY.put("IOException", ErrorCodes.IO_EXCEPTION);
EXCEPTION_DICTIONARY.put("MethodArgumentNotValidException", ErrorCodes.METHOD_ARGUMENT_NOT_VALID);
EXCEPTION_DICTIONARY.put("MissingServletRequestParameterException", ErrorCodes.MISSING_SERVLET_REQUEST_PARAMETER_EXCEPTION);
EXCEPTION_DICTIONARY.put("NoResourceFoundException", ErrorCodes.NO_RESOURCE_FOUND_EXCEPTION);
EXCEPTION_DICTIONARY.put("NullPointerException", ErrorCodes.NULL_POINTER_EXCEPTION);
EXCEPTION_DICTIONARY.put("ProviderNotFoundException", ErrorCodes.PROVIDER_NOT_FOUND);
EXCEPTION_DICTIONARY.put("RedisPipelineException", ErrorCodes.PIPELINE_INVALID_COMMANDS);
EXCEPTION_DICTIONARY.put("TypeMismatchException", ErrorCodes.TYPE_MISMATCH_EXCEPTION);
EXCEPTION_DICTIONARY.put("TransactionRollbackException", ErrorCodes.TRANSACTION_ROLLBACK);
}
......
}
说明
EXCEPTION_DICTIONARY
Map 的 Key 是以具体 Exception 类的名称作为 Key。EXCEPTION_DICTIONARY
Map 的 Value 就是上一步定义的错误码
这里使用 Map 是为了提升错误查询的效率,而且方便维护
[3]添加错误配置器
完成以上步骤后,需要将新定义的错误描述,添加至统一的错误配置器中。这一步主要的目的是让具体的错误描述生效,并且在实现错误码的自动计算。
步骤一:新建转换器
在代码中新建一个 XXXErrorCodeMapperBuilderCustomizer
类,该类需要实现 ErrorCodeMapperBuilderCustomizer
接口。然后将新建的错误描述信息,添加到其中,如下所示:
public class AccessErrorCodeMapperBuilderCustomizer implements ErrorCodeMapperBuilderCustomizer, Ordered {
@Override
public void customize(ErrorCodeMapperBuilder builder) {
builder.preconditionFailed(
AccessErrorCodes.ACCESS_CONFIG_ERROR,
AccessErrorCodes.ACCESS_HANDLER_NOT_FOUND,
AccessErrorCodes.SMS_IDENTITY_VERIFICATION_FAILED,
AccessErrorCodes.WXAPP_IDENTITY_VERIFICATION_FAILED,
AccessErrorCodes.WXMPP_IDENTITY_VERIFICATION_FAILED,
AccessErrorCodes.JUATAUTH_IDENTITY_VERIFICATION_FAILED,
AccessErrorCodes.ACCESS_PRE_PROCESS_FAILED_EXCEPTION,
AccessErrorCodes.ILLEGAL_ACCESS_ARGUMENT,
AccessErrorCodes.ILLEGAL_ACCESS_SOURCE
);
}
@Override
public int getOrder() {
return ErrorCodeMapperBuilderOrdered.ACCESS;
}
}
提示
上例中,还实现了一个 Ordered
接口。
因为实际开发中可能会有很多个模块,可能每个模块都需要定义一个 XXXErrorCodeMapperBuilderCustomizer
类,在统一进行错误码计算时,是利用 List 遍历进行计算的,这样可能出现计算顺序不一致的情况,就会导致错误码每次计算都不一致。
实现了 Ordered
接口,是为了保证某个模块中的 XXXErrorCodeMapperBuilderCustomizer
类,在计算时始终按照指定的顺序进行计算,这样保证错误码不会计算出错。
步骤二:定义配置器Bean
XXXErrorCodeMapperBuilderCustomizer
类定义完成之后,将其定义为 Bean 以保证其在启动时,可以正确注入。示例代码如下:
@Configuration(proxyBeanMethods = false)
public class AssistantAccessConfiguration {
private static final Logger log = LoggerFactory.getLogger(AssistantAccessConfiguration.class);
@PostConstruct
public void init() {
log.debug("[Herodotus] |- Module [Assistant Access] Configure.");
}
.....
@Bean
public ErrorCodeMapperBuilderCustomizer accessErrorCodeMapperBuilderCustomizer() {
AccessErrorCodeMapperBuilderCustomizer customizer = new AccessErrorCodeMapperBuilderCustomizer();
log.debug("[Herodotus] |- Strategy [Access ErrorCodeMapper Builder Customizer] Auto Configure.");
return customizer;
}
......
}
注意
上例中,
XXXErrorCodeMapperBuilderCustomizer
类 Bean 的类型用的是基础类型ErrorCodeMapperBuilderCustomizer
而不是具体的类,即ErrorCodeMapperBuilderCustomizer
子类型。- 具体的方法名使用的是
accessErrorCodeMapperBuilderCustomizer()
。不同模块中,这个方法名不能相同,否则会出现 Bean 冲突导致无法启动。
[4]注意事项
随着使用的第三方组件越来越多,常规的 Exception 定义也会越来越多。一个开发人员也不可能在开发过程中遇到所有的 Exception 情况的,是无法穷举所有 Exception 类,并且生成一个全面的 EXCEPTION_DICTIONARY
Map。
目前采取的方式是:随用随加。
如果在使用 Dante Cloud 开发或使用过程中,出现了 EXCEPTION_DICTIONARY
Map 中没有的 Exception。那么,可以自己手动将其补充进入到错误码体系中。同时,在 Dante Cloud 中为了方便发现这些未被记录的错误,会在系统日志中打印相关的 Exception 信息,方便发现和记录。如下所示:
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] |- Can not find the exception name [{}] in dictionary, please do optimize ", exceptionName);
}
另外,第三方组件中定义的 Exception 虽然很多,但开发中因为会进行大量的保护性代码的编写,实际会遇到的或者说需要提醒的错误并不是特别多。把经常会出现的 Exception 记录下来,就足以满足大量使用场景的需要了。
[二]特殊类型异常
前面提到,为了解决常规类型异常的统一,Dante Cloud 定义了统一的 GlobalExceptionHandler
类来处理。这足以满足绝大多数使用需求。
除了 GlobalExceptionHandler
类以外,在 Dante Cloud 中还有定义了一个 SecurityGlobalExceptionHandler
类。SecurityGlobalExceptionHandler
类的使用方法以及用途与 GlobalExceptionHandler
完全相同。
[1]额外处理的原因
之所以额外定义了一个 SecurityGlobalExceptionHandler
类,是因为 SecurityGlobalExceptionHandler
类主要是用于 Spring Security 以及 Spring OAuth2 等特殊组件的 Exception 处理。处于以下几点考虑,才会采取这样的处理方式:
方便模块的解耦,减少不必要的依赖
Spring Security 是一个完整的生态,整体也比较重,SecurityGlobalExceptionHandler
就依赖于 Spring Security 的部分组件中的类。如果将 SecurityGlobalExceptionHandler
和 GlobalExceptionHandler
合并在一起,就意味着在不需要使用 Spring Security 的场景下,也需要依赖部分 Spring Security 的包。不仅没有什么意义,还增加了依赖间的耦合。
Spring Security 中 Exception 类型不简单
Spring Security 生态中,很多自定义 Exception 都是继承类型的。很多场景下,只会抛出统一的自定义父类型 Exception,具体的 Exception(自定义父类型 Exception 的子类)会被包裹在父 Exception 中,需要额外的解析才能拿到具体的错误。
另外,Spring Security 生态中,很多错误都有自成体系的错误码,例如:access_denied
。这个与前面所说的常规类型异常不同。所以就需要不同的方式来定位和转换错误描述。
[2]添加方式
添加此类错误信息的方式和步骤,与前面添加常规类型异常的方式和步骤完全相同。
唯一不同的是,EXCEPTION_DICTIONARY
Map 中 Key 的值不同,如下所示:
public class SecurityGlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(SecurityGlobalExceptionHandler.class);
private static final Map<String, Feedback> EXCEPTION_DICTIONARY = new HashMap<>();
static {
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.ACCESS_DENIED, ErrorCodes.ACCESS_DENIED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INSUFFICIENT_SCOPE, ErrorCodes.INSUFFICIENT_SCOPE);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INVALID_CLIENT, ErrorCodes.INVALID_CLIENT);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INVALID_GRANT, ErrorCodes.INVALID_GRANT);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INVALID_REDIRECT_URI, ErrorCodes.INVALID_REDIRECT_URI);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INVALID_REQUEST, ErrorCodes.INVALID_REQUEST);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INVALID_SCOPE, ErrorCodes.INVALID_SCOPE);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.INVALID_TOKEN, ErrorCodes.INVALID_TOKEN);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.SERVER_ERROR, ErrorCodes.SERVER_ERROR);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.TEMPORARILY_UNAVAILABLE, ErrorCodes.TEMPORARILY_UNAVAILABLE);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.UNAUTHORIZED_CLIENT, ErrorCodes.UNAUTHORIZED_CLIENT);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.UNSUPPORTED_GRANT_TYPE, ErrorCodes.UNSUPPORTED_GRANT_TYPE);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.UNSUPPORTED_RESPONSE_TYPE, ErrorCodes.UNSUPPORTED_RESPONSE_TYPE);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.UNSUPPORTED_TOKEN_TYPE, ErrorCodes.UNSUPPORTED_TOKEN_TYPE);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.ACCOUNT_EXPIRED_EXCEPTION, ErrorCodes.ACCOUNT_EXPIRED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.BAD_CREDENTIALS_EXCEPTION, ErrorCodes.BAD_CREDENTIALS);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.CREDENTIALS_EXPIRED_EXCEPTION, ErrorCodes.CREDENTIALS_EXPIRED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.DISABLED_EXCEPTION, ErrorCodes.ACCOUNT_DISABLED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.LOCKED_EXCEPTION, ErrorCodes.ACCOUNT_LOCKED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.ACCOUNT_ENDPOINT_LIMITED_EXCEPTION, ErrorCodes.ACCOUNT_ENDPOINT_LIMITED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.USERNAME_NOT_FOUND_EXCEPTION, ErrorCodes.USERNAME_NOT_FOUND);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.SESSION_EXPIRED_EXCEPTION, ErrorCodes.SESSION_EXPIRED);
EXCEPTION_DICTIONARY.put(OAuth2ErrorKeys.NOT_AUTHENTICATED, ErrorCodes.NOT_AUTHENTICATED);
}
......
}
[三]自定义类型异常
自定义类型异常,是开发中最常见的异常类型。就是实际开发中,根据业务需要自定义的异常类型。
[1]定义错误信息
第一步还是定义错误信息,与前面“常规类型异常”中,定义错误信息的方式相同。
不同的是,自定义类型异常的错误信息,不需要像“常规类型异常”一样在 core-definition
模块中进行定义。在任意模块中,新建一个常量接口进行定义即可。例如:
public interface AccessErrorCodes {
PreconditionFailedFeedback ACCESS_CONFIG_ERROR = new PreconditionFailedFeedback("Access 模块配置错误");
PreconditionFailedFeedback ACCESS_HANDLER_NOT_FOUND = new PreconditionFailedFeedback("Access 模块接入处理器未找到错误");
PreconditionFailedFeedback ACCESS_PRE_PROCESS_FAILED_EXCEPTION = new PreconditionFailedFeedback("接入预操作失败错误");
PreconditionFailedFeedback SMS_IDENTITY_VERIFICATION_FAILED = new PreconditionFailedFeedback("手机短信验证码登录身份认证错误");
PreconditionFailedFeedback WXAPP_IDENTITY_VERIFICATION_FAILED = new PreconditionFailedFeedback("微信小程序登录身份认证错误");
PreconditionFailedFeedback WXMPP_IDENTITY_VERIFICATION_FAILED = new PreconditionFailedFeedback("微信公众号登录身份认证错误");
PreconditionFailedFeedback JUATAUTH_IDENTITY_VERIFICATION_FAILED = new PreconditionFailedFeedback("JustAuth 第三方登录身份认证错误");
PreconditionFailedFeedback ILLEGAL_ACCESS_ARGUMENT = new PreconditionFailedFeedback("社交登录参数错误");
PreconditionFailedFeedback ILLEGAL_ACCESS_SOURCE = new PreconditionFailedFeedback("社交登录Source参数错误");
}
提示
早期 Dante Cloud 版本的错误码,不管实际 Exception 类在哪个模块中,都需要在统一的一个类中进行错误描述信息的定义。这样做不仅导致错误描述信息类越来越臃肿,而且每次添加错误信息都需要修改核心组件库,然后重新编译才能生效。
现在这种方式,允许在任意模块中随意定义错误描述信息,不用每次都修改核心组件库,在具体使用时仍旧会汇总在一起使用,这就给开发打来了极大地灵活性。
[2]编写自定义异常
添加自定义异常类型,第一步就是编写一个 Exception。这个 Exception 需要继承 Dante Cloud 中统一的 PlatformRuntimeException
,如下所示:
public class AccessConfigErrorException extends PlatformRuntimeException {
public AccessConfigErrorException() {
super();
}
public AccessConfigErrorException(String message) {
super(message);
}
public AccessConfigErrorException(String message, Throwable cause) {
super(message, cause);
}
public AccessConfigErrorException(Throwable cause) {
super(cause);
}
protected AccessConfigErrorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
@Override
public Feedback getFeedback() {
return AccessErrorCodes.ACCESS_CONFIG_ERROR;
}
}
注意
在自定义异常中,将 Exception 与错误描述信息进行关联,就不需要像前面 常规类型异常
和 特殊类型异常
一样,要手动定义一个 EXCEPTION_DICTIONARY
Map
[3]定义配置器Bean
最后一步,就像前面常规类型异常
和 特殊类型异常
一样,将自定义的错误描述信息,添加至指定的错误配置器就行。
提示
不用每一个模块都定义 XXXErrorCodeMapperBuilderCustomizer
类。也可以几个模块共用一个 XXXErrorCodeMapperBuilderCustomizer
类。这完全看你自己代码的设计以及模块划分以及解耦的需要。