在Netty中,您拥有入站和出站处理程序的概念。只需在管道的末尾(尾部)添加一个通道处理程序并实现exceptionCaught
覆盖,即可实现catch-all入站异常处理程序。沿着入站管道发生的异常将沿着处理程序行进,直到遇到最后一个,如果没有沿途处理的话。
与传出处理程序完全相反。相反(根据Netty in Action,第94页),你需要为频道 Future
添加一个监听器,或者向Promise
传入一个监听器。 write
的{{1}}方法。
由于我不确定在哪里插入前者,我以为我会选择后者,所以我做了以下Handler
:
ChannelOutboundHandler
这被添加到管道的负责人:
}
/**
* Catch and log errors happening in the outgoing direction
*
* @see <p>p94 in "Netty In Action"</p>
*/
private ChannelOutboundHandlerAdapter createOutgoingErrorHandler() {
return new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
logger.info("howdy! (never gets this far)");
final ChannelFutureListener channelFutureListener = future -> {
if (!future.isSuccess()) {
future.cause().printStackTrace();
// ctx.writeAndFlush(serverErrorJSON("an error!"));
future.channel().writeAndFlush(serverErrorJSON("an error!"));
future.channel().close();
}
};
promise.addListener(channelFutureListener);
ctx.write(msg, promise);
}
};
问题是如果我在@Override
public void addHandlersToPipeline(final ChannelPipeline pipeline) {
pipeline.addLast(
createOutgoingErrorHandler(),
new HttpLoggerHandler(), // an error in this `write` should go "up"
authHandlerFactory.get(),
// etc
中抛出运行时异常,则永远不会调用我的错误处理程序的write
方法。
我将如何使这项工作?任何传出处理程序中的错误都应该“冒出来”#34;到附在头上的那个。
需要注意的一点是,我不想仅仅关闭频道,我想将错误信息写回客户端(从HttpLoggerHandler.write()
可以看出。)在我的洗牌试验期间处理程序的顺序(也试用this answer的东西),我已经激活了监听器,但我无法写任何东西。如果我在监听器中使用serverErrorJSON('...')
,似乎我进入一个循环,而使用ctx.write()
没有做任何事情。
答案 0 :(得分:1)
我发现了一个非常简单的解决方案,它允许入站和出站异常都可以到达与管道中最后一个ChannelHandler相同的异常处理程序。
我的管道设置如下:
//Inbound propagation
socketChannel.pipeline()
.addLast(new Decoder())
.addLast(new ExceptionHandler());
//Outbound propagation
socketChannel.pipeline()
.addFirst(new OutboundExceptionRouter())
.addFirst(new Encoder());
这是我的ExceptionHandler的内容,它记录捕获的异常:
public class ExceptionHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("Exception caught on channel", cause);
}
}
现在,OutHandler甚至可以通过ExceptionHandler处理出站异常:
public class OutboundExceptionRouter extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
promise.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
super.write(ctx, msg, promise);
}
}
这是在我的管道中调用的第一个出站处理程序,它的工作是在出站写承诺中添加一个侦听器,当承诺失败时,将执行future.channel().pipeline().fireExceptionCaught(future.cause());
。 fireExceptionCaught
方法沿入站方向通过管道传播异常,最终到达ExceptionHandler。
如果有人感兴趣,从Netty 4.1开始,我们需要添加一个侦听器以获取异常的原因是,对通道执行writeAndFlush之后,在AbstractChannelHandlerContext.java中调用invokeWrite0 method将写操作包装在try catch块中。 catch块通知Promise,而不是为入站消息调用fireExceptionCaught
like the invokeChannelRead method does。
答案 1 :(得分:0)
基本上你所做的是正确的...唯一不正确的是处理程序的顺序。您的ChannelOutboundHandlerAdapter
桅杆被放置&#34;作为最后一个出站处理器&#34;在管线中。这意味着它应该是这样的:
pipeline.addLast(
new HttpLoggerHandler(),
createOutgoingErrorHandler(),
authHandlerFactory.get());
这样做的原因是,当入站事件从头部流向尾部时,从尾部到管道头部的出站事件。
答案 2 :(得分:0)
似乎没有一个笼统的概念,即笼统的异常处理程序对于将在任何地方捕获错误的传出处理程序都适用。这意味着,除非您注册了侦听器以捕获某个错误,否则运行时错误很可能会导致该错误被“吞噬”,从而使您无所适从。
也就是说,也许总是有给定的错误执行的处理程序/侦听器没有意义(因为它必须非常通用),但是它确实使日志记录错误比需要的要复杂。 / p>
写完a bunch of learning tests(我建议您检查一下!)后,我得到了以下见解,这些见解基本上是我的JUnit测试的名称(经过一些正则表达式操作之后):
以问题的示例为例,这种洞察力意味着,如果错误应该在尾部和 authHandler
附近发生,那么错误处理程序将在头部附近永远不会被调用,因为它会提供新的承诺,因为ctx.write(msg)
本质上是ctx.channel.write(msg, newPromise())
。
在这种情况下,我们最终通过在所有业务逻辑处理程序之间注入相同的可共享错误处理来解决了这种情况。
处理程序如下
@ChannelHandler.Sharable
class OutboundErrorHandler extends ChannelOutboundHandlerAdapter {
private final static Logger logger = LoggerFactory.getLogger(OutboundErrorHandler.class);
private Throwable handledCause = null;
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
ctx.write(msg, promise).addListener(writeResult -> handleWriteResult(ctx, writeResult));
}
private void handleWriteResult(ChannelHandlerContext ctx, Future<?> writeResult) {
if (!writeResult.isSuccess()) {
final Throwable cause = writeResult.cause();
if (cause instanceof ClosedChannelException) {
// no reason to close an already closed channel - just ignore
return;
}
// Since this handler is shared and added multiple times
// we need to avoid spamming the logs N number of times for the same error
if (handledCause == cause) return;
handledCause = cause;
logger.error("Uncaught exception on write!", cause);
// By checking on channel writability and closing the channel after writing the error message,
// only the first listener will signal the error to the client
final Channel channel = ctx.channel();
if (channel.isWritable()) {
ctx.writeAndFlush(serverErrorJSON(cause.getMessage()), channel.newPromise());
ctx.close();
}
}
}
}
然后在我们的管道设置中,我们有了这个
// Prepend the error handler to every entry in the pipeline.
// The intention behind this is to have a catch-all
// outbound error handler and thereby avoiding the need to attach a
// listener to every ctx.write(...).
final OutboundErrorHandler outboundErrorHandler = new OutboundErrorHandler();
for (Map.Entry<String, ChannelHandler> entry : pipeline) {
pipeline.addBefore(entry.getKey(), entry.getKey() + "#OutboundErrorHandler", outboundErrorHandler);
}