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

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

先说明一下背景:

  • 项目日志中的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. delphi XE8 NetHTTPRequest NetHTTPClient

    delphi xe8 推出2个新http控件,NetHTTPRequest.NetHTTPClient 可以调用ASP.Net 一般应用程序获取网页数据,用旧的控件idhttp控件也可以,推荐用新的这 ...

  2. FoxPro 游标指针操作

    查询上一记录skip -1 *相对定位 指针向上移动一条记录if bof() *测试当前记录指针是否超出第一条记录  go top *绝对定位表的第一条记录endifthisform.refresh ...

  3. c# 之Web.config

    Web.config文件是一个XML文本文件,它用来储存 ASP.NET Web 应用程序的配置信息(如最常用的设置ASP.NET Web 应用程序的身份验证方式), 它可以出现在应用程序的每一个目录 ...

  4. CentOS装机必备-基本设置以及缺失文件

    主要是虚拟机中安装CentOS每次总会做一些设置,记录下来方便以后. 纯粹基本设置,比如本地SecureCRT可以连接虚拟机中的CentOS. 复杂的非基本设置见:Linux  命令集锦 设置网络 自 ...

  5. Redis 位操作

    [Redis 位操作] 1.GETBIT key offset 对 key 所储存的字符串值,获取指定偏移量上的位(bit). 当 offset 比字符串值的长度大,或者 key 不存在时,返回 0  ...

  6. selenium IDE测试中的坑

    selenium IDE工具是firefox自带的一个网页自动化测试工具,因为它是IDE所以它很方便使用,但也因为它是IDE所以它有那么些坑. 问题:selenium回放中timeout问题 网页的打 ...

  7. Python打包工具

    打包Python应用,使用工具: 1.Linux和Windows下,使用pyinstaller pyinstaller -F -w XXX.py 在当前文件夹下生成两个文件夹:build .dist ...

  8. c语言静态断言

    在php中可以通过xdebug来显示详细的错误信息,可以细化到哪个文件哪行代码引起的报错.在C语言里面也可以通过静态断言(assert)来使得调试代码更加方便.关于断言,可以作为一种很强大的调试方式或 ...

  9. Laravel 5.4 实现无限级分类

    最近在工作中遇到一个需求,是要在laravel 5.4中实现无限级分类,但发现网上这个的资料较少,所以只能自己来实现了,下面这篇文章主要给大家介绍了关于在laravel 5.4中实现无限级分类的方法示 ...

  10. windows-x64 php5.6+apache2.4+mysql配置

    随手一记, 方便以后查找! 1.安装apache2.4 - 下载压缩文件并解压到  D:\Develop\Apache24 - 修改 conf 目录下: httpd.conf 文件 - 服务器目录:  ...