110、【建议】异常封装
- 为什么要有异常封装?
a: Java语言的异常处理机制可以确保程序的健壮性,提高系统的可用率,但是Java API提供的异常都是比较低级的,只有开发人员才能看得懂,对于用户(客户端)来说就是天书=> 不友好的提示。
- 异常封装的优点
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、【建议】采用异常链传递异常
- 责任链模式(Chain ofResponsibility)将多个对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
- 步骤
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);
}
- 异常需要封装和传递,在进行系统开发时不要“吞噬”异常,也不要“赤裸裸”地抛出异常,封装后再抛出,或者通过异常链传递,可以达到系统更健壮、友好的目的。
112、【建议】受检异常尽可能转化为非受检异常
- 受检异常是正常逻辑的一种补偿处理手段,特别是对可靠性要求比较高的系统来说,在某些条件下必须抛出受检异常以便由程序进行补偿处理。
- 受检异常确实有不足的地方 即有异常的地方
a: 受检异常使接口声明脆弱
b: 受检异常使代码的可读性降低 因为要在引用的地方捕获处理异常
c: 受检异常增加了开发工作量
- 受检异常威胁到系统的安全性、稳定性、可靠性、正确性时,不能转换为非受检异常。
113、【建议】不要在finally块中处理返回值
- 误导开发者
- 覆盖了try代码块中的return返回值
- 屏蔽异常
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、【建议】不要在构造函数中抛出异常
- Java 异常机制
a: Error类及其子类表示的是错误,它是不需要程序员处理也不能处理的异常,比如VirtualMachineError虚拟机错误,ThreadDeath线程僵死等
b: RuntimeException类及其子类表示的是非受检异常,是系统可能会抛出的异常,程序员可以去处理,也可以不处理,最经典就是NullPointerException空指针异常和IndexOutOfBoundsException越界异常。
c: Exception类及其子类(不包含非受检异常)表示的是受检异常,这是程序员必须处理的异常,不处理则程序不能通过编译,比如IOException表示I/O异常,SQLException表示数据库访问异常。
- 构造函数抛出错误是程序员无法处理的
在构造函数执行时,若发生了VirtualMachineError虚拟机错误,那就没招了,只能抛出,程序员不能预知此类错误的发生,也就不能捕捉处理。
- 构造函数不应该抛出非受检异常
a: 加重了上层代码编写者的负担
b: 出现异常后续代码不会执行
- 构造函数尽可能不要抛出受检异常
a: 导致子类代码膨胀
b: 违背了里氏替换原则
c: 子类构造函数扩展受限
- 对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了“对己对人”都是有害的;受检异常尽量不抛出。
115、【建议】使用Throwable获得栈信息
- AOP编程可以很轻松地控制一个方法调用哪些类,也能够控制哪些方法允许被调用,一般来说切面编程(比如AspectJ)只能控制到方法级别,不能实现代码级别的植入(Weave),比如一个方法被类A的m1方法调用时返回1,在类B的m2方法调用时返回0(同参数情况下)。
- 可以知道类间的调用顺序、方法名称及当前行号等了。
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、【建议】异常只为异常服务
- 异常只为确实异常的事件服务
- 如果当中主逻辑的坏处
a: 异常判断降低了系统性能
b: 降低了代码的可读性
c: 隐藏了运行期可能产生的错误,catch到异常,但没有做任何处理。
117、【建议】多使用异常,把性能问题放一边
- 可让正常代码和异常代码分离、能快速查找问题(栈信息快照)等,
- 但是异常有一个缺点:性能比较慢。
- 性能问题不是拒绝异常的借口。