关于异常处理的解决方案

来源:互联网 发布:linux文件写入权限 编辑:程序博客网 时间:2024/06/10 02:11

许多网友们都对 Smart Framework 的异常处理机制抱有疑问,我想很有必要补充一篇博文,描述一下为何我要采用基于“错误代码”的解决方案,来替换原有 Java 异常处理方案。

先来回顾一下 Java 异常处理的来龙去脉。

早在 JDK 1.0 的时候,Sun 公司的牛逼人物 Josh Bloch 就写了一个 Throwable 类,它是所有异常的父类,包括两个子类:Error 与 Exception

Error 表示程序运行错误,立马需停止执行。此时没必要使用 try-catch 结构来捕获 Error 错误,因为 catch 块根本就就进不去

Exception 就不用解释了,我们太熟悉了,可以用 try-catch 结构来捕获异常。

当 Exception 出世后,程序可以这样写了:

先定义一个接口:

?
1
2
3
4
publicinterface Greeting {
 
    voidsayHello(String name) throwsException;
}

在方法声明处定义了所抛出异常的类型,可以抛出多个异常,每个异常类需要用逗号隔开。

假设我们写一个实现类,在其中判断参数是否为 null,若为 null,就抛出 Exception。

?
1
2
3
4
5
6
7
8
9
10
publicclass GreetingImpl implementsGreeting {
 
    @Override
    publicvoid sayHello(String name) throwsException {
        if(name == null) {
            thrownew Exception("The name is null.");
        }
        System.out.println("Hello! " + name);
    }
}

在 Exception 的构造方法中传入异常消息。如果抛出了异常,后面的 System.out.println() 语句是绝对不会执行的。那下一步执行什么呢?或者说,当前线程跑到哪里去了呢?跑到调用这个方法的地方了,请看如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
publicclass Client {
 
    publicstatic void main(String[] args) {
        Greeting greeting = newGreetingImpl();
        try{
            greeting.sayHello(null);
        }catch(Exception e) {
            System.out.println("Error! Message: " + e.getMessage());
        }
    }
}

通过一个 try-catch 语句,捕获了 Exception 异常,线程进入了 catch 块中,使用控制台输出了异常信息。

也就是说,异常是从下往上抛的。打个比方吧,当项目组里出了一个小问题,Leader 看了一下,自己管不了,就向上抛给 Manager 去处理,Manager 看了一下发现自己也管不了,于是就继续往上抛,抛给了 Boss,此时,Boss 再也不能向上抛了,只有自己解决(当然有这样的 Boss 那真是一件幸福的事了)。在这个例子中,最终就交给 Client 类来处理了,看看输出的异常信息就明白了。

OK,这就是 Java 异常的基本用法。

需要注意的是,凡是在方法签名中 throws 了 Exception 或其子类,必须在调用的时候使用 try-catch,这是 Java 的语言规范(可不是我规定的)。这就是传说中的“Checked Exception(受检异常)”。

如果在项目开发中经常使用受检异常,不见得是一件好事。举个例子,某方法定义可抛出 A 受检异常,这个方法已经被许多地方调用了,现在突然增加了 B 受检异常,那么也就意味着,所有调用该方法的地方都会报错,除非 catch 的是 Exception 或 Throwable。这种改变受检异常的方式需要谨慎使用。

与受检异常对应就是 Unchecked Exception(非受检异常)了,它有什么特点呢?

不妨先来举个非受检异常的例子,最经典的就是 RuntimeException 了,它是 Sun 公司年轻的程序员屌丝 Frank Yellin 的杰作。他提出,可以将方法签名无需显式地 throws 某个异常,而是在方法体中通过 throw 语句抛出 RuntimeException。看起来就像这样:

接口不再定义受检异常:

?
1
2
3
4
publicinterface Greeting {
 
    voidsayHello(String name);
}

在实现类的具体方法执行的时候抛出 RuntimeException:

?
1
2
3
4
5
6
7
8
9
10
publicclass GreetingImpl implementsGreeting {
 
    @Override
    publicvoid sayHello(String name) {
        if(name == null) {
            thrownew RuntimeException("The name is null.");
        }
        System.out.println("Hello! " + name);
    }
}

这样处理以后,Client 再也不需要 try-catch 了:

?
1
2
3
4
5
6
7
publicclass Client {
 
    publicstatic void main(String[] args) {
        Greeting greeting = newGreetingImpl();
        greeting.sayHello(null);
    }
}

代码确实精简了不少,脱掉了恶心的 try-catch 这件衣服,但是这样再也不能捕获异常了,因为非受检异常是隐性的,除非在项目中约定,这个方法可能会抛出某某异常,然后调用的人还是用 try-catch 去捕获这些异常。总之,非受检异常是代码干净了,但需要增加文档约束,所以就必须写一大堆的 JavaDoc 了。

?
1
2
3
4
5
6
7
8
9
10
11
publicclass Client {
 
    publicstatic void main(String[] args) {
        Greeting greeting = newGreetingImpl();
        try{
            greeting.sayHello(null);
        }catch(RuntimeException e) {
            System.out.println("Error! Message: " + e.getMessage());
        }
    }
}

看来,还是脱不了那件衣服啊!

既然脱不掉,就多买几件好看的衣服吧。于是,后面就有了 IOException、FileNotFoundExceptoin、NullPointerException 等等这些异常类了。

用一张图来表达这个异常结构吧:

上图清晰地表明了,红色的是受检异常,蓝色的是非受检异常。

当然这些都是 JDK 给我们提供的,我们可以随时拿来使用,但情况远远没有那么理想。有些情况下,我们发现这些异常根本难以表达业务的具体含义,于是我们可以通过继承的方式来自定义异常,或者说扩展异常。看起来是这样的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicclass ParameterNullException extendsRuntimeException {
 
   publicParameterNullException() {
   }
 
   publicParameterNullException(String message) {
        super(message);
   }
 
   publicParameterNullException(String message, Throwable cause) {
        super(message, cause);
   }
 
   publicParameterNullException(Throwable cause) {
        super(cause);
   }
}

上面自定义了一个非受检异常 ParameterNullException(表示当参数为空时的异常),因为它直接继承了 RuntimeException。也就是说,这个异常可以隐式抛出,无需定义在方法签名上。看起来和使用 RuntimeException 差不多:

?
1
2
3
4
5
6
7
8
9
10
publicclass GreetingImpl implementsGreeting {
 
    @Override
    publicvoid sayHello(String name) {
        if(name == null) {
            thrownew ParameterNullException("The name is null.");
        }
        System.out.println("Hello! " + name);
    }
}

没错!只是改了一个名字,这样代码可读性确实上升了不少,至少可以做到望名生意了。

有了这个法宝以后,在 Java 项目中大量出现自定义异常,管理起来非常不便,而且有可能多个自定义异常类只是名字不同而已,意义却是完全相同的。

能否有更好的方法来改进异常处理行为呢?即需要保证有异常中止的功能,又需要保证有具体的业务意义,还需要保证代码可读性良好。

于是,我借鉴了 C/C++ 中的“错误代码”编程风格,现在是这样处理异常的:

?
1
2
3
4
5
6
7
8
9
10
11
publicclass GreetingImpl implementsGreeting {
 
    @Override
    publicint sayHello(String name) {
        if(name == null) {
            return1;// Error: The name is null.
        }
        System.out.println("Hello! " + name);
        return0;// Success
    }
}

方法签名中无需声明任何受检异常,也无需在方法体中抛出非受检异常,只需提供方法返回值(假设为 int 类型,当然也可以为 String 类型),当返回 0 时,表示操作成功;当非 0 时,表示操作失败。对于非零的情况,可以自由定义错误代码,比如:1 表示参数为空,2 表示...,3 表示...。这些返回值就是错误代码。

在 Client 中是这样进行错误处理的:

?
1
2
3
4
5
6
7
8
9
10
publicclass Client {
 
    publicstatic void main(String[] args) {
        Greeting greeting = newGreetingImpl();
        intresult = greeting.sayHello(null);
        if(result == 1) {
            System.out.println("Error! Code: " + result);
        }
    }
}

没有了 try-catch,代码精简了。与受检异常完全不同,若方法中变更了错误代码,对调用方没有任何影响。此方案与非受检异常相似,同样需要给出一定的契约或者说是规范,让调用者知道该方法具体会返回多少种错误情况。于此不同的是,可以直接将这个错误代码返回到前端,通过 JS 来获取错误代码,从而给出相应的错误提示信息。在这一点上,似乎比非受检异常要强大一些。为了提高可读性与防止手误,可以将常用的返回值定义为常量。

细心的读者肯定会发现一个问题,如果方法本身就需要返回值,而这里的错误代码却充当了返回值,一个方法可以返回多个值吗?

可以的。那就是将返回值封装成 JavaBean 的样子,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
publicclass Result extendsBaseBean {
 
   privateboolean success = true;
   privateint error = 0;
   privateObject data = null;
 
    publicResult(booleansuccess) {
        this.success = success;
    }
 
    publicResult data(Object data) {
        this.data = data;
        returnthis;
    }
 
    publicResult error(interror) {
        this.error = error;
        returnthis;
    }
...
}
将方法的返回值统一为 Result 对象,该对象包括三个属性:
  1. success:操作是否成功(默认为 true,表示成功
  2. error:错误代码(默认为 0,表示没有错误)
  3. data:真正的返回值(默认为 null,表示没有返回值)

此外,还通过链式调用进行传递这些属性,例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
publicclass ProductAction extendsBaseAction {
...
    @Request("get:/product/{id}")
    publicResult getProductById(longproductId) {
        if(productId == 0) {
            returnnew Result(false).error(ERROR_PARAM);
        }
        Product product = productService.getProduct(productId);
        if(product != null) {
            returnnew Result(true).data(product);
        }else{
            returnnew Result(false).error(ERROR_DATA);
        }
    }
...

以上是 Action 的写法,可以 if 语句检测方法参数是否有效,也可以检测 Service 方法返回值是否有效,通过链式操作来创建 Result 对象,并为其属性赋值。

代码可读性增强了,程序员可将更多的精力放在业务流程上,而并非那些 try-catch 上。

期待您的宝贵建议!如果对您有帮助,请顶起来吧!谢谢!







---精彩的评论

引用来自“黄勇”的评论

引用来自“陈春伟”的评论

连续看了几篇文章了。这篇也由的想说几句。一定程度上看,你的思想又要倒退了。退到了错误码时代。其实异常的提出,倒不是说是用来取代错误码的。错误码有它的应用场景,在异构平台的rpc等交互时,错误码有它的优势和通用性。但是错误码的缺点也明显。1、默认忽略性、需要人的主动性。2、不能跨作用域传送,无法冒泡传递,如果需要把持久层更底层的错误传递到UI,需在反复的手动(当然可以用工具类)包装往上传递,3、它缺少堆栈的包装信息,如果为了查错需要。需要自己包装堆栈。4、异常能独立于返回值独立成体系,方便统一的捕获(AOP)来处理。给关注分离带来好处。合理的使用异常和错误码才是更正确的选择。异常体系是每个高级语言非常重要的组成部分,为什么每个语言都设计了强大的异常体系,证明了它在面向对象世界里的位置,所以一旦你对某种体系有比较大的怀疑的时候,一定要想办法理解它更深层的用意。这样更有利于你去选择。这个纯个人感受。不代码对楼主的评价。

感谢您专业的评价!没错,其实我写这篇文章,并非是否定异常处理,而是想让大家知道,通过错误代码也是可以解决问题的,尤其是在 Action 里面,由于 Action 是处理业务逻辑的,如果混杂了大量的 try-catch,势必会让人无法关注在核心的业务逻辑上。什么地方应该用异常处理,什么时候应该用错误代码,需要根据不同的场景来做出选择,这是我想表达的。

这位哥们写的很全面了,提一点个人看法吧:

就如所说的,错误码在异构平台的交互就比较有用,例如Ajax的Action层面,我觉得Struts的返回值就是这种思想。Action往外就是前端了,定义异常也没有意义,错误码反而把所有情况都限定好了,更合理一些,一句话,不折腾!

另外,返回码更适合API双方约定的能预料到的错误(业务中的错误),如果一些IO异常之类的东西,我觉得还是异常更合适。

另外之二,Java中的try-catch确实非常破坏代码结构,特别是涉及到变量作用域的时候更加丑陋,需要写很多"Type value=null;"这样的声明,看不惯也很久了。


-------

看了文章,还是认为采用错误码的方式很不友好,并且不赞成使用错误码。1.从业务逻辑上来讲,采用错误码并不能很好融入到业务上,相比之下枚举类型更好点,如果使用错误码,那必然有很多不确定性以及后期维护的困难。程序员除了要知道本身的业务之外,还得熟知每一个码对应的意思。而Exception则很清楚,很有针对性
2.从编程角度上来说,存在很多不确定性,比如若采用int类型,那么所有int类型的数值,编译器会认为都是正确的,如果在运行是出了问题,必然是层层debug才能发现。这无疑降低了开发和维护的效率。
3.正如文中所描述的,当真正需要返回值的时候如如何操作,文中是将其封装到一个bean中,这固然可以解决这一问题,但随之而来的却是业务逻辑的不清晰,首先在Result这个类中,data类型是一个Object,那么我每次调用的时候都要强制转换吗,当然,这个也很好解决,采用类泛型就可以了,假设这里使用泛型,但是如果调用端采用的是不带泛型的Result作为变量声明的时候呢。
4.开发效率问题,如果返回码很多怎么办,不停的加if else,这是件很痛苦的事情。当然exception也是需要加try catch的。但是有一点很重要就是现在的IDE,在编程的时候IDE就能帮助你创建catch异常的代码,这点可以很大程度的提升开发效率,如果你添加一种异常,编译器,IDE也能很及时的提示你,这样能在开发期间降低后期维护的成本。

最后就是,现在开发框架不仅仅要关注框架本身,还要整合好IDE,第三方工具如maven,ant, 协调好这些就能提升开发效率和运行效率。

个人愚见,见笑了




0 0
原创粉丝点击