主题
SDK 错误处理
本文档说明一单位一平台 SDK 的异常处理机制和常见错误的处理方法。
异常体系
SDK 使用了三层异常体系:
SdkException (SDK 基础异常)
├── CryptoException (加密异常)
└── ApiException (API 调用异常)异常类型
SdkException
SDK 基础异常,所有 SDK 异常的父类。
java
package cn.hztech.sdk.exception;
public class SdkException extends RuntimeException {
private String errorCode;
public SdkException(String message) {
super(message);
}
public SdkException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}CryptoException
加密相关异常,在加密、解密、签名、验签过程中抛出。
java
package cn.hztech.sdk.exception;
public class CryptoException extends SdkException {
public CryptoException(String message) {
super(message);
}
public CryptoException(String message, Throwable cause) {
super(message, cause);
}
}常见场景:
- 密钥格式错误
- 密钥加载失败
- 加密算法初始化失败
- 加密数据格式错误
ApiException
API 调用异常,在调用平台接口时抛出。
java
package cn.hztech.sdk.exception;
public class ApiException extends SdkException {
public ApiException(String message) {
super(message);
}
public ApiException(String errorCode, String message) {
super(errorCode, message);
}
}常见场景:
- 网络连接失败
- 请求超时
- 服务器返回错误
- 业务逻辑错误
错误码
通用错误码
| 错误码 | 说明 | HTTP 状态码 |
|---|---|---|
| 1000 | 成功 | 200 |
| 1001 | 请求参数错误 | 400 |
| 1002 | 缺少必填参数 | 400 |
| 1003 | 签名验证失败 | 401 |
| 1004 | 解密失败 | 401 |
| 1005 | 应用 ID 不存在 | 403 |
| 1006 | 时间戳验证失败 | 403 |
| 1007 | Nonce 重复 | 403 |
| 1008 | 请求已过期 | 403 |
| 1999 | 系统错误 | 500 |
业务错误码
人员管理
| 错误码 | 说明 | HTTP 状态码 |
|---|---|---|
| 2501 | 用户不存在 | 404 |
| 2502 | 用户已存在 | 409 |
| 2503 | 用户状态异常 | 403 |
部门管理
| 错误码 | 说明 | HTTP 状态码 |
|---|---|---|
| 2601 | 部门不存在 | 404 |
| 2602 | 部门已存在 | 409 |
| 2603 | 部门存在子部门 | 409 |
角色管理
| 错误码 | 说明 | HTTP 状态码 |
|---|---|---|
| 2701 | 角色不存在 | 404 |
| 2702 | 角色已存在 | 409 |
全局异常处理
使用 @RestControllerAdvice
推荐在 Spring Boot 项目中使用全局异常处理器来统一处理 SDK 异常:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理 API 异常
*/
@ExceptionHandler(ApiException.class)
public Result<Void> handleApiException(ApiException e) {
log.error("API 调用异常: {}", e.getMessage(), e);
String errorCode = e.getErrorCode();
// 根据错误码返回不同的提示信息
if (StringUtils.hasText(errorCode)) {
switch (errorCode) {
case "2501":
return Result.error(404, "用户不存在");
case "2502":
return Result.error(409, "用户已存在");
case "2503":
return Result.error(403, "用户状态异常");
case "2601":
return Result.error(404, "部门不存在");
case "2602":
return Result.error(409, "部门已存在");
case "2701":
return Result.error(404, "角色不存在");
case "2702":
return Result.error(409, "角色已存在");
case "1003":
return Result.error(401, "签名验证失败");
case "1004":
return Result.error(401, "解密失败");
case "1005":
return Result.error(403, "应用 ID 不存在");
case "1006":
return Result.error(403, "时间戳验证失败");
case "1007":
return Result.error(403, "Nonce 重复");
case "1008":
return Result.error(403, "请求已过期");
default:
return Result.error(500, "系统错误: " + e.getMessage());
}
}
return Result.error(500, "API 调用失败: " + e.getMessage());
}
/**
* 处理加密异常
*/
@ExceptionHandler(CryptoException.class)
public Result<Void> handleCryptoException(CryptoException e) {
log.error("加密异常: {}", e.getMessage(), e);
return Result.error(500, "数据处理异常");
}
/**
* 处理 SDK 基础异常
*/
@ExceptionHandler(SdkException.class)
public Result<Void> handleSdkException(SdkException e) {
log.error("SDK 异常: {}", e.getMessage(), e);
return Result.error(500, "SDK 异常: " + e.getMessage());
}
}错误响应格式
java
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}常见错误处理
1. 用户不存在
错误码:2501
处理方法:
java
try {
User user = userApiClient.getDetail(userId);
return Result.success(user);
} catch (ApiException e) {
if (e.getMessage().contains("2501")) {
return Result.error(404, "用户不存在");
}
throw e;
}2. 签名验证失败
错误码:1003
可能原因:
- 密钥配置错误
- 密钥被修改
- 密钥格式不正确
处理方法:
java
try {
// 调用 API
} catch (ApiException e) {
if (e.getMessage().contains("1003")) {
log.error("签名验证失败,请检查密钥配置");
return Result.error(401, "认证失败");
}
throw e;
}3. 解密失败
错误码:1004
可能原因:
- 私钥配置错误
- 加密数据被篡改
处理方法:
java
try {
// 调用 API
} catch (ApiException e) {
if (e.getMessage().contains("1004")) {
log.error("解密失败,请检查私钥配置");
return Result.error(401, "解密失败");
}
throw e;
}4. 应用 ID 不存在
错误码:1005
可能原因:
- 应用 ID 配置错误
- 应用未在平台注册
处理方法:
java
try {
// 调用 API
} catch (ApiException e) {
if (e.getMessage().contains("1005")) {
log.error("应用 ID 不存在,请检查配置");
return Result.error(403, "应用未授权");
}
throw e;
}5. 时间戳验证失败
错误码:1006
可能原因:
- 系统时间不同步
- 时间误差超过容差范围
处理方法:
java
try {
// 调用 API
} catch (ApiException e) {
if (e.getMessage().contains("1006")) {
log.error("时间戳验证失败,请同步系统时间");
return Result.error(403, "时间验证失败");
}
throw e;
}解决方案:
- 使用 NTP 服务同步时间
- 增加
timestamp-tolerance配置值 - 检查系统时区设置
6. Nonce 重复
错误码:1007
可能原因:
- 短时间内重复请求
- Nonce 生成器配置错误
处理方法:
SDK 自动处理 Nonce,正常情况不会出现此错误。如果频繁出现,请检查:
- 是否存在代码逻辑错误导致的重复调用
- 是否使用了自定义的 Nonce 生成器
7. 连接超时
错误信息:Connect timeout
可能原因:
- 网络不通
- 防火墙阻止
- 服务器地址错误
处理方法:
java
try {
// 调用 API
} catch (ApiException e) {
if (e.getMessage().contains("timeout")) {
log.error("连接超时: {}", e.getMessage());
return Result.error(504, "连接超时");
}
throw e;
}解决方案:
- 检查网络连接
- 增加
connect-timeout配置值 - 检查防火墙设置
- 确认网关地址正确
8. 加密异常
异常类型:CryptoException
可能原因:
- 密钥格式错误
- 密钥加载失败
- 加密算法初始化失败
处理方法:
java
try {
// 调用 API
} catch (CryptoException e) {
log.error("加密异常: {}", e.getMessage(), e);
return Result.error(500, "数据处理异常");
}解决方案:
- 检查密钥格式是否正确(PEM 格式)
- 确认密钥包含完整的头尾标记
- 检查密钥是否已损坏
日志记录
记录异常日志
java
@Slf4j
@Service
public class UserService {
@Autowired
private UserApiClient userApiClient;
public User getUser(String userId) {
try {
return userApiClient.getDetail(userId);
} catch (ApiException e) {
log.error("查询用户失败, userId: {}, error: {}", userId, e.getMessage(), e);
throw new BusinessException("查询用户失败");
}
}
}启用 SDK 调试日志
在配置文件中设置:
yaml
logging:
level:
cn.hztech.sdk: DEBUG调试日志会记录:
- 请求详情(URL、参数、加密后的报文)
- 响应详情(状态码、解密后的数据)
- 加解密过程
- 重试记录
重试机制
SDK 内置了自动重试机制,配置方法详见 配置说明文档。
重试策略
SDK 会在以下情况自动重试:
- 网络连接失败
- 读取超时
- 5xx 服务器错误
注意:4xx 客户端错误(如参数错误、用户不存在)不会重试。
自定义重试逻辑
如果需要自定义重试逻辑,可以捕获异常并手动重试:
java
@Service
public class UserService {
@Autowired
private UserApiClient userApiClient;
@Retryable(value = {ApiException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public User getUserWithRetry(String userId) {
return userApiClient.getDetail(userId);
}
}使用 Spring Retry 注解需要添加依赖:
xml
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>最佳实践
1. 使用全局异常处理器
统一处理所有异常,避免在业务代码中大量 try-catch。
2. 记录详细的错误日志
记录错误信息时,应包括:
- 错误码
- 错误消息
- 请求参数
- 堆栈跟踪
java
log.error("API 调用失败, code: {}, message: {}, params: {}",
e.getErrorCode(), e.getMessage(), params, e);3. 友好的错误提示
向客户端返回友好的错误提示,不要直接暴露技术细节。
java
// 不好的做法
return Result.error(500, "CryptoException: Invalid key format");
// 好的做法
log.error("密钥格式错误: {}", e.getMessage());
return Result.error(500, "系统异常,请联系管理员");4. 区分可重试和不可重试的错误
- 可重试:网络超时、5xx 错误
- 不可重试:参数错误、资源不存在
5. 监控和告警
对异常进行监控和告警:
java
@ExceptionHandler(ApiException.class)
public Result<Void> handleApiException(ApiException e) {
// 记录监控指标
meterRegistry.counter("sdk.api.errors",
"error_code", e.getErrorCode()).increment();
// 严重错误发送告警
if (isCriticalError(e.getErrorCode())) {
alertService.sendAlert("SDK 调用失败", e.getMessage());
}
return Result.error(500, "系统错误");
}