java异常处理规范

正文在下面,先打个广告:
在这里插入图片描述

前面介绍了 日志打印规范, 如果想打印出合格的日志,还需要了解Java对异常处理的一些知识。

java 异常简介

在这里插入图片描述

先来看一下Java中异常的类图,该类图只是把常见的异常列了一下。更详细的请读者去看jdk源码。

先看一下Throwable ,它是所有errors和exceptions的超类。

 * The {@code Throwable} class is the superclass of all errors and
 * exceptions in the Java language. Only objects that are instances of this
 * class (or one of its subclasses) are thrown by the Java Virtual Machine or
 * can be thrown by the Java {@code throw} statement. Similarly, only
 * this class or one of its subclasses can be the argument type in a
 * {@code catch} clause.

再看一下Error
从Error类的注释中可以清晰的看到,Error是不应该try-catch的,Error的异常时不应该发生的系统性错误。它是unchecked exceptions

/**
 * An {@code Error} is a subclass of {@code Throwable}
 * that indicates serious problems that a reasonable application
 * should not try to catch. Most such errors are abnormal conditions.
 * The {@code ThreadDeath} error, though a "normal" condition,
 * is also a subclass of {@code Error} because most applications
 * should not try to catch it.
 * <p>
 * A method is not required to declare in its {@code throws}
 * clause any subclasses of {@code Error} that might be thrown
 * during the execution of the method but not caught, since these
 * errors are abnormal conditions that should never occur.
 *
 * That is, {@code Error} and its subclasses are regarded as unchecked
 * exceptions for the purposes of compile-time checking of exceptions.
 *
 */
public class Error extends Throwable 

再看一下Exception
Exception是我们平时工作中最常遇到的异常,是需要try-catch的。Exception和其所有子类(除RuntimeException及其子类)是checked exceptions

 * The class {@code Exception} and its subclasses are a form of
 * {@code Throwable} that indicates conditions that a reasonable
 * application might want to catch.
 *
 * <p>The class {@code Exception} and any subclasses that are not also
 * subclasses of {@link RuntimeException} are <em>checked
 * exceptions</em>.  Checked exceptions need to be declared in a
 * method or constructor's {@code throws} clause if they can be thrown
 * by the execution of the method or constructor and propagate outside
 * the method or constructor boundary.
 */
public class Exception extends Throwable {

最后看一下RuntimeException
RuntimeException及其子类是unchecked exceptions

/**
 * {@code RuntimeException} is the superclass of those
 * exceptions that can be thrown during the normal operation of the
 * Java Virtual Machine.
 *
 * <p>{@code RuntimeException} and its subclasses are <em>unchecked
 * exceptions</em>.  Unchecked exceptions do <em>not</em> need to be
 * declared in a method or constructor's {@code throws} clause if they
 * can be thrown by the execution of the method or constructor and
 * propagate outside the method or constructor boundary.
 */
public class RuntimeException extends Exception {

异常处理规范

  1. 【强制】不要捕获 Java 类库中定义的继承自 RuntimeException 的运行时异常类,如:IndexOutOfBoundsException / NullPointerException,这类异常由程序员预检查来规避,保证程序健壮性。
    正例: if(obj != null) {…}
    反例: try { obj.method() } catch(NullPointerException e){…}

  2. 【强制】异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。
    更加不要使用异常的msg信息来做业务校验,因为Java有实时的JIT编译优化,当频繁的报同一个异常时,异常msg信息会被忽略,导致判断有误。详见博客:JIT实时编译优化带来的问题:几千次异常以后取不到错误信息了

  3. 【强制】对大段代码进行 try-catch,这是不负责任的表现。 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
    正例:

map.put("status", 500);   
try{
    //代码省略
    map.put("message", "success!");   
    map.put("status", 200);   
} catch (UnknownHostException e) {
    //域名错误
    map.put("message", "请输入正确的网址");
    log.error("*** 网址异常[{}]", url, e);
} catch (SocketTimeoutException e) {
    //超时
    map.put("message", "请求地址超时");
    log.error("*** 请求地址超时[{}]", url, e);
} catch (Exception e) {
    //其他异常
    map.put("message", "请求出现未知异常,请重试!\r\n" + e.getMessage());
    log.error("*** 请求出现未知异常,请重试![{}]", url, e);
}
return map;

反例, 要么什么都没做,要么只catch了Exception:

try {
   //此处省略1024行代码
} catch(Exception e){
   //TODO
}
  1. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
    反例:
try{
    //代码省略
} catch (Exception e) {
    System.out.println("插入异常");//调用者对异常没有任何感知。而且日志输出只可以使用log框架,不要用sout
}

try{
    //代码省略
} catch (UnknownHostException e) {
    throw new RuntimeException("500");//调用者对异常无法定位和判断
}
  1. 【强制】有 try 块放到了事务代码中, catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。当然,特殊的业务可能会要求有异常也要提交事务。不管怎么样,都要主动提交或回滚事务。
    曾经遇到过这样一个错误: 事务时开发人员自己控制的,但是没有处理好事务的提交与回滚,导致依赖了数据库驱动的默认提交策略。注意:当有未提交或为回滚的事务时,有的数据库连接池会默认回滚,有的(如Tomcat JDBC Pool)会默认提交。虽然说修改数据库连接池的默认策略可以解决一定的问题,但是千万不可强依赖。一定要手动提交或回滚事务·
try{
    //代码省略
    conn.commit();
} catch (Exception e) {
	log.error("业务信息,错误信息,参数信息", e);
    conn.rollback();
}

如果你使用的是spring管理的事务,则务必添加上@Transactional(rollbackFor = Exception.class)

根据最开始的异常源码,可以将所有异常分为两类:

  • 可查的异常(checked exceptions):Exception下除了RuntimeException外的异常
  • 不可查的异常(unchecked exceptions):RuntimeException及其子类和错误(Error)
/**
 * <p>This annotation type is generally directly comparable to Spring's
 * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute}
 * class, and in fact {@link AnnotationTransactionAttributeSource} will directly
 * convert the data to the latter class, so that Spring's transaction support code
 * does not have to know about annotations. If no rules are relevant to the exception,
 * it will be treated like
 * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute}
 * (rolling back on {@link RuntimeException} and {@link Error} but not on checked
 * exceptions)
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

我们再来看一下Transactional的源码,从注释中可以清晰的看出,默认策略是所有的unchecked exceptions会回滚。

也就是说,当抛出checked exceptions时,事务是不会回滚的。如以下两个实例。虽然下面这两种给写法都是不合理的,尤其在事务中读文件,事务的范围应该尽可能小。但是一个团队中的开发人员能力肯定是参差不齐的,谁也无法保证不会写出这样的代码。这种代码应该在CR过程中发现,而不是为此导致事务不回滚,这可是大BUG。

@Transactional
public void demo() throws Exception {
    this.userRepository.save(new User(USERNAME));
    throw new Exception("No Rollback");
}

@Transactional
public void demo() throws FileNotFoundException{
    new FileInputStream("文件地址");
}

结论: 如果方法没有声明异常,则不需要配置在 Spring 事务注解中配置 rollbackFor。但是为了保证万无一失,还是建议所有Transactional注解都指定rollbackFor = Exception.class

另外注意:如果异常被try{}catch{}了,事务就不回滚了,如果想让事务回滚必须再往外抛try{}catch{throw Exception}。

  1. 【强制】 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
    说明: 如果 JDK7,可以使用 try-with-resources 方法。
File file = new File("文件");
try (FileInputStream inputStream = new FileInputStream(file);) {
    // use the inputStream to read a file    
} catch (FileNotFoundException e) {
    log.error("业务标识, 文件[{文件名称}]不存在", filename);//这种非常明确的异常,有时候是可以不打印堆栈的
} catch (IOException e) {
    log.error("业务标识, 读取文件[{文件名称}]失败", filename, e);
}
  1. 【强制】不能在 finally 块中使用 return, finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。

  2. 【强制】捕获异常与抛出的异常,必须是完全匹配,或者捕获异常是抛异常的父类。而且打印的日志也要与对应异常内容相匹配。

  3. 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。说明: 本规约明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回 null 的情况。

  4. 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:

  • Java自动拆箱装箱的过程可能产生空指针
    在这里插入图片描述
  • 据库的查询结果可能为 null。
  • 判断字符串为空使用isBlank,除非你认为空字符串是非空。
String str = "  ";
System.out.println(StringUtils.isEmpty(str));//false
System.out.println(StringUtils.isBlank(str));//true
  • 远程调用返回对象,一律要求进行 NPE 判断。
  • 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
  • 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
  1. 【推荐】在代码中使用“ 抛异常” 还是“ 返回错误码” ?对于公司外的 http/api 开放接口必须使用“ 错误码” ;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess、 “ 错误码” 、 “ 错误简短信息” 。

说明: 关于 RPC 方法返回方式使用 Result 方式的理由:

  • 用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
  • 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多,result可以包含更多的信息。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
  1. 【推荐】定义时区分 unchecked / checked 异常,避免直接使用 RuntimeException 抛出,更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如: DaoException / ServiceException 等。

  2. 【参考】复制代码时,记得修改异常打印信息。

  3. 优先使用明确的异常。这样调用者可以更加清晰的了解你这个方法或者接口可能抛出的异常类型。不要什么都抛出exception异常。

public void doThis() throws NumberFormatException { ... }
  1. 对外的接口描述里,要写清楚什么情况会抛出什么样的异常,注释要完善
  2. 当有多个异常抛出的时候,有限捕获范围更小的,更明确的异常。
public void catchMostSpecificExceptionFirst() {
   try {
       doSomething("A message");
    } catch (NumberFormatException e) {
        //记录日志
    } catch (IllegalArgumentException e) {
        //记录日志
    }
}
  1. 不要捕获Throwable。 Throwable是所有异常(Exception)和错误(Error)的父类,虽然它能在catch从句中使用,但永远都不要这样做!错误通常时有JVM抛出的,是致命的错误。
  2. 不要忽略异常。即使当前代码你需要cache住所有异常让其继续执行下去,也一定不要什么都不做,至少打印出日志,方便后续有问题排查。如下反例
public void doNotIgnoreExceptions() {
    try {
        // do something
    } catch (NumberFormatException e) {
        // this will never happen
    }
}
  1. 包裹某个异常的同时不要丢弃它原本的信息。很多时候,我们抛出的异常时自定义的异常。此时,务必将原异常的信息作为msg的一部分抛出。这样调用方才能知道到底是什么原因出错的。
    这一点也不是绝对的。这一条主要适用于系统内部,如果是对外提供的接口,那么有时候就需要进行一些异常的转换,典型的例子,我们都不希望将DB的错误抛给用户。

异常错误码

通常会把返回值封装成result, 类似这样的:

{
  "status": 200,
  "errorCode": "OK",
  "errorMsg": "OK",
}

我们就需要为其定义错误码。如

200 成功
400 参数错误
403 无权限
500 服务器异常
501数据获取失败
......
  • status: 通常参考 HTTP Status,这样门槛相对较低。熟悉 HTTP 的都知道,4xx 代表什么, 500 代表什么。这样可以复用 status,例如:400 代表参数错误,user.name.invalid 也是参数错误, user.email.invalid 也是参数错误,这样能够有效避免status的增长。
  • 同一类的status,统一处理。例如:400 为参数错误,前端可以将这一类状态码统一处理,例如:展示 errorMsg 内容。如需特殊处理,也可通过 errorCode 进行判断。
  • errorCode: 编写具体的错误信息,如:user.name.invalid (用户名无效) user.email.invalid (用户邮箱无效)。有的业务会有很多很多的errorcode , 客服或者开发人员在排查问题的时候首先就是要根据这个错误码来缩小排查范围,这是非常重要的,比如银行系统。
  • errorMsg:这里是提示给用户的文案,我们可以在controller response时通过 errorCode 作为key 配置在国际化文件中动态给 errorMsg 赋值。这样能够满足不同语言的文案要求。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐崇拜234

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值