JAVA异常的最佳工程学实践探索

勿忘初心2018-11-01 10:40

此文已由作者占金武授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

先说明一下背景:

  • 项目日志中的Exception会被哨兵统一监控并报警

  • 比较多的项目基于dubbo在做服务化


表单参数校验中异常使用的建议

异常机制存在的一个最大好处是让JAVA函数实现了“多返回值”,比如:

public int caculate(int a, int b) throws MyException {
}

这段代码的本质是让函数caculate拥有了这样一个返回值[int, MyException],这样做有什么好处呢?

假设不使用异常,上面的函数只能用-1、-2这类魔法数来表达异常情况,这样做会比较糟糕,因为使用这个函数的人必须非常小心地去处理返回值里的这些魔法数,而时常这是一件容易遗漏的事。

这样看来,在进行入参检验的时候,发现不合法参数而返回IllegalArgumentException是非常合理且自然的用法。

结合一下web表单场景,假设这里是对用户输入参数的校验,后台校验不合法,由MVC的Controller层统一汇总封装返回给前端会是一种比较优雅的做法,而前端要做的是配合后端的返回的数据结构把有用的错误信息展示给用户。再结合前面提到的背景,这里出现的异常不应该打印堆栈日志,否则会造成哨兵误报(之所以说是误报是因为这是一种常见情况不应该引起运维人员的注意并介入处理),建议的做法是记录相应的异常日志。

这里我们有必要再思考清楚一些,如果没有哨兵报警误报的问题,我们是否有必要打印堆栈日志呢?一般而言,打印异常堆栈是为了帮助运维人员(或开发人员)迅速定位异常原因,进而修复异常。而这里的场景其实是不需要运维人员介入的,由前端页面提示给用户,用户调整相应的参数后重新发起请求即可恢复。

说得有点啰嗦,但其实是为了更清楚的强调这样一个观点:使用异常并不代表一定要把堆栈打印出来,比如web表单入参的检测


dubbo接口中异常使用的建议

先抛出几个dubbo异常相关的常见问题:


  • dubbo provider方法的实现底层使用了自定义的XXRuntimeException,在api jar中并未包含此XXRuntimeException定义,consumer调用发现无法识别XXRuntimeException,提示“Got unchecked and undeclared exception...”

  • dubbo provider方法的实现底层使用了IllegalArgumentException,consumber调用产生IllegalArgumentException,而provider并未发现自己系统产生了这些异常(比较典型的情况是provider的数据库连接异常),也没有相应的监控,只能等到consumber来投诉。


以上两个问题的产生与 dubbo 的实现有关,来看看 dubbo 是怎么处理异常的(ExceptionFilter):


Result result = invoker.invoke(invocation);if (result.hasException() && GenericService.class != invoker.getInterface()) {    try {
        Throwable exception = result.getException();        // 如果是checked异常,直接抛出
        if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {            return result;
        }        // 在方法签名上有声明,直接抛出
        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日志
        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);
...


从以上实现可以看出,如果unchecked异常未显示声明,则会自动打印error日志。


那该如何优雅地解决呢?


  • dubbo接口要避免向外抛出RuntimeException(不仅仅是为了避免扰人的error提醒,更为了避免异常泄漏)。建议的一种方法是在接口实现代码的最外层统一使用类似以下示例的方式进行包装:


Response r;try {
    r = ...;
} catch (RuntimeException e) {
    logger.error("error msg", e);//相当于单个项目下异常处理的最外层,需要把异常记录下来
    r = Response.buildErrorResponse(e.getMessage());
}return r;


  • 如果一定要使用异常来表达接口语义,使用Checked Exception


使用异常 vs 使用null

前面已经提到,异常的使用会带来编码的便利,但同时也为更多的无用日志输出埋下了隐患。就上面包装代码的例子,如果RuntimeException是网络连接或者数据库连接异常倒还好,属于有用的异常,但如果是因为某项数据不存在而抛出IllegalArgumentException,则日志就显得有点多余了(无法根据日志内容采取有效的行动来阻止)。


public Permission loadPermission(Long userId) {    if(...) {        return ...
    } else {        throw new IllegalArgumentException();// or throw new NotFoundException();
    }
}


这种写法下,loadPermission需要 try{...}catch(){...}的额外‘照应’才得处理得当,是不是有点繁琐呢?此时直接返回null可能来得更加直接呢?出错了和没有其实是两回事,应该仔细斟酌。

所以,这里我给出的建议是:如果能简单方便地避免使用异常,则避免之。


异常统一处理的建议

前面提到了:与前端交互时的异常处理、dubbo接口中的异常处理,其中都提到了一点:运行时异常建议统一处理(Checked异常已经强制由程序员进行处理了)。扩展一下,还有哪些异常需要统一处理呢?是以什么样的维度进行统一呢?我理解可以围绕线程用途进行聚合处理。常见的WEB系统中一般有以下几类线程在运行:


  • 主线程(Main函数,异常交由JVM处理)

  • HttpRequest线程(一般由Controller提供的hook方法一处理)

  • 异步任务线程池中的工作线程(一般由线程池提供hook接口方法进行统一处理)

  • dubbo线程(由ExceptionFilter进行统一处理)


以上思路理清以后,大家就可以参照进行异常的统一处理了。

Spring MVC:

@ExceptionHandlerpublic Object exception(Exception exception, HttpServletRequest request, HttpServletResponse response) {    return ExceptionUtil.processException(request, response, exception, logger);
}


线程池:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(11, 100, 1, TimeUnit.MINUTES, //   
        new ArrayBlockingQueue<Runnable>(10000),//   
        new DefaultThreadFactory()) {   
    protected void afterExecute(Runnable r, Throwable t) {   
        super.afterExecute(r, t);   
        printException(r, t);   
    }   
};


dubbo:

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    Result result = invoker.invoke(invocation);    if(result.hasException()) {
        printException(result.getException());
    }    return result;
}


结束语

互联网上关于JAVA异常使用和最佳实践的文章比较多,大多流于理论,缺乏对实际工作的指导意义,缺少对常见应用场景如web层、dubbo接口层的实践讨论。本文试图结合实际应用描述异常使用场景,展开了工程学上的最佳异常实践探索。由于水平有限,内容难免出现理解上的偏差,还请大家批评指正。


免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击




相关文章:
【推荐】 Docker中搭建zookeeper集群
【推荐】 分布式存储系统可靠性系列五:副本放置算法 & CopySet Replication
【推荐】 如何安全地运行用户的 JavaScript 脚本