前言
记录 Dubbo 对于自定义异常的处理方式.
实现目标
服务层异常,直接向上层抛出,web 层统一捕获处理
如果是系统自定义异常,则返回{“code”:xxx,”msg”:yyy} 其中 code 对应为错误码,msg 对应为异常信息
如果非系统自定义异常,返回{“code”:-1,”msg”:” 未知错误 ”},同时将异常堆栈信息输出到日志, 便于定位问题
项目架构
先来张系统架构图吧,这张图来源自网络,相信现在大部分中小企业的分布式集群架构都是类似这样的设计:
简要说明下分层架构:
通常情况下会有专门一台堡垒机做统一的代理转发,客户端 (pc,移动端等) 访问由 nginx 统一暴露的入口
nginx 反向代理,负载均衡到 web 服务器,由 tomcat 组成的集群,web 层仅仅是作为接口请求的入口,没有实际的业务逻辑
web 层再用 rpc 远程调用注册到 zookeeper 的 dubbo 服务集群,dubbo 服务与数据层交互,处理业务逻辑
前后端分离,使用 json 格式做数据交互,格式可以统一如下:
{
“code”: 200, // 状态码:200 成功,其他为失败
“msg”: “success”, // 消息,成功为 success,其他为失败原因
“data”: object // 具体的数据内容,可以为任意格式
}
映射为 javabean 可以统一定义为:
/**
* @program: easywits
* @description: http 请求 返回的最外层对象
* @author: zhangshaolin
* @create: 2018-04-27 10:43
**/
@Data
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class BaseResult<T> implements Serializable{
private static final long serialVersionUID = -6959952431964699958L;
/**
* 状态码:200 成功,其他为失败
*/
public Integer code;
/**
* 成功为 success,其他为失败原因
*/
public String msg;
/**
* 具体的内容
*/
public T data;
}
返回结果工具类封装:
/**
* @program: easywits
* @description: http 返回结果工具类
* @author: zhangshaolin
* @create: 2018-07-14 13:38
**/
public class ResultUtil {
/**
* 访问成功时调用 包含 data
* @param object
* @return
*/
public static BaseResult success(Object object){
BaseResult result = new BaseResult();
result.setCode(200);
result.setMsg(“success”);
result.setData(object);
return result;
}
/**
* 访问成功时调用 不包含 data
* @return
*/
public static BaseResult success(){
return success(null);
}
/**
* 返回异常情况 不包含 data
* @param code
* @param msg
* @return
*/
public static BaseResult error(Integer code,String msg){
BaseResult result = new BaseResult();
result.setCode(code);
result.setMsg(msg);
return result;
}
/**
* 返回异常情况 包含 data
* @param resultEnum 结果枚举类 统一管理 code msg
* @param object
* @return
*/
public static BaseResult error(ResultEnum resultEnum,Object object){
BaseResult result = error(resultEnum);
result.setData(object);
return result;
}
/**
* 全局基类自定义异常 异常处理
* @param e
* @return
*/
public static BaseResult error(BaseException e){
return error(e.getCode(),e.getMessage());
}
/**
* 返回异常情况 不包含 data
* @param resultEnum 结果枚举类 统一管理 code msg
* @return
*/
public static BaseResult error(ResultEnum resultEnum){
return error(resultEnum.getCode(),resultEnum.getMsg());
}
}
因此,模拟一次前端调用请求的过程可以如下:
web 层接口
@RestController
@RequestMapping(value = “/user”)
public class UserController {
@Autowired
UserService mUserService;
@Loggable(descp = “ 用户个人资料 ”, include = “”)
@GetMapping(value = “/info”)
public BaseResult userInfo() {
return mUserService.userInfo();
}
}
服务层接口
@Override
public BaseResult userInfo() {
UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
return ResultUtil.success(userInfoVo);
}
自定义系统异常
定义一个自定义异常,用于手动抛出异常信息,注意这里基础 RuntimeException 为未受检异常:
简单说明,RuntimeException 及其子类为未受检异常,其他异常为受检异常,未受检异常是运行时抛出的异常,而受检异常则在编译时则强则报错
public class BaseException extends RuntimeException{
private Integer code;
public BaseException() {
}
public BaseException(ResultEnum resultEnum) {
super(resultEnum.getMsg());
this.code = resultEnum.getCode();
}
… 省略 set get 方法
}
为了方便对结果统一管理,定义一个结果枚举类:
public enum ResultEnum {
UNKNOWN_ERROR(-1, “o(╥﹏╥)o~~ 系统出异常啦!, 请联系管理员!!!”),
SUCCESS(200, “success”);
private Integer code;
private String msg;
ResultEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
web 层统一捕获异常
定义 BaseController 抽象类,统一捕获由服务层抛出的异常,所有新增 Controller 继承该类即可。
public abstract class BaseController {
private final static Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
/**
* 统一异常处理
*
* @param e
*/
@ExceptionHandler()
public Object exceptionHandler(Exception e) {
if (e instanceof BaseException) {
// 全局基类自定义异常, 返回{code,msg}
BaseException baseException = (BaseException) e;
return ResultUtil.error(baseException);
} else {
LOGGER.error(“ 系统异常: {}”, e);
return ResultUtil.error(ResultEnum.UNKNOWN_ERROR);
}
}
}
验证
以上 web 层接口 UserController 继承 BaseController,统一捕获异常
服务层假设抛出自定义系统异常 BaseException,代码如下:
@Override
public BaseResult userInfo() {
UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
if (userInfoVo != null) {
// 这里假设抛个自定义异常, 返回结果{code:10228 msg:” 用户存在!”}
throw new BaseException(ResultEnum.USER_EXIST);
}
return ResultUtil.success(userInfoVo);
}
然而调用结果后,上层捕获到的异常却不是 BaseException,而被认为了未知错误抛出了.带着疑问看看 Dubbo 对于异常的处理
Dubbo 异常处理
Dubbo 对于异常有统一的拦截处理,以下是 Dubbo 异常拦截器主要代码:
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
// 服务调用
Result result = invoker.invoke(invocation);
// 有异常,并且非泛化调用
if (result.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = result.getException();
// directly throw if it’s checked exception
// 如果是 checked 异常,直接抛出
if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
return result;
}
// directly throw if the exception appears in the signature
// 在方法签名上有声明,直接抛出
try {
Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
Class<?>[] exceptionClassses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClassses) {
if (exception.getClass().equals(exceptionClass)) {
return result;
}
}
} catch (NoSuchMethodException e) {
return result;
}
// 未在方法签名上定义的异常,在服务器端打印 ERROR 日志
// for the exception not found in method’s signature, print ERROR message in server’s log.
logger.error(“Got unchecked and undeclared exception which called by ” + RpcContext.getContext().getRemoteHost()
+ “. service: ” + invoker.getInterface().getName() + “, method: ” + invocation.getMethodName()
+ “, exception: ” + exception.getClass().getName() + “: ” + exception.getMessage(), exception);
// 异常类和接口类在同一 jar 包里,直接抛出
// directly throw if exception class and interface class are in the same jar file.
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return result;
}
// 是 JDK 自带的异常,直接抛出
// directly throw if it’s JDK exception
String className = exception.getClass().getName();
if (className.startsWith(“java.”) || className.startsWith(“javax.”)) {
return result;
}
// 是 Dubbo 本身的异常,直接抛出
// directly throw if it’s dubbo exception
if (exception instanceof RpcException) {
return result;
}
// 否则,包装成 RuntimeException 抛给客户端
// otherwise, wrap with RuntimeException and throw back to the client
return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
} catch (Throwable e) {
logger.warn(“Fail to ExceptionFilter when called by ” + RpcContext.getContext().getRemoteHost()
+ “. service: ” + invoker.getInterface().getName() + “, method: ” + invocation.getMethodName()
+ “, exception: ” + e.getClass().getName() + “: ” + e.getMessage(), e);
return result;
}
}
// 返回
return result;
} catch (RuntimeException e) {
logger.error(“Got unchecked and undeclared exception which called by ” + RpcContext.getContext().getRemoteHost()
+ “. service: ” + invoker.getInterface().getName() + “, method: ” + invocation.getMethodName()
+ “, exception: ” + e.getClass().getName() + “: ” + e.getMessage(), e);
throw e;
}
}
简要说明:
有异常,并且非泛化调用时,如果是受检异常,则直接抛出
有异常,并且非泛化调用时,在方法签名上有声明,则直接抛出
有异常,并且非泛化调用时,异常类和接口类在同一 jar 包里,则直接抛出
有异常,并且非泛化调用时,是 Dubbo 本身的异常(RpcException),则直接抛出
有异常,并且非泛化调用时,剩下的情况,全部都会包装成 RuntimeException 抛给客户端
到现在问题很明显了,我们自定义的 BaseException 为未受检异常,况且不符合 Dubbo 异常拦截器中直接抛出的要求,Dubbo 将其包装成了 RuntimeException,所以在上层 BaseController 中统一捕获为系统未知错误了.
解决办法
异常类 BaseException 和接口类在同一 jar 包里,但是这种方式要在每个 jar 中放置一个异常类,不好统一维护管理
在接口方法签名上显式声明抛出 BaseException,这种方式相对简单一些,比较好统一维护,只是每个接口都要显式声明一下异常罢了,这里我选择这种方式解决
问题
为什么一定要抛出自定义异常来中断程序运行,用 return ResultUtil.error(ResultEnum resultEnum) 强制返回 {code:xxx msg:xxx} 结果,不是一样可以强制中断程序运行?
玩过 Spring 的肯定都知道,Spring 哟声明式事物的概念,即在接口中添加事物注解,当发生异常时,全部接口执行事物回滚..看下方的伪代码:
@Transactional(rollbackFor = Exception.class)
public BaseResult handleData(){
//1. 操作数据库,新增数据表A一条数据,返回新增数据主键 id
//2. 操作数据库,新增数据库B一条数据,以数据表A主键 id 为外键关联
//3. 执行成功 返回结果
}
该接口声明了异常事物回滚,发送异常时会全部回滚
步骤1数据入库失败,理论上是拿不到主键 id 的,此时应当抛出自定义异常,提示操作失败
如果步骤1数据入库成功,步骤2中数据入库失败,那么理论上步骤1中的数据应当也要回滚,如果此时强制返回异常结果,那么步骤1入库数据则成为脏数据,此时抛出自定义异常是最合理的
最后的思考
在实际问题场景中去阅读源码是最合适的,带着问题有目的的看指定源码会让人有豁然开朗的感觉.
更多原创文章会第一时间推送公众号【张少林同学】,欢迎关注!