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

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

先说明一下背景:

  • 项目日志中的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接口层的实践讨论。本文试图结合实际应用描述异常使用场景,展开了工程学上的最佳异常实践探索。由于水平有限,内容难免出现理解上的偏差,还请大家批评指正。

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

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

相关文章:
【推荐】 消息中间件客户端消费控制实践
【推荐】 分布式存储系统可靠性系列二:系统估算示例

JAVA异常的最佳工程学实践探索的更多相关文章

  1. java多线程中最佳的实践方案是什么?

    java多线程中最佳的实践方案是什么? 给你的线程起个有意义的名字.这样可以方便找bug或追踪.OrderProcessor, QuoteProcessor or TradeProcessor 这种名 ...

  2. 避免Java中NullPointerException的Java技巧和最佳实践

    Java中的NullPointerException是我们最经常遇到的异常了,那我们到底应该如何在编写代码是防患于未然呢.下面我们就从几个方面来入手,解决这个棘手的​问题吧.​ 值得庆幸的是,通过应用 ...

  3. 使用DataStax Java驱动程序的最佳实践

    引言 如果您想开始建立自己的基于Cassandra的Java程序,欢迎! 也许您已经参加过我们精彩的DataStax Academy课程或开发者大会,又或者仔细阅读过Cassandra Java驱动的 ...

  4. paip.复制文件 文件操作 api的设计uapi java python php 最佳实践

    paip.复制文件 文件操作 api的设计uapi java python php 最佳实践 =====uapi   copy() =====java的无,要自己写... ====php   copy ...

  5. Java 网络编程最佳实践(转载)

    http://yihongwei.com/2015/09/remoting-practice/ Java 网络编程最佳实践 Sep 10, 2015 | [Java, Network] 1. 通信层 ...

  6. java异常有效实践

    异常在我们的平时开发过程中是非常寻常并且经常会面对的,我们有很多方式来处理和使用异常.充分发挥异常的优点可以提高程序的可读性,可靠性和可维护性.但是如果使用不当,也会带来很多负面影响. 参考 effe ...

  7. 10个关于Java异常的常见问题

    这篇文章总结了十个经常被问到的JAVA异常问题: 1.检查型异常VS非检查型异常 简单的说,检查型异常是指需要在方法中自己捕获异常处理或者声明抛出异常由调用者去捕获处理: 非检查型异常指那些不能解决的 ...

  8. Java异常错误的面试题及答案

    1) Java中什么是Exception? 这个问题经常在第一次问有关异常的时候或者是面试菜鸟的时候问.我从来没见过面高级或者资深工程师的 时候有人问这玩意,但是对于菜鸟,是很愿意问这个的.简单来说, ...

  9. java异常面试常见题目

    在Java核心知识的面试中,你总能碰到关于 处理Exception和Error的面试题.Exception处理是Java应用开发中一个非常重要的方面,也是编写强健而稳定的Java程序的关键,这自然使它 ...

随机推荐

  1. 12 并发编程-(线程)-线程queue&进程池与线程池

    queue 英 /kjuː/ 美 /kju/ 队列 1.class queue.Queue(maxsize=0) #队列:先进先出 import queue q=queue.Queue() q.put ...

  2. ArrayList原理(一)

    需要使用到动态数组的时候用的最多的就是ArrayList了,底层其实是Object数组,以下demo基于JDK1.8: List<Integer> list  = new ArrayLis ...

  3. Notepad++中的高级查找

      准备以下字符串用来演示 abcdeab cdeabcde abcd eabcde   基于扩展的查找 基于扩展的查找不能算是真正的正则表达式搜索,因此这种查找方式仅是提供了支持转义字符.主要常用的 ...

  4. x64位windows 2003 server中“Server 对象 错误 'ASP 0177 : 800700c1' Server.CreateObject 失败”问题

    给朋友看一个老asp网站图片不能上传问题,试过网上各种办法都提示: Server 对象 错误 'ASP 0177 : 800700c1' Server.CreateObject 失败 最终问题出在x6 ...

  5. 插件 uploadify

    一.属性 属性名称 默认值 说明 auto true 设置为true当选择文件后就直接上传了,为false需要点击上传按钮才上传 . buttonClass ” 按钮样式 buttonCursor ‘ ...

  6. Emacs及扩展配置

    Emacs及扩展配置 Table of Contents 1. 动机之反思 2. 它山之石 3. 扩展的管理 4. 我额外安装的通用扩展(在purcell基础上) 5. LaTex相关的问题和配置 6 ...

  7. Golang之排序算法

    冒泡排序 package main //冒泡排序 import "fmt" func bsort(a []int) { ; i < len(a); i++ { ; j < ...

  8. 在Linux(Ubuntu)下安装Arial、Times New Roman等字体

    在Linux下做文档.作图的时候,可能需要用到Arial和Times New Roman等字体.但是由于版权问题,Linux一般是不直接提供这些字体的. 注意字体也是有版权的!不过有版权也不代表一定会 ...

  9. Laravel/Homestead storage:link -> symlink(): Protocol error

    I'm trying to run the following artisan command: php artisan storage:link I get this error: [ErrorEx ...

  10. Java 设计模式系列(五)原型模式

    Java 设计模式系列(五)原型模式 原型模式属于对象的创建模式.通过给出一个原型对象来指明所有创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象.这就是选型模式的用意. 一.原型模 ...