【编程思想】06 编写高质量代码 之 异常处理

110、【建议】异常封装

  1. 为什么要有异常封装?

a: Java语言的异常处理机制可以确保程序的健壮性,提高系统的可用率,但是Java API提供的异常都是比较低级的,只有开发人员才能看得懂,对于用户(客户端)来说就是天书=> 不友好的提示。

  1. 异常封装的优点

a: 提高系统的友好性:比如是直接提示“文件没有找到”比FileNotFoundException异常抛出到上层应用中友好。

// 不友好
public static void doSomething() throws Exception{
    InputStream is = new FileInputStream("无效文件.txt");
    ...
    // 如果出现异常 => FileNotFoundException 异常
}

// 有好异常 抛出自定义友好异常
public static void doSomething() throws MyDefinedException{
    try{
        InputStream is = new FileInputStream("无效文件.txt");
        ...
    } catch (FileNotFoundException e){
        // 如果出现异常 => FileNotFoundException 异常
        log.error("e", e);
        throw new MyDefinedException("code",e.getMessage);
    }
}

b: 提高系统的可维护性: 对异常进行分类处理,并进行封装输出,比把所有的异常都放在一个catch块来处理。这样不方便维护人员处理,需要深入代码研究。

// 在一个catch块来处理所有异常
public static void doSomething() {
    try{
        // do something 
        ...
    } catch (Exception e){
        log.error("e", e);
    }
}

// 分开处理
public static void doSomething() {
    try{
        // do something 
        ...
    } catch (FileNotFoundException e){
        log.ingo("文件没有找到,做一些处理")
        ...
    } catch (SecurityException e) {
        log.error("无权访问,权限不够...")
    }
    // 这样维护人员就可以针对性的解决问题,研究代码逻辑实际很费时间在紧急情况下
}

c: 解决Java异常机制自身的缺陷:建立异常容器,一次性地对信息进行处理校验,然后返回所有的异常。

d:Java中的异常一次只能抛出一个,比如一个方法有两个逻辑代码片段,在第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了。

// 异常容器
class MyException extends Exception {
    // 容纳所有的异常
    private List<Throwable> causes = new ArrayList<Throwable>();
    // 构造函数,传递一个异常列表
    public MyException(List<? extends Throwable> causes) {
        causes.addAll(causes);
    }
    // 读取所有的异常
    public List<Throwable> getExceptions() {
        return causes;
    }
}

// 例子
public static doSomething() throws MyException {
    List<Throwable> list = new ArrayList<Throwable>();
    // 第一个逻辑片段
    try {
        ...do something
    } catch (Exception e) {
        list.add(e);
    }
    // 第二个逻辑
     try {
        ...do something
    } catch (Exception e) {
        list.add(e);
    }
    // 检查是否有必要抛出异常
    if (list.size() > 0) {
        throw new MyException(list);
    }
}

111、【建议】采用异常链传递异常

  1. 责任链模式(Chain ofResponsibility)将多个对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
  1. 步骤

a: 把FileNotFoundException封装为MyException

b: 抛出到逻辑层,逻辑层根据异常代码(或者自定义的异常类型)确定后续处理逻辑,然后抛出到展现层。

c: 展现层自行决定要展现什么,如果是管理员则可以展现低层级的异常,如果是普通用户则展示封装后的异常。

// 例子
public class IOException extends Exception {
    // 定义异常原因
    public IOException (String msg) {
        super(msg);
    }
    // 定义异常原因 并携带原始异常
    public IOException(String msg, Throwable cause) {
        super(msg, cause);
    }
    // 保留原始异常信息
    public IOException(Throwable cause) {
        super(cause);
    }
}

 // 如何传递异常?捕捉到Exception异常,然后把它转化为IOException异常并抛出=> 异常转译
 try {
     
 } catch (Exception e) {
     throw new IOException(e);
 }
  1. 异常需要封装和传递,在进行系统开发时不要“吞噬”异常,也不要“赤裸裸”地抛出异常,封装后再抛出,或者通过异常链传递,可以达到系统更健壮、友好的目的。

112、【建议】受检异常尽可能转化为非受检异常

  1. 受检异常是正常逻辑的一种补偿处理手段,特别是对可靠性要求比较高的系统来说,在某些条件下必须抛出受检异常以便由程序进行补偿处理。
  1. 受检异常确实有不足的地方 即有异常的地方

a: 受检异常使接口声明脆弱

b: 受检异常使代码的可读性降低 因为要在引用的地方捕获处理异常

c: 受检异常增加了开发工作量

  1. 受检异常威胁到系统的安全性、稳定性、可靠性、正确性时,不能转换为非受检异常。

113、【建议】不要在finally块中处理返回值

  1. 误导开发者
  1. 覆盖了try代码块中的return返回值
  1. 屏蔽异常
package com.hao.example1;

import lombok.extern.slf4j.Slf4j;

/**
 * @author haoxiansheng
 */
@Slf4j
public class test1 {
    public static void main(String[] args) {
        System.out.println(doSomething());
        System.out.println(doSomething1());
        try {

            doSomething2();
        } catch (RuntimeException e) {
            log.info("语句不会执行到的地方");
        }

    }

    public static int doSomething() {
        int a = 1;
        try {
            return a;
        } catch (Exception e) {
            log.error("e", e);
        } finally {
            // 重新修改一下返回值
            a = -1;
        }
        return 0;
    }

    public static Person doSomething1() {
        Person person = new Person();
        person.setName("测试");
        try {

            return person;
        } catch (Exception e) {

        } finally {
            // 修改一下返回值
            person.setName("修改");
        }
        person.setName("llll");
        return person;
    }

    static class Person {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

    // 不要在finally代码块中出现return语句。
    public static void doSomething2() {
        try {
            // 正常抛出异常
            throw new RuntimeException();
        } finally {
            // 告诉JVM: 该方法正常返回
            return;
        }
    }
}

114、【建议】不要在构造函数中抛出异常

  1. Java 异常机制

a: Error类及其子类表示的是错误,它是不需要程序员处理也不能处理的异常,比如VirtualMachineError虚拟机错误,ThreadDeath线程僵死等

b: RuntimeException类及其子类表示的是非受检异常,是系统可能会抛出的异常,程序员可以去处理,也可以不处理,最经典就是NullPointerException空指针异常和IndexOutOfBoundsException越界异常。

c: Exception类及其子类(不包含非受检异常)表示的是受检异常,这是程序员必须处理的异常,不处理则程序不能通过编译,比如IOException表示I/O异常,SQLException表示数据库访问异常。

  1. 构造函数抛出错误是程序员无法处理的

在构造函数执行时,若发生了VirtualMachineError虚拟机错误,那就没招了,只能抛出,程序员不能预知此类错误的发生,也就不能捕捉处理。

  1. 构造函数不应该抛出非受检异常

a: 加重了上层代码编写者的负担

b: 出现异常后续代码不会执行

  1. 构造函数尽可能不要抛出受检异常

a: 导致子类代码膨胀

b: 违背了里氏替换原则

c: 子类构造函数扩展受限

  1. 对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了“对己对人”都是有害的;受检异常尽量不抛出。

115、【建议】使用Throwable获得栈信息

  1. AOP编程可以很轻松地控制一个方法调用哪些类,也能够控制哪些方法允许被调用,一般来说切面编程(比如AspectJ)只能控制到方法级别,不能实现代码级别的植入(Weave),比如一个方法被类A的m1方法调用时返回1,在类B的m2方法调用时返回0(同参数情况下)。
  1. 可以知道类间的调用顺序、方法名称及当前行号等了。
package com.hao.example1;

import lombok.extern.slf4j.Slf4j;

/**
 * @author haoxiansheng
 */
@Slf4j
public class Foo {
    public static boolean m() {
        // 获取当前栈信息
        StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
        // 检查是否是m1方法调用
        for (StackTraceElement stackTraceElement : stackTraceElements) {
            log.info("MethodName=>{}", stackTraceElement.getMethodName());
            log.info("ClassName=>{}", stackTraceElement.getClassName());
            log.info("FileName=>{}", stackTraceElement.getFileName());
            log.info("LineNumber=>{}", stackTraceElement.getLineNumber());
            log.info("isNativeMethod=>{}", stackTraceElement.isNativeMethod());
            if (stackTraceElement.getMethodName().equals("m1")) {
                return true;
            }
        }
        /**
         * 该方法常用作离线注册码校验,当破解者试图暴力破解时,
         * 由于主执行者不是期望的值,因此会返回一个经过包装和混淆的异常信息,大大增加了破解的难度
         */
        // throw new RuntimeException("出m1方法外,该方法不允许其他方法调用");
        return false;

    }
}

class Invoker {
    // 测试
    public static void m1() {
        System.out.println(Foo.m());
    }

    public static void m2() {
        System.out.println(Foo.m());
    }

    public static void main(String[] args) {
        m1();
        m2();
    }
}

116、【建议】异常只为异常服务

  1. 异常只为确实异常的事件服务
  1. 如果当中主逻辑的坏处

a: 异常判断降低了系统性能

b: 降低了代码的可读性

c: 隐藏了运行期可能产生的错误,catch到异常,但没有做任何处理。

117、【建议】多使用异常,把性能问题放一边

  1. 可让正常代码和异常代码分离、能快速查找问题(栈信息快照)等,
  1. 但是异常有一个缺点:性能比较慢。
  1. 性能问题不是拒绝异常的借口。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值