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

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

先说明一下背景:

  • 项目日志中的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. Numpy的ndarry:一种多维数组对象

    Numpy的ndarry:一种多维数组对象 Numpy最重要的一个特点就是其N维数组对象(即ndarry),该对象是一个快速而灵活的大数据集容器.你可以利用这种数组对整块数据执行一些数学运算,其语法跟 ...

  2. python的多线程编程之锁

    1. 背景概述 在上篇文章中,主要讲述了python中的socket编程的一些基本方面,但是缺少关于锁的相关概念,从而在这篇文章中进行补充. 由于在python中,存在了GIL,也就是全局解释器锁,从 ...

  3. leetcode654

    class Solution { public: TreeNode* constructMaximumBinaryTree(vector<int>& nums) { ) retur ...

  4. YII 自带验证码实现

    共三步,分别controllers,models,views各一层添置一行代码即可实现 第一步在controllers添加 public function actions() { return arr ...

  5. http://www.5xcg.com/bbs/forum.php?mod=viewthread&tid=51143&extra=page%3D1

    http://www.5xcg.com/bbs/forum.php?mod=viewthread&tid=51143&extra=page%3D1 因为身在酒店设备有限,只能尽量把文字 ...

  6. 模m的剩余类里的一切数与m的最大公约数相等

    [模m的剩余类里的一切数与m的最大公约数相等] 设剩余类里的任意两元素,a.b.则: a=mq1+r1, b= mq2+r1. 根据上式可得,(a,m)=(m,r1), (b,m)=(m,r2).可推 ...

  7. Graphics.Blit

    [Graphics.Blit] 需求注意第4个参数,用4个参数pass用于指定使用哪一个pass.默认值为-1,即使用所有的pass. 参考:file:///C:/Program%20Files%20 ...

  8. intellij idea15,SVN commit file提示No changes detected

    备注:我用的intellij 15,已经配置了SVN.且我的工程是从SVN导出的   遇到的问题:Subversion提交代码时提示No changes detected,当然新文件想要add也是选项 ...

  9. 每月IT摘录201806

    一.技术 1.架构师的技术升级要点:用两个字来描述:集群,用三个字:分布式,再用多点的文字:把海量的流量和数据合理分摊到数量合适的机器上. 想明白这点,后面就能知道该学哪些了,比如流量分摊时得负载均衡 ...

  10. Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段

    目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...