12, 异常处理

1, 捕获了异常后直接生吞。在任何时候,我们捕获了异常都不应该生吞,也就是直接丢弃异常不记录、不抛出。这样的处理方式还不如不捕获异常,因为被生吞掉的异常一旦导致Bug,就很难在程序中找到蛛丝马迹,使得Bug排查工作难上加难

2 ,丢弃异常的原始信息。我们来看两个不太合适的异常处理方式,虽然没有完全生吞异常,但也丢失了宝贵的异常信息。
比如有这么一个会抛出受检异常的方法readFile:

private void readFile() throws IOException {
	Files.readAllLines(Paths.get("a_file"));
}

像这样调用readFile方法,捕获异常后,完全不记录原始异常,直接抛出一个转换后异常,导致出了问题不知道IOException具体是哪里引起的:

@GetMapping("wrong1")
public void wrong1(){
try {
	readFile();
} catch (IOException e) {2020/6/11 12 | 异常处理:别让自己在出问题的时候变为瞎子
	//原始异常信息丢失
	throw new RuntimeException("系统忙请稍后再试");
}
}

正确处理方式

catch (IOException e) {
	log.error("文件读取错误", e);
	throw new RuntimeException("系统忙请稍后再试");
}

或把原始异常作为转换后新异常的cause,原始异常信息同样不会丢

catch (IOException e) {
	throw new RuntimeException("系统忙请稍后再试", e);
}
finaly中的异常
@GetMapping("wrong")
public void wrong() {
try {
	log.info("try");
	//异常丢失
	throw new RuntimeException("try");
} finally {
	log.info("finally");
	throw new RuntimeException("finally");
}
}
结果
[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in c
java.lang.RuntimeException: finally

最后在日志中只能看到finally中的异常,虽然try中的逻辑出现了异常,但却被finally中的异常覆盖了。

修复方法:
1,finally代码块自己负责异常捕获和处理

@GetMapping("right")
public void right() {
try {
	log.info("try");
	throw new RuntimeException("try");
} finally {
log.info("finally");
try {
	throw new RuntimeException("finally");
} catch (Exception ex) {
	log.error("finally", ex);
}
}
}

2,可以把try中的异常作为主异常抛出,使用addSuppressed方法把finally中的异常附加到主异常上

@GetMapping("right2")
public void right2() throws Exception {
Exception e = null;
try {
	log.info("try");
	throw new RuntimeException("try");
} catch (Exception ex) {
e = ex;
} finally {
	log.info("finally");
	try {
	throw new RuntimeException("finally");
	} catch (Exception ex) {
		if (e!= null) {
			e.addSuppressed(ex);
	} else {
		e = ex;
	}
	}
	} 
	throw e;
}
结果
java.lang.RuntimeException: try
at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.right2(FinallyIssueController.java:69)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
Suppressed: java.lang.RuntimeException: finally
at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.right2(FinallyIssueController.java:75)
... 54 common frames omitted

3, 对于实现了AutoCloseable接口的资源,建议使用try-with-resources来释放资源,

@GetMapping("useresourceright")
public void useresourceright() throws Exception {
try (TestResource testResource = new TestResource()){
	testResource.read();
}
}
千万别把异常定义为静态变量
把异常定义为静态变量会导致异常信息固化,这就和异常的栈一定是需要根据当前调用来动态获取相矛盾。 ```java 错误定义方式 public class Exceptions { public static BusinessException ORDEREXISTS = new BusinessException("订单已经存在", 3001); ... } ```
@GetMapping("wrong")
public void wrong() {
try {
createOrderWrong();
} catch (Exception ex) {
log.error("createOrder got error", ex);
} t
ry {
cancelOrderWrong();
} catch (Exception ex) {
log.error("cancelOrder got error", ex);
}
} p
rivate void createOrderWrong() {
//这里有问题
throw Exceptions.ORDEREXISTS;
} p
rivate void cancelOrderWrong() {
//这里有问题
throw Exceptions.ORDEREXISTS;
}

结果: 运行程序后看到如下日志,cancelOrder got error的提示对应了createOrderWrong方法。显然,cancelOrderWrong方法在出错后抛出的异常,其实是createOrderWrong方法出错的异常:
[14:05:25.782] [http-nio-45678-exec-1] [ERROR] [.c.e.d.PredefinedExceptionController:25 ] - cancelOrder got error
org.geekbang.time.commonmistakes.exception.demo2.BusinessException: 订单已经存在
at org.geekbang.time.commonmistakes.exception.demo2.Exceptions.<clinit>(Exceptions.java:5)
at org.geekbang.time.commonmistakes.exception.demo2.PredefinedExceptionController.createOrderWrong(PredefinedExceptionController.java:50)
at org.geekbang.time.commonmistakes.exception.demo2.PredefinedExceptionController.wrong(PredefinedExceptionController.java:18)

修改方式

public class Exceptions {
public static BusinessException orderExists(){
return new BusinessException("订单已经存在", 3001);
}
}
提交线程池的任务出了异常会怎么样
确保正确处理了线程池中任务的异常,如果任务通过execute提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;

如果任务通过submit提交意味着我们关心任务
的执行结果,应该通过拿到的Future调用其get方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了

new ThreadFactoryBuilder()
.setNameFormat(prefix+"%d")
.setUncaughtExceptionHandler((thread, throwable)-> log.error("ThreadPool {} got exception", thread, throwable))
.get()
@GetMapping("execute")
public void execute() throws InterruptedException {
String prefix = "test";
ExecutorService threadPool = Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setNameFormat(prefix+"%d").get());
//提交10个任务到线程池处理,第5个任务会抛出运行时异常
IntStream.rangeClosed(1, 10).forEach(i -> threadPool.execute(() -> {
if (i == 5) throw new RuntimeException("error");
log.info("I'm done : {}", i);
}));
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
}

结果:
[14:33:55.990] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:26 ] - I'm done : 4
Exception in thread "test0" java.lang.RuntimeException: error
at org.geekbang.time.commonmistakes.exception.demo3.ThreadPoolAndExceptionController.lambda$null$0(ThreadPoolAndExceptionController.java:25
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
[14:33:55.990] [test1] [INFO ] [e.d.ThreadPoolAndExceptionController:26 ] - I'm done : 6
...

修改:
1, 以execute方法提交到线程池的异步任务,最好在任务内部做好异常处理;
2. 设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序:

修改后

[15:44:33.769] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 1
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 2
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 3
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 4
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 6
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 7
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 8
[15:44:33.771] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 9
[15:44:33.771] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47 ] - I'm done : 10

submit提交方式

List<Future> tasks = IntStream.rangeClosed(1, 10).mapToObj(i -> threadPool.submit(() -> {
if (i == 5) throw new RuntimeException("error");2020/6/11 12 | 异常处理:别让自己在出问题的时候变为瞎子
https://time.geekbang.org/column/article/220230 8/13
log.info("I'm done : {}", i);
})).collect(Collectors.toList());
tasks.forEach(task-> {
try {
	task.get();
} catch (Exception e) {
	log.error("Got exception", e);
}
});

结果:
[15:44:13.543] [http-nio-45678-exec-1] [ERROR] [e.d.ThreadPoolAndExceptionController:69 ] - Got exception
java.util.concurrent.ExecutionException: java.lang.RuntimeException: error
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章