美文网首页
异常处理的最佳实践

异常处理的最佳实践

作者: 知止9528 | 来源:发表于2021-02-27 20:36 被阅读0次

针对异常的一般操作
转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过 cause 关联老异常。

重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。

恢复,即尝试进行降级处理,或使用默认值来替代原始数据


一些错误示例

:不应该用 AOP 对所有方法进行统一异常处理,异常要么不捕获不处理,要么根据不同的业务逻辑、不同的异常类型进行精细化、针对性处理

IllegalArgumentException: 入参错误,比如参数类型int输入string。
IllegalStateException: 状态错误,比如订单已经支付完成,二次请求支付接口。
UnsupportedOperationException: 不支持操作错误,比如对一笔不能退款的订单退款。
其他异常
SecurityException: 权限错误,比如未登陆用户调用修改用户信息接口。

image.png

每层架构的工作性质不同,且从业务性质上异常可能分为业务异常和系统异常两大类,这就决定了很难进行统一的异常处理。我们从底向上看一下三层架构:

Repository 层出现异常或许可以忽略,或许可以降级,或许需要转化为一个友好的异常。如果一律捕获异常仅记录日志,很可能业务逻辑已经出错,而用户和程序本身完全感知不到。

Service 层往往涉及数据库事务,出现异常同样不适合捕获,否则事务无法自动回滚。此外 Service 层涉及业务逻辑,有些业务逻辑执行中遇到业务异常,可能需要在异常后转入分支业务流程。如果业务异常都被框架捕获了,业务功能就会不正常。

如果下层异常上升到 Controller 层还是无法处理的话,Controller 层往往会给予用户友好提示,或是根据每一个 API 的异常表返回指定的异常类型,同样无法对所有异常一视同仁。因此,我不建议在框架层面进行异常的自动、统一处理,尤其不要随意捕获异常。但,框架可以做兜底工作。如果异常上升到最上层逻辑还是无法处理的话,可以以统一的方式进行异常转换,比如通过 @RestControllerAdvice + @ExceptionHandler,来捕获这些“未处理”异常:


对于自定义的业务异常,以 Warn 级别的日志记录异常以及当前 URL、执行方法等信息后,提取异常中的错误码和消息等信息,转换为合适的 API 包装体返回给 API 调用方;

对于无法处理的系统异常,以 Error 级别的日志记录异常和上下文信息(比如 URL、参数、用户 ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以 API 包装体返回给调用方

比如,下面这段代码的做法

@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
        private static int GENERIC_SERVER_ERROR_CODE = 2000;
        private static String GENERIC_SERVER_ERROR_MESSAGE = "服务器忙,请稍后再试";

        @ExceptionHandler
        public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
            if (ex instanceof BusinessException) {
                BusinessException exception = (BusinessException) ex;
                log.warn(String.format("访问 %s -> %s 出现业务异常!", req.getRequestURI(), method.toString()), ex);
                return new APIResponse(false, null, exception.getCode(), exception.getMessage());
            } else {
                log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequestURI(), method.toString()), ex);
                return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
            }
        }
    }

出现运行时系统异常后,异常处理程序会直接把异常转换为 JSON 返回给调用方:


image.png

:其次,处理异常应该杜绝生吞,并确保异常栈信息得到保留
:小心异常被覆盖


1.异常生吞
说明:

在任何时候,我们捕获了异常都不应该生吞,也就是直接丢弃异常不记录、不抛出。这样的处理方式还不如不捕获异常,因为被生吞掉的异常一旦导致 Bug,就很难在程序中找到蛛丝马迹,使得 Bug 排查工作难上加难。


2.丢弃异常的原始信息
错误示例1

private void readFile() throws IOException { 
Files.readAllLines(Paths.get("a_file"));
}

说明
像这样调用 readFile 方法,捕获异常后,完全不记录原始异常,直接抛出一个转换后异常,导致出了问题不知道 IOException 具体是哪里引起的:


错误示例2

public void wrong1(){
 try { 
readFile(); 
} catch (IOException e) {
 //原始异常信息丢失 
throw new RuntimeException("系统忙请稍后再试");
 }
}

说明
像这样调用 readFile 方法,捕获异常后,完全不记录原始异常,直接抛出一个转换后异常,导致出了问题不知道 IOException 具体是哪里引起的:


错误示例3

catch (IOException e) { 
//只保留了异常消息,栈没有记录 
log.error("文件读取错误, {}", e.getMessage()); 
throw new RuntimeException("系统忙请稍后再试");
}

说明
只记录了异常消息,却丢失了异常的类型、栈等重要信息:

修改方式

catch (IOException e) { 
throw new RuntimeException("系统忙请稍后再试", e);
}

如果需要重新抛出异常的话,请使用具有意义的异常类型和异常消息。


错误示例4

public void wrong() { 
try { 
log.info("try"); 
//异常丢失 
throw new RuntimeException("try"); 
} finally { 
log.info("finally"); 
throw new RuntimeException("finally");
 }
}

说明
try里面的异常被finally里面的异常覆盖了

修复方式
把 try 中的异常作为主异常抛出,使用 addSuppressed 方法把 finally 中的异常附加到主异常上:

public void right2() throws Exception {
        Exception e = null;
        try {
            log.info("try");
            throw new RuntimeException("try");
        } catch (Exception ex) {
            e = ex;
        } finally {
            log.info("finally");
            try {
                throw new RuntimeException("finally");
            } catch (Exception ex) {
                if (e != null) {
                    e.addSuppressed(ex);
                } else {
                    e = ex;
                }
            }
        }
        throw e;
    }

备注
对于实现了 AutoCloseable 接口的资源,建议使用 try-with-resources 来释放资源,否则也可能会产生刚才提到的,释放资源时出现的异常覆盖主异常的问题。


确保正确处理了线程池中任务的异常
如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;
如果任务通过 submit 提交意味着我们关心任务的执行结果,应该通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。

修复方式
1.以 execute 方法提交到线程池的异步任务,最好在任务内部做好异常处理;

new ThreadFactoryBuilder().
                setNameFormat(prefix + "%d").
                setUncaughtExceptionHandler((thread, throwable) -> 
                        log.error("ThreadPool {} got exception", thread, throwable))
                .get()

2.设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序
设置全局的默认未捕获异常处理程序

static { Thread.setDefaultUncaughtExceptionHandler((thread, throwable)-> 
                log.error("Thread {} got exception", thread, throwable));
        }

异常栈是跟据当前上下文生成,不可用static定义异常,否则会导致异常栈固化,从而影响问题定位(认为是走到其他分支了)。

错误示例

public class Exceptions {
        public static BusinessException ORDEREXISTS = new BusinessException("订单已经存在", 3001);
    }

说明
把异常定义为静态变量会导致异常信息固化,这就和异常的栈一定是需要根据当前调用来动态获取相矛盾。

修复方式

public class Exceptions { 
        public static BusinessException orderExists(){
            return new BusinessException("订单已经存在", 3001); 
        }
    }

相关文章

  • Spring Boot统一异常处理实践

    摘要: SpringBoot异常处理。 原文:Spring MVC/Boot 统一异常处理最佳实践 作者:赵俊 前...

  • 异常处理的最佳实践

    针对异常的一般操作转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个...

  • springboot 统一异常处理 自定义异常返回

    看完以下三篇就懂了: Spring MVC/Boot 统一异常处理最佳实践讲了异常处理的正确思路:service/...

  • Java 最佳实践的经验

    Java 最佳实践的面试问题 包含 Java 中各个部分的最佳实践,如集合,字符串,IO,多线程,错误和异常处理,...

  • springboot 异常处理最佳实践

    通常情况下,在一个系统中,抽象出公用的部分的过程是一个DRY的一个过程。这是所有计算机工程中的一个准则。防止重复是...

  • Java异常处理最佳实践

    在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考...

  • 前端异常处理最佳实践

    前端可以说是最贴近用户的一层,当产品不断的迭代完善,产品的用户体验会更加趋向于完美,然而前端异常却是很另人头疼的一...

  • 前端异常处理最佳实践

    前端可以说是最贴近用户的一层,当产品不断的迭代完善,产品的用户体验会更加趋向于完美,然而前端异常却是很另人头疼的一...

  • Java异常处理最佳实践

    我们为什么要做异常处理 1、给请求端明确的操作指导。 2、正确记录系统异常时的完整场景,包括代码的调用过程、出错点...

  • 项目开发

    Retrofit封装 RxJava 与 Retrofit 结合的最佳实践 flatmap统一处理异常 Rx处理服务...

网友评论

      本文标题:异常处理的最佳实践

      本文链接:https://www.haomeiwen.com/subject/vbgifltx.html