code-learning/netty/26-Netty 源码解析-ChannelPipeline(六)之异常事件的传播.md

229 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 精尽 Netty 源码解析 —— ChannelPipeline之异常事件的传播
# 1. 概述
在 [《精尽 Netty 源码解析 —— ChannelPipeline之 Outbound 事件的传播》](http://svip.iocoder.cn/Netty/Pipeline-4-outbound/) 和 [《精尽 Netty 源码解析 —— ChannelPipeline之 Inbound 事件的传播》](http://svip.iocoder.cn/Netty/Pipeline-5-inbound/) 中,我们看到 Outbound 和 Inbound 事件在 pipeline 中的传播逻辑。但是,无可避免,传播的过程中,可能会发生异常,那是怎么处理的呢?
本文,我们就来分享分享这块。
# 2. notifyOutboundHandlerException
我们以 Outbound 事件中的 **bind** 举例子,代码如下:
```
// AbstractChannelHandlerContext.java
private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
if (invokeHandler()) { // 判断是否符合的 ChannelHandler
try {
// 调用该 ChannelHandler 的 bind 方法 <1>
((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise); // 通知 Outbound 事件的传播,发生异常 <2>
}
} else {
// 跳过,传播 Outbound 事件给下一个节点
bind(localAddress, promise);
}
}
```
-`<1>` 处,调用 `ChannelOutboundHandler#bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)` 方法**发生异常**时,会在 `<2>` 处调用 `AbstractChannelHandlerContext#notifyOutboundHandlerException(Throwable cause, ChannelPromise promise)` 方法,通知 Outbound 事件的传播,发生异常。
- 其他 Outbound 事件,大体的代码也是和 `#invokeBind(SocketAddress localAddress, ChannelPromise promise)` 是一致的。如下图所示:[![类图](26-Netty 源码解析-ChannelPipeline之异常事件的传播.assets/01.png)](http://static.iocoder.cn/images/Netty/2018_06_16/01.png)类图
------
`AbstractChannelHandlerContext#notifyOutboundHandlerException(Throwable cause, ChannelPromise promise)` 方法,通知 Outbound 事件的传播,发生异常。代码如下:
```
private static void notifyOutboundHandlerException(Throwable cause, ChannelPromise promise) {
// Only log if the given promise is not of type VoidChannelPromise as tryFailure(...) is expected to return
// false.
PromiseNotificationUtil.tryFailure(promise, cause, promise instanceof VoidChannelPromise ? null : logger);
}
```
- 在方法内部,会调用 `PromiseNotificationUtil#tryFailure(Promise<?> p, Throwable cause, InternalLogger logger)` 方法,通知 bind 事件对应的 Promise 对应的监听者们。代码如下:
```
public static void tryFailure(Promise<?> p, Throwable cause, InternalLogger logger) {
if (!p.tryFailure(cause) && logger != null) {
Throwable err = p.cause();
if (err == null) {
logger.warn("Failed to mark a promise as failure because it has succeeded already: {}", p, cause);
} else {
logger.warn(
"Failed to mark a promise as failure because it has failed already: {}, unnotified cause: {}",
p, ThrowableUtil.stackTraceToString(err), cause);
}
}
}
```
- 以 bind 事件来举一个监听器的例子。代码如下:
```
ChannelFuture f = b.bind(PORT).addListener(new ChannelFutureListener() { // <1> 监听器就是我!
@Override
public void operationComplete(ChannelFuture future) throws Exception {
System.out.println("异常:" + future.casue());
}
}).sync();
```
- `<1>` 处的监听器,就是示例。当发生异常时,就会通知该监听器,对该异常做进一步**自定义**的处理。**也就是说,该异常不会在 pipeline 中传播**。
- 我们再来看看怎么通知监听器的源码实现。调用 `DefaultPromise#tryFailure(Throwable cause)` 方法,通知 Promise 的监听器们,发生了异常。代码如下:
```
@Override
public boolean tryFailure(Throwable cause) {
if (setFailure0(cause)) { // 设置 Promise 的结果
// 通知监听器
notifyListeners();
// 返回成功
return true;
}
// 返回失败
return false;
}
```
- 若 `DefaultPromise#setFailure0(Throwable cause)` 方法,设置 Promise 的结果为方法传入的异常。但是有可能会传递失败例如说Promise 已经被设置了结果。
- 如果该方法返回 `false` 通知 Promise 失败,那么 `PromiseNotificationUtil#tryFailure(Promise<?> p, Throwable cause, InternalLogger logger)` 方法的后续,就会使用 `logger` 打印错误日志。
# 3. notifyHandlerException
我们以 Inbound 事件中的 **fireChannelActive** 举例子,代码如下:
```
private void invokeChannelActive() {
if (invokeHandler()) { // 判断是否符合的 ChannelHandler
try {
// 调用该 ChannelHandler 的 Channel active 方法 <1>
((ChannelInboundHandler) handler()).channelActive(this);
} catch (Throwable t) {
notifyHandlerException(t); // 通知 Inbound 事件的传播,发生异常 <2>
}
} else {
// 跳过,传播 Inbound 事件给下一个节点
fireChannelActive();
}
}
```
- 在 `<1>` 处,调用 `ChannelInboundHandler#channelActive(ChannelHandlerContext ctx)` 方法**发生异常**时,会在 `<2>` 处调用 `AbstractChannelHandlerContext#notifyHandlerException(Throwable cause)` 方法,通知 Inbound 事件的传播,发生异常。
- 其他 Inbound 事件,大体的代码也是和
```
#invokeChannelActive()
```
是一致的。如下图所示:
![类图](26-Netty 源码解析-ChannelPipeline之异常事件的传播.assets/02.png)
类图
- 😈 **注意,笔者在写的时候,突然发现 Outbound 事件中的 read 和 flush 的异常处理方式和 Inbound 事件是一样的**。
- 😈 **注意,笔者在写的时候,突然发现 Outbound 事件中的 read 和 flush 的异常处理方式和 Inbound 事件是一样的**。
- 😈 **注意,笔者在写的时候,突然发现 Outbound 事件中的 read 和 flush 的异常处理方式和 Inbound 事件是一样的**。
------
`AbstractChannelHandlerContext#notifyHandlerException(Throwable cause)` 方法,通知 Inbound 事件的传播,发生异常。代码如下:
```
private void notifyHandlerException(Throwable cause) {
// <1> 如果是在 `ChannelHandler#exceptionCaught(ChannelHandlerContext ctx, Throwable cause)` 方法中,仅打印错误日志。否则会形成死循环。
if (inExceptionCaught(cause)) {
if (logger.isWarnEnabled()) {
logger.warn(
"An exception was thrown by a user handler " +
"while handling an exceptionCaught event", cause);
}
return;
}
// <2> 在 pipeline 中,传播 Exception Caught 事件
invokeExceptionCaught(cause);
}
```
- `<1>` 处,调用 `AbstractChannelHandlerContext#inExceptionCaught(Throwable cause)` 方法,如果是在 `ChannelHandler#exceptionCaught(ChannelHandlerContext ctx, Throwable cause)` 方法中,**发生异常**,仅打印错误日志,**并 `return` 返回** 。否则会形成死循环。代码如下:
```
private static boolean inExceptionCaught(Throwable cause) {
do {
StackTraceElement[] trace = cause.getStackTrace();
if (trace != null) {
for (StackTraceElement t : trace) { // 循环 StackTraceElement
if (t == null) {
break;
}
if ("exceptionCaught".equals(t.getMethodName())) { // 通过方法名判断
return true;
}
}
}
cause = cause.getCause();
} while (cause != null); // 循环异常的 cause() ,直到到没有
return false;
}
```
- 通过 StackTraceElement 的方法名来判断,是不是 `ChannelHandler#exceptionCaught(ChannelHandlerContext ctx, Throwable cause)` 方法。
- `<2>` 处,调用 `AbstractChannelHandlerContext#invokeExceptionCaught(Throwable cause)` 方法,在 pipeline 中,传递 Exception Caught 事件。在下文中,我们会看到,和 [《精尽 Netty 源码解析 —— ChannelPipeline之 Inbound 事件的传播》](http://svip.iocoder.cn/Netty/Pipeline-5-inbound/) 的逻辑( `AbstractChannelHandlerContext#invokeChannelActive()` )是**一致**的。
- 比较特殊的是Exception Caught 事件在 pipeline 的起始节点,不是 `head` 头节点,而是**发生异常的当前节点开始**。怎么理解好呢?对于在 pipeline 上传播的 Inbound **xxx** 事件,在发生异常后,转化成 **Exception Caught** 事件,继续从当前节点,继续向下传播。
- 如果 **Exception Caught** 事件在 pipeline 中的传播过程中,一直没有处理掉该异常的节点,最终会到达尾节点 `tail` ,它对 `#exceptionCaught(ChannelHandlerContext ctx, Throwable cause)` 方法的实现,代码如下:
```
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
onUnhandledInboundException(cause);
}
```
- 在方法内部,会调用 `DefaultChannelPipeline#onUnhandledInboundException(Throwable cause)` 方法,代码如下:
```
/**
* Called once a {@link Throwable} hit the end of the {@link ChannelPipeline} without been handled by the user
* in {@link ChannelHandler#exceptionCaught(ChannelHandlerContext, Throwable)}.
*/
protected void onUnhandledInboundException(Throwable cause) {
try {
logger.warn(
"An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
"It usually means the last handler in the pipeline did not handle the exception.",
cause);
} finally {
ReferenceCountUtil.release(cause);
}
}
```
- 打印**告警**日志,并调用 `ReferenceCountUtil#release(Throwable)` 方法,释放需要释放的资源。
- 从英文注释中,我们也可以看到,这种情况出现在**使用者**未定义合适的 ChannelHandler 处理这种异常,所以对于这种情况下,`tail` 节点只好打印**告警**日志。
- 实际使用时,笔者建议胖友一定要定义 ExceptionHandler ,能够处理掉所有的异常,而不要使用到 `tail` 节点的异常处理。😈
- 好基友【闪电侠】对尾节点 `tail` 做了很赞的总结
> 总结一下tail 节点的作用就是结束事件传播,并且对一些重要的事件做一些善意提醒
# 666. 彩蛋
推荐阅读文章:
- 闪电侠 [《netty 源码分析之 pipeline(二)》](https://www.jianshu.com/p/087b7e9a27a2)