本文是对Java异常处理的经验总结和实践建议,欢迎提意见~
1.异常处理规范
1.1 异常的类型
受检异常(checked Exception) :继承自Exception类。强制调用方需要显示捕捉该异常,或使用throws声明要抛出该异常
非受检异常(unchecked Exception):RuntimeException或其他继承自RuntimeException的子类,通常表示程序运行时发生的错误,代码不需要处理这些异常也能通过编译,所以称作unchecked exception
1.2 何时抛出异常
检测到受检异常上抛:调用的方法声明了受检异常时,当前层无法处理,可直接抛到外层或进行包装后重新抛出
校验流程问题抛出:程序检测到非预期状态,后续流程无法继续进行。如业务参数校验失败,数据不符合预期
if(invalid){
throw new XXXException();
}
处理异常转换后重新抛出:调用的方法抛出异常时(受检或非受检),可catch后进行一定的处理,然后重新throw出去(或封装一下再throw); 不用层层声明受检异常
1.3 异常类型的选择
受检异常是和具体业务逻辑相关的异常,如“用户名找不到”,此时需要上层调用方感知,希望上层处理异常,采取措施从异常中恢复,该情形选择受检异常。
非受检异常是偶然发生的,一般是程序问题导致,可通过提高代码健壮性避免。出现该异常此时流程无法继续,需立即终止,则选择非受检异常。
1.4 异常处理
1.4.1 抛出的异常与当前层次对应
不要将底层某特定实现的受检异常用到更高的层次中去,否则导致需要层层声明或上层显示catch,不利于层次之间的隔离。例如,DAO层SQLException需要上层的业务层关心。可以利用非受检异常来封装检测异常,从而降低层次之间的耦合度。 正面例子:
public class JdbcDriverClassHelper {
public static void loadDriverClass(String driverClass) throws DataSourceException {
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
throw new DataSourceConfigException("Cannot find driver class : " + driverClass, e);
}
}
}
class DataSourceConfigException extends RuntimeException
上述ClassNotFoundException被包装后抛出DataSourceException,与JdbcDriverClassHelper的层次和职责对应,保持封装性。 在向上层抛出时,尽量进行转换,且包装业务信息与原始异常堆栈后抛出。
1.4.2 try
尽量减小 try 块的长度
将不会抛出异常、稳定的代码放到 try 块之外
1.4.3 catch
不要忽略异常, 即用空的语句块catch{}。需要忽略加注释特殊说明
不要catch最高层次的exception, 如catch(Exception ex), catch住的异常尽量都定义成checked exception
1.4.4 finally
不要在 finally 块中执行 return 语句,否则try 块中的return不会执行
在 finally 块中进行资源释放,有异常也需要try catch
1.5 异常日志
异常堆栈只记录一次 方法A中调用了方法B,方法B中调用有声明异常的方法,且A和B中都捕捉打印了异常,使得同一个异常被打印了两次。会造成消耗不必要的系统性能,不利于定位异常来源。
定义合适的日志level,配合异常报警及时发现服务问题
2.实践与建议
2.1 异常记录
通过拦截器统一打印异常堆栈, 进行统计打点
2.2 使用自定义异常
异常类型应当具体明确,根据业务场景创建不同的异常类型。 方法声明异常时加上javaDoc说明何时会抛异常,接口更友善
2.3 与前端交互的异常处理
2.3.1 SpringMVC方式
所有接口返回组合统一的Result对象,包含调用是否成功和错误信息。
public class Result{
private Boolean isSuccess;
private ErrorEnum errorReason;
}
public enum ErrorEnum{
private Integer code;
private String desc;
INVALID_ARGS(1001, "无效参数"),
……
}
不用传递异常信息,利用SpringMVC的HandlerExceptionResolver对异常统一处理,转换成Result结构并记录异常。将其转化为用户可以理解的内容
2.3.2 Thrift方式
前后端通过IDL定义交互对象,与后端服务间异常处理类似。
2.4 后端服务间的异常处理
在接口最外层用切面的方式对服务处理过程中产生的异常进行统一拦截,进行异常log打印、转换、rethrow,组装为异常code、message的形式抛出。
一般RPC交互不加栈信息,因为在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题,其实这种方式与返回Result的方式类似了,但正常返回的情况下不需要判断Result.isSuccess。
Thrift异常处理举例(thrift接口必须声明框架内置的TException,默认情况下服务抛出的其他runTimeException都会被包装为TException):
@Component
@Aspect
public class ThriftInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(ThriftInterceptor.class);
@Pointcut("execution(public * com.thrift.impl.*.*(..))")
private void thriftImpl() {
}
//TServiceException, TException都定义为checked Exception
@AfterThrowing(pointcut = "thriftImpl()",throwing="ex")
public void afterThrowing(JoinPoint jp,Throwable ex) throws TServiceException, TException{
Signature signature = jp.getSignature();
LOGGER.error("thrift server exception, request = {} , args = {} ", signature.getName(), jp.getArgs(), ex);
//都封装为TServiceException,并且返回的异常message是具有可读性的
if (! (e instanceof TServiceException)) {
if (e instanceof InvalidParamException) {
//InvalidParamException为运行时异常,这里进行转换
throw new TServiceException(TServiceExceptionCode.INVALID_PARAM.getCode(), ((InvalidParamException) e).getShowMessage());
} else {
//其他未知异常用一个统一的系统错误码
throw new TServiceException(TServiceExceptionCode.SERVER.getCode(),TServiceExceptionCode.SERVER.getMessage());
}
}
}
3.参考
阿里巴巴Java开发手册v1.2.0.pdf