fail(ServiceException exception) {
return fail(exception.getMessageEnum(), exception.getMessage());
}
}
```
---
[^开发者的误区]: 在维也纳的项目代码里,`controller`层,到处可见使用`fastjson`手动转换对象成`String`返回,这是开发者使用上的误区,为了`Json`而`Json`,而其实`spring`已经帮开发者做完了这些事情,开发者只需要关注自己需要的对象是什么就🆗,如果我想支持`xml`呢?如果我想同时支持`json`和`xml`呢?我们知道是应该可以根据`@RequestMapping`的`producers`和`consumers`向请求方提供适应的数据样式的(`Content-Type`)。
### 异常
**背景**
异常/错误是开发不得不要面对的,如何优雅简单的处理异常/错误呢?
异常无非2种,一种运行时错误,即开发未预知的错误;一种已知/可预知异常,某段程序可能抛出某类异常,开发提前预知。后者还可分为正常的异常和程序运行错误的异常。
相应的,处理方式通常如下[^异常和日志]:
1. 运行时异常,运行时拦截并记录堆栈(最简单的比如空指针)
2. 已知正常异常,抛出返回(如参数校验失败不允许执行的)
3. 可预知异常,捕获忽略(如依赖不重要的组件,异常不影响主程序)
服务的异常通常不暴露给请求方,需要转换为友好的错误提示。
**设计**
异常包含错误码和`Exception`。主要参考《Java开发手册》使用较通用的错误码定义和业务`ServiceException`。
`@RestControllerAdvice`结合`@ExceptionHandler`处理运行时异常,返回友好提示。`AbstractErrorController`处理请求路径错误。
**实现**
```java
package com.yundasys.bizframework.business.error.code;
import lombok.ToString;
/**
* 参考Java开发手册,错误码:
*
* 1. 【强制】错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号
* 说明:错误产生来源分为 A/B/C,
* A 表示错误来源于用户,比如参数错误,用户安装版本过低等问题;
* B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;
* C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;
* 四位数字编号从 0001 到 9999,大类之间的步长间距预留 100
*
* 2. 【强制】全部正常,但不得不填充错误码时返回五个零:00000
*
* 3. 【参考】错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。
* 说明:在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,
* 分别是:A0001(用户端错误)、B0001(系统执行出错)、C0001(调用第三方服务出错)。
*
* @author wenxy
* @date 2020/9/28
*/
@ToString
public enum ErrorCode implements MessageEnum {
/*
* 一级宏观码
*/
SUCCESS("00000", "操作成功"),
A0001("A0001", "用户端错误"),
B0001("B0001", "系统执行出错"),
C0001("C0001", "调用第三方服务出错"),
/*
* 二级、三级宏观码
*/
/****************************************************************/
// 用户
A0100("A0100", "用户注册失败"),
A0200("A0200", "用户登录异常"),
A0300("A0300", "访问权限异常"),
A0301("A0301", "访问未授权"),
A0303("A0303", "用户授权申请被拒绝"),
A0310("A0310", "因访问对象隐私设置被拦截"),
A0311("A0311", "授权已过期"),
A0312("A0312", "无权限使用API"),
A0320("A0320", "用户访问被拦截"),
A0321("A0321", "黑名单用户"),
A0322("A0322", "账号被冻结"),
A0323("A0323", "非法IP地址"),
A0324("A0324", "网关访问受限"),
A0330("A0330", "服务已欠费"),
A0340("A0340", "用户签名异常"),
A0341("A0341", "RSA签名错误"),
A0400("A0400", "用户请求参数错误"),
A0402("A0402", "无效的用户输入"),
A0410("A0410", "请求必填参数为空"),
A0411("A0411", "用户订单号为空"),
A0413("A0413", "缺少时间戳参数"),
A0414("A0414", "非法的时间戳参数"),
A0420("A0420", "请求参数值超出允许的范围"),
A0421("A0421", "参数格式不匹配"),
A0422("A0422", "地址不在服务范围"),
A0423("A0423", "时间不在服务范围"),
A0424("A0424", "金额超出限制"),
A0425("A0425", "数量超出限制"),
A0426("A0426", "请求批量处理总个数超出限制"),
A0427("A0427", "请求JSON解析失败"),
A0430("A0430", "用户输入内容非法"),
A0440("A0440", "用户操作异常"),
A0500("A0500", "用户请求服务异常"),
A0501("A0501", "请求次数超出限制"),
A0502("A0502", "请求并发数超出限制"),
A0506("A0506", "用户重复请求"),
A0600("A0600", "用户资源异常"),
A0605("A0605", "用户配额已用光"),
A0700("A0700", "用户上传文件异常"),
A0800("A0800", "用户当前版本异常"),
A0900("A0900", "用户隐私未授权"),
A1000("A1000", "用户设备异常"),
/*****************************************************************/
// 系统
B0100("B0100", "系统执行超时"),
B0101("B0101", "系统订单处理超时"),
B0200("B0200", "系统容灾功能被触发"),
B0210("B0210", "系统限流"),
B0220("B0220", "系统功能降级"),
B0300("B0300", "系统资源异常"),
B0310("B0310", "系统资源耗尽"),
B0320("B0320", "系统资源访问异常"),
B0321("B0321", "系统读取磁盘文件失败"),
/****************************************************************/
// 第三方
C0100("C0100", "中间件服务出错"),
C0110("C0110", "RPC服务出错"),
C0111("C0111", "RPC服务未找到"),
C0112("C0112", "RPC服务未注册"),
C0113("C0113", "接口不存在"),
C0120("C0120", "消息服务出错"),
C0121("C0121", "消息投递出错"),
C0122("C0122", "消息消费出错"),
C0123("C0123", "消息订阅出错"),
C0124("C0124", "消息分组未查到"),
C0130("C0130", "缓存服务出错"),
C0131("C0131", "key长度超过限制"),
C0132("C0132", "value长度超过限制"),
C0133("C0133", "存储容量已满"),
C0134("C0134", "不支持的数据格式"),
C0140("C0140", "配置服务出错"),
C0150("C0150", "网络资源服务出错"),
C0151("C0151", "VPN服务出错"),
C0152("C0152", "CDN服务出错"),
C0153("C0153", "域名解析服务出错"),
C0154("C0154", "网关服务出错"),
C0200("C0200", "第三方系统执行超时"),
C0210("C0210", "RPC执行超时"),
C0220("C0220", "消息投递超时"),
C0230("C0230", "缓存服务超时"),
C0240("C0240", "配置服务超时"),
C0250("C0250", "数据库服务超时"),
C0300("C0300", "数据库服务出错"),
C0311("C0311", "表不存在"),
C0321("C0321", "多表关联中存在多个相同名称的列"),
C0331("C0331", "数据库死锁"),
C0341("C0341", "主键冲突"),
C0400("C0400", "第三方容灾系统被触发"),
C0401("C0401", "第三方系统限流"),
C0402("C0402", "第三方功能降级"),
C0500("C0500", "通知服务出错"),
C0501("C0501", "短信提醒服务失败"),
C0502("C0502", "语音提醒服务失败"),
C0503("C0503", "邮件提醒服务失败"),
;
final String code;
final String msg;
ErrorCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public String code() {
return code;
}
@Override
public String msg() {
return msg;
}
public static boolean isSuccess(String code) {
return SUCCESS.code.equals(code);
}
}
```
```java
package com.yundasys.bizframework.business.error.exception;
import com.yundasys.bizframework.business.error.code.MessageEnum;
/**
* ServiceException
*
* @author wenxy
* @date 2020/9/28
*/
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = -2351852312234556372L;
MessageEnum messageEnum;
public ServiceException(MessageEnum messageEnum, String s) {
// 2020/12/15 不传递自身异常堆栈
this(messageEnum, s, null);
}
public ServiceException(MessageEnum messageEnum, String s, Throwable throwable) {
super(s, throwable);
this.messageEnum = messageEnum;
}
public MessageEnum getMessageEnum() {
return messageEnum;
}
public void setMessageEnum(MessageEnum messageEnum) {
this.messageEnum = messageEnum;
}
}
```
```java
package com.yundasys.bizframework.web.error.handle;
import com.yundasys.bizframework.business.error.code.ErrorCode;
import com.yundasys.bizframework.business.error.exception.DaoException;
import com.yundasys.bizframework.business.error.exception.ServiceException;
import com.yundasys.bizframework.business.web.controller.dto.R;
import com.yundasys.bizframework.log.common.util.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;
/**
* 控制器异常处理
*
* 1. 处理web框架类异常、自定义异常
* 2. 返回A类错误码
* × 2.1 对于B、C类错误,直接返回
* × 2.2 记录日志,封装A类错误码返回
*
* @author wenxy
* @date 2020/10/26
*/
@RestControllerAdvice
@Slf4j
public class ControllerErrorHandler extends ResponseEntityExceptionHandler {
/**
* 第三方调用C类异常-服务端-第三方错误、第二类B类异常-服务端错误
*
* @param daoException daoException
*
* @return ResponseEntity>
*/
@ExceptionHandler(DaoException.class)
public ResponseEntity> handleException(DaoException daoException) {
ServiceException serviceException = new ServiceException(daoException.getMessageEnum(), daoException.getMessage());
return handleException(serviceException);
}
/**
* 第三方调用C类异常-服务端-第三方错误、第二类B类异常-服务端错误
*
* @param serviceException
*
* @return
*/
@ExceptionHandler(ServiceException.class)
public ResponseEntity> handleException(ServiceException serviceException) {
// B/C类异常在DaoLogAspect/ServiceLogAspect已经处理并打印堆栈,这里冗余简单记录,防止手动异常错误使用,如在Service/controller抛出daoException,在controller抛出service不被处理
logWarnSimple(serviceException);
return ResponseEntity.ok(R.fail(serviceException));
}
/**
* A类异常-客户端错误 覆盖ResponseEntityExceptionHandler,返回统一的异常响应
*
* @param ex @param ex
* @param body body
* @param headers headers
* @param status status
* @param request request
*
* @return R
*/
@Override
protected ResponseEntity