如何正确处理Java异常

2019-07-03 00:00:00 java 异常 正确处理

Java异常类型结构:

《如何正确处理Java异常》
《如何正确处理Java异常》

  • Error
    Error 描述了 Java 程序运行时系统的内部错误,通常比较严重,除了通知用户和尽力使应用程序安全地终止之外,其它无能为力,应用程序不应该尝试去捕获这种异常,通常为一些虚拟机异常,如 StackOverflowError、OutOfMemoryError 等等。
  • Exception
    Exception 类型下面又分为两个分支,一个分支派生自 RuntimeException,称为非受检查异常(UncheckedException),这种异常通常为程序错误导致的异常;另一个分支为派生自 IOException 的异常,称为受检查异常(CheckedException),这种异常通常是程序本身没有问题,由于像 I/O 错误等问题导致的异常。

CheckedException和UncheckedException的区别:

CheckedException会在编译时被检测,即要么使用try catch 代码块,要么在方法签名中用 throws 关键字声明该方法可能会抛出的CheckedException,否则编译无法通过。

CheckedException是Java中一个非常糟糕的设计

  1. 它会强迫程序员编写try catch来处理,事实上当出现此类异常时大多数时候程序员也不知道该采取什么措施来处理,这就导致大部分的程序员在catch之后什么也没做,掩盖异常仅仅是为了能编译通过。
  2. 破坏向后兼容性。受检查异常可以在方法签名上标明具体错误原因,但实践中,每次在签名上增加一个新的异常,都会破坏向后兼容性。

第二点还是次要的,最主要的是第一点,在我从事Java编程三年多接触过的实际项目中,目睹了无数的代码胡乱的的try catch,catch后又不throw,或者throw一个其它异常,又不带上原始异常,导致原始异常丢失,或者干脆啥也不做,结果埋下了无数的坑,给以后接手的人带来很多问题。

CheckedException在Java8越来越被标准库所摒弃。Java 7的Files API 用的还是基类IOException,Java 8的Function、BiFunction等压根就不支持CheckedException。Java8标准库中还加入了UncheckedIOException用来避免CheckedException带来的问题。

准确地讲,Java标准库中大部分的CheckedException并不应该定义为CheckedException,因为这些异常和 NullPointerException、 OutOfMemoryError 等一样,除了报错,没什么可特别处理的,也处理不了,除了强迫程序员编写try catch增加工作量之外,并没有其它更多的用处。

例如,我们看看下面的代码,很普遍的写法,大部分人都是try catch之后一句e.printStackTrace()就完事了:

    public static void main(String[] args) {
        System.out.println(getIpInfo("www.baidu.com"));
    }
    public static String getIpInfo(String host) {
        try {
            InetAddress[] addresses = InetAddress.getAllByName(host);
            return Arrays.stream(addresses)
                         .map(InetAddress::getHostAddress)
                         .collect(Collectors.joining(";"));
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return null;
    }

这个InetAddress类的getAllByName方法声明了会抛出一个受检查的异常UnknownHostException,什么情况下会抛出这个异常呢?可能是传入的host有误,可能是网络出现问题,不管是哪种情况,都不是catch之后能解决的。既然不能解决,又何必要强迫程序员去try catch呢,因此这里这个UnknownHostException就不应该定义为CheckedException,但是类库既然已经设计成这样,我们也无法改变,更好的做法是这样:

    public static String getIpInfo(String host) {
        try {
            InetAddress[] addresses = InetAddress.getAllByName(host);
            return Arrays.stream(addresses)
                         .map(InetAddress::getHostAddress)
                         .collect(Collectors.joining(";"));
        } catch (UnknownHostException e) {
            // 将出错方法的入参一起放到异常信息里,这样方便以后调用方查找问题所在             throw new RuntimeException("occur error,host:" + host, e);
        }
    }

再看另外一个例子:

    public static void main(String[] args) {
        System.out.println(encodeStr("Java编程"));
    }
    public static String encodeStr(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

这里encode方法会抛出一个UnsupportedEncodingException,这里如果出现这个异常,表明传入的编码有误,在catch里根本什么也做不了,因此强迫强迫程序员去try catch毫无意义。出现了此异常则只能修改传入的编码字段。

如果传入了正确的编码,如UTF-8,我们知道UTF-8是所有平台的JDK都支持的编码,所以这里根本不可能出现UnsupportedEncodingException,因此这个UnsupportedEncodingException定义UncheckedException才是合理的做法。所以说在Java类库API里,有很多CheckedException我们明知道不可能发生,但还是不得不try catch一下,大大增加了代码噪声。

总的来说,Java的CheckedException特性在工程实践中一直是很糟糕的体验,比如我写一个保存当前编辑信息到远程服务器文件的方法saveToRemoteFile,这个方法里有非常复杂的保存逻辑,可能会有n层的子方法调用,而内层的文件API调用可能会抛出各种类型的异常,比如远程文件不存在,磁盘空间不足,权限不足,网络连接异常等等。

所有这些异常对保存逻辑来说全都没法进行处理,通常遇到这些异常内层的子方法完全不必理会,只需要在最外层的saveToRemoteFile里catch住所有的IO异常,然后清理下现场,告知下用户就可以了。整个代码干净整洁,不用担心内层的子方法忘记检查异常从而出现程序失控,同时对异常也进行了良好的处理。

反之,有了CheckedException机制,所有内层函数都必须声明会抛出IO异常,多了很多无谓的声明代码。如果哪个新手在声明方法时忘记了声明异常,然后在写实现代码的时候又遇到了编译错误,于是根据编辑器提示加上了try catch实现以通过编译,在catch里面又什么都不做,结果就导致了忽略异常的严重逻辑错误。

所以CheckedException通常并不能使你的代码变的更安全,反而因为繁琐的声明更容易误导程序员做出危险的操作。所以很多项目干脆直接把异常包装成RuntimeException以规避Check,将CheckedException不分青红皂白包装成RuntimeException抛出,在Java编程圈里已经是一个公开的秘密了吧。

后来Java8 UncheckedIOException 的出现,连官方都鼓励不要CheckedException了,在大名鼎鼎的Spring框架的设计哲学中更是没有CheckedException这个东西。

关于CheckedException的我总结了项目中最普遍的错误用法(下面的logger用的是slf4j类库)

错误用法一:

try {
    // 业务代码...... } catch (IOException e) {
    // 毫无用处,丢失异常,在系统真的出现异常时,你永远不知道到底发生了什么事情     e.printStackTrace();
}

错误用法二:

try {
    // 业务代码...... } catch (IOException e) {
    // 丢失异常栈信息和其它一些有用的异常信息,异常栈信息对于定位是哪一行代码出错     // 非常关键,一旦丢失异常栈信息,则很难找到是哪一行代码出错     throw new RuntimeException("error:" + e.getMessage());
}

错误用法三:

try {
    // 业务代码...... } catch (IOException e) {
    // 丢失原始异常,无法排除问题     throw new RuntimeException("something wrong");
}

错误用法四:

try {
    // 业务代码...... } catch (IOException e) {
    // 要打印异常,就不要抛出,不要两者都做。这样会导致日志文件出现大量同样的异常     // 信息,极大增加排除问题的难度     logger.error("error", e);
    throw e; 
}

错误用法五:

try {
    // 业务代码...... } catch (IOException e) {
    // 未打印异常栈信息导致调用栈信息丢失和其它一些有用的的异常信息丢失,假如try块内     // 有数百行代码,那么无法知道是哪一行出错。所以打印这样的日志对于排查问题基本也没     // 什么用处     logger.error("error:" + e.getMessage()); 
}

正确的做法:

try {
    // 业务代码...... } catch (IOException e) {
    throw new RuntimeException("这里可以放入一些异常现场的信息,例如方法的入参等等,方便后期"
    +"排查问题,方法入参:"+"方法的入参" , e);
}

或者

try {
    // 业务代码...... } catch (IOException e) {
    logger.error("这里可以放入一些异常现场的信息,例如方法的入参等等,方便后期排查问题,方法"
    +"入参:{}","方法的入参" , e);
}

在catch异常之后要如何处理,一定要考虑业务上的可能性,至于是继续抛出还是打印日志后给个默认值要视业务上的要求而定。

例如我们使用SimpleDateFormat来解析字符串到Date对象时,有这么个方法:

    public static Date str2Date(String dateStr, String pattern) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
        Date date;
        try {
            date = dateFormat.parse(dateStr);
        } catch (ParseException e) {
            // 假设dateStr是从数据库中查出来的字段,根据业务上的要求,这个字段肯定是合法的日期字符串,那么             // 如果出现异常,则表明数据库数据出现了问题,则此处最好的做法是直接抛出,并在异常信息中带上dateStr             throw new RuntimeException("error dateStr:" + dateStr, e);
        }
        return date;
    }

另一种情况:

    /**  * @return 解析失败, 返回null  */
    public static Date str2Date(String dateStr, String pattern) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
        Date date;
        try {
            date = dateFormat.parse(dateStr);
        } catch (ParseException e) {
            // 假设dateStr是别人传进来的,不可控,那么如果出现异常,此处较好的做法是打印日志,并表明方法可能会             // 返回null,由调用方自行决定如何处理这个null,或者包装成RuntimeException也可以,由调用方决定是             // 否需要捕捉处理,我推荐最好还是包装成RuntimeException抛出,因为返回null的话调用方就不知道是出             // 现了什么问题导致的,要查找问题还得来找这个日志             Logger.error("error dateStr:{}", dateStr, e);
            return null;
        }
        return date;
    }

Spring框架中如何正确处理和使用异常

在Spring框架中,Controller和Service不处理任何异常,所有异常由ControllerAdvice统一处理,可以遵循以下实践:

一个异常枚举类,所有异常码定义在这里:

public enum BizExceptionEnum {
    APPLICATION_ERROR(1000, "网络繁忙,请稍后再试"),
    INVALID_USER(1001, "用户名或密码错误"),
    INVALID_REQ_PARAM(1002, "参数错误"),
    EXAM_NOT_FOUND(1003, "未查到考试信息"),
    ;
    BizExceptionEnum(Integer errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
    private final Integer errorCode;
    private final String errorMsg;

    // get...... }

一个业务异常类:

public class BizException extends RuntimeException {
    private final BizExceptionEnum bizExceptionEnum;
    public BizException(BizExceptionEnum bizExceptionEnum) {
        super(bizExceptionEnum.getErrorMsg());
        this.bizExceptionEnum = bizExceptionEnum;
    }
    public BizExceptionEnum getBizExceptionEnum() {
        return bizExceptionEnum;
    }
}

一个全局的异常处理类:

@RestControllerAdvice
public class GlobalHandler {
    private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result exceptionHandler(MethodArgumentNotValidException e) {
        Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
                BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
        logger.error("req params error", e);
        return result;
    }
    @ExceptionHandler(BizException.class)
    public Result exceptionHandler(BizException e) {
        BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
        Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
        logger.error("business error", e);
        return result;
    }
    @ExceptionHandler(value = Exception.class)
    public Result exceptionHandler(Exception e) {
        Result result = new Result(BizExceptionEnum.APPLICATION_ERROR.getErrorCode(),
                BizExceptionEnum.APPLICATION_ERROR.getErrorMsg());
        logger.error("application error", e);
        return result;
    }

}

其中Result类的定义:

public class Result<T> {
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;
    public Result(T data) {
        this(true, null, null, data);
    }
    public Result(Integer errorCode, String errorMsg) {
        this(false, errorCode, errorMsg, null);
    }
    public Result(Boolean success, Integer errorCode, String errorMsg, T data) {
        this.success = success;
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
        this.data = data;
    }
    // get set...... }

示例Controller类:

@RestController
@RequestMapping("/json/exam")
public class ExamController {
    @Autowired
    private IExamService examService;
    @PostMapping("/getExamList")
    public Result<List<GetExamListResVo>> getExamList(@Validated @RequestBody GetExamListReqVo reqVo,
                                                      @AuthenticationPrincipal UserDetails userDetails)
            throws IOException {
        List<GetExamListResVo> resVos = examService.getExamList(reqVo, userDetails);
        Result<List<GetExamListResVo>> result = new Result(resVos);
        return result;
    }
}

示例Service类:

@Service
public class IExamServiceImpl implements IExamService {
    @Autowired
    private ManualMicrowebsiteMapper microwebsiteMapper;
    @Override
    public List<GetExamListResVo> getExamList(GetExamListReqVo reqVo, UserDetails userDetails) throws IOException {
        List<MicrowebsiteExam> examEntities = microwebsiteMapper.select(reqVo.getExamType(), userDetails.getUsername());
        // 按照业务的定义要求,此处考试列表必须不为空,一旦为空,则说明后台配置有误或其它未知原因,这种情况视为一种业务异常         if (examEntities.isEmpty()) {
            // 未查到考试信息,抛出相应的业务异常             throw new BizException(BizExceptionEnum.EXAM_NOT_FOUND);
        }
        // 此处代码还有其它各类异常抛出......         List<GetExamListResVo> resVos = examEntities.stream().map(examEntity -> {
            GetExamListResVo resVo = new GetExamListResVo();
            BeanUtils.copyProperties(examEntity, resVo);
            return resVo;
        }).collect(toList());
        return resVos;
    }
}

还有一些Java Bean和Mybatis相关类就不贴出来了。

不管是Controller还是Service,只管抛出异常,不做任何处理,由ControllerAdvice统一处理。通过遵循这样的实践,代码变得简洁清晰,没有太多的代码嵌套,同时所有的异常都得到了妥善的处理。

项目github地址:

https://github.com/jufeng98/java-master.gitgithub.com

    原文作者:梁煜东
    原文地址: https://zhuanlan.zhihu.com/p/68536207
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章