异常处理是我们每天都需要面对的问题,但很多代码中往往会出现:
反例 1:
重复且繁琐的的异常处理逻辑
@Slf4j
public class DuplicatedExceptionHandlerExample {
// 堆代码 duidaima.com
private UserService userService;
public User query(String id) {
try {
return userService.query(id);
} catch (Exception e) {
log.error("query error, userId: {}", id, e);
return null;
}
}
public User create(String id) {
try {
return userService.create(id);
} catch (Exception e) {
log.error("query error, userId: {}", id, e);
return null;
}
}
}
反例 2:
异常被吞掉或者丢失部分信息
@Slf4j
public class ExceptionShouldLogOrThrowExample {
private UserService userService;
public User query(String id) {
try {
return userService.query(id);
} catch (Exception e) {
// 异常被吞并, 问题被隐藏
return null;
}
}
public User create(String id) {
try {
return userService.create(id);
} catch (Exception e) {
// 堆代码 duidaima.com
// 堆栈丢失, 后续难以定位问题
log.error("query error, userId: {}, error: {}", id,e.getMessage() );
return null;
}
}
}
反例 3:
对外抛出未知异常, 导致调用方序列化失败
public class OpenAPIService {
public void handle(){
// HSF 服务对外抛出 client 中未定义的异常, 调用方反序列化失败
throw new InternalSystemException("");
}
}
正确方式1.通过 AOP 统一异常处理
1.避免未知异常抛给调用方, 将未知异常转为 Result 或者通用异常类型
2.统一异常日志的打印和监控
正确方式2.处理 Checked Exception
Checked Exception 是在编译期要求必须处理的异常,也就是非 RuntimeException 类型的异常,但 Java Checked 的异常给接口的调用者造成了一定的负担,导致异常声明层层传递,如果顶层能够处理该异常,我们可以通过 lombok 的 @SneakyThrows 注解规避 Checked exception
正确方式3.Try catch 线程逻辑
反例:
@RequiredArgsConstructor
public class ThreadNotTryCatch {
private final ExecutorService executorService;
public void handle() {
executorService.submit(new Runnable() {
@Override
public void run() {
// 未捕获异常, 线程直接退出, 异常信息丢失
remoteInvoke();
}
});
}
}
正例:
@RequiredArgsConstructor
@Slf4j
public class ThreadNotTryCatch {
private final ExecutorService executorService;
public void handle() {
executorService.submit(new Runnable() {
@Override
public void run() {
try {
remoteInvoke();
} catch (Exception e) {
log.error("handle failed", e);
}
}
});
}
}
特殊异常的处理
InterruptedException 一般是上层调度者主动发起的中断信号,例如某个任务执行超时,那么调度者通过将线程置为 interuppted 来中断任务,对于这类异常我们不应该在 catch 之后忽略,应该向上抛出或者将当前线程置为 interuppted。
反例:
public class InterruptedExceptionExample {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
public void handleWithTimeout() throws InterruptedException {
Future<?> future = executorService.submit(() -> {
try {
// sleep 模拟处理逻辑
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
System.out.println("continue task");
// 异常被忽略, 继续处理
});
// 等待任务结果, 如果超过 500ms 则中断
Thread.sleep(500);
if (!future.isDone()) {
System.out.println("cancel");
future.cancel(true);
}
}
}
避免 catch Error
不要吞并 Error,Error 设计本身就是区别于异常,一般不应该被 catch,更不能被吞掉。举个例子,OOM 有可能发生在任意代码位置,如果吞并 Error,让程序继续运行,那么以下代码的 start 和 end 就无法保证一致性。
public class ErrorExample {
private Date start;
private Date end;
public synchronized void update(long start, long end) {
if (start > end) {
throw new IllegalArgumentException("start after end");
}
this.start = new Date(start);
// 如果 new Date(end) 发生 OOM, start 有可能大于 end
this.end = new Date(end);
}
}