二刷Netty源码的时候又有了新的认识,特此记录。
@Sharable是什么
在我们按照Netty变成范式的编写过程中,会编写一个Handler的配置类,也就是继承ChannelInitializer抽象类,并重写initChannel方法,大致如下:
package com.leolee.netty.secondExample;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
/**
* @ClassName MySocketServerInitializer
* @Description: 一旦客户端和服务端建立联系之后initChannel就会被调用
* @Author LeoLee
* @Date 2020/8/23
* @Version V1.0
**/
public class MySocketServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//声明管道
ChannelPipeline pipeline = ch.pipeline();
//绑定自带的解码器,就是对二进制数据的解析工具,至于解码器构造方法的参数之后详细分析
pipeline.addLast("lengthFieldBasedFrameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//编码器
pipeline.addLast("lengthFieldPrepender", new LengthFieldPrepender(4));
//由于涉及到服务端和客户端的字符串数据,需要绑定字符串的编解码
pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));
//自定义处理器
pipeline.addLast("mySocketServerHandler", new MySocketServerHandler());
}
}
其实就是添加一些业务需要的Handler到Channel对应的Pipeline当中,参与整个I/O请求的Inbound和Outbound。
这个ChannelInitializer抽象类带有一个@Sharable注解,其Java Doc如下:
io.netty.channel @Inherited
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public static interface ChannelHandler.Sharable
extends annotation.Annotation
Indicates that the same instance of the annotated ChannelHandler can be added to one or more ChannelPipelines multiple times without a race condition.
If this annotation is not specified, you have to create a new handler instance every time you add it to a pipeline because it has unshared state such as member variables.
This annotation is provided for documentation purpose, just like the JCIP annotations .
Gradle: io.netty:netty-all:4.1.51.Fina
带有@Sharable的ChannelHandler的同一个实例可以被添加到ChannelPipeline多次,没有带有@Sharable的ChannelHandler每次被添加到ChannelPipline的时候,每次都是新创建的一个实例。
这句话很好理解,那么是怎么实现的呢?从addLast方法开始!
ChannelPipeline接口的唯一实现类——DefaultChannelPipeline
对addLast的实现如下:
public final ChannelPipeline addLast(ChannelHandler handler) {
return addLast(null, handler);
}
@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
ObjectUtil.checkNotNull(handlers, "handlers");
for (ChannelHandler h: handlers) {
if (h == null) {
break;
}
addLast(executor, null, h);
}
return this;
}
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
addLast0(newCtx);
// If the registered is false it means that the channel was not registered on an eventLoop yet.
// In this case we add the context to the pipeline and add a task that will call
// ChannelHandler.handlerAdded(...) once the channel is registered.
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
callHandlerAddedInEventLoop(newCtx, executor);
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
private static void checkMultiplicity(ChannelHandler handler) {
if (handler instanceof ChannelHandlerAdapter) {
ChannelHandlerAdapter h = (ChannelHandlerAdapter) handler;
if (!h.isSharable() && h.added) {
throw new ChannelPipelineException(
h.getClass().getName() +
" is not a @Sharable handler, so can't be added or removed multiple times.");
}
h.added = true;
}
}
checkMultiplicity(ChannelHandler handler)先检查了出入的handler是否是ChannelHandlerAdapter的实例,这个没好说的。
重点在于isSharable()方法,该方法返回一个handler的是否是共享的状态:首先在当前Netty thread-local缓存中找传入handler的缓存信息,如果可以找到就从缓存信息中获取该handle共享状态,如果缓存中找不到,就使用传入handler的class对象来判断是否添加了@Sharable注解,把结果添加入thread-local缓存后返回结果。
/**
* Return {@code true} if the implementation is {@link Sharable} and so can be added
* to different {@link ChannelPipeline}s.
*/
public boolean isSharable() {
/**
* Cache the result of {@link Sharable} annotation detection to workaround a condition. We use a
* {@link ThreadLocal} and {@link WeakHashMap} to eliminate the volatile write/reads. Using different
* {@link WeakHashMap} instances per {@link Thread} is good enough for us and the number of
* {@link Thread}s are quite limited anyway.
*
* See <a href="https://github.com/netty/netty/issues/2289">#2289</a>.
*/
Class<?> clazz = getClass();
Map<Class<?>, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache();
Boolean sharable = cache.get(clazz);
if (sharable == null) {
sharable = clazz.isAnnotationPresent(Sharable.class);
cache.put(clazz, sharable);
}
return sharable;
}
回到addList方法,将isSharable方法的结果和handler的added做 && 运算(每个handler在初始化的时候added为false):
- 如果是一个非共享的handler,且已经被添加到pipline过,就抛出ChannelPipelineException
我们都知道ChannelPipeline类似于一个容器,里面有很多ChannelHandler,每个ChannelHandler其实是靠ChannelContext持有并通过链表维护在ChannelPipeline中的,所以handler在被添加到pipline中的时候会创建context对象,并且设置当前handler为该context的一个内部属性,实际被添加到pipeline中的就是该context。
官方关于ChannelHandlerContext的Java Doc中有一个很直观的解释:
* // Different context objects are given to "f1", "f2", "f3", and "f4" even if
* // they refer to the same handler instance. Because the FactorialHandler
* // stores its state in a context object (using an {@link AttributeKey}), the factorial is
* // calculated correctly 4 times once the two pipelines (p1 and p2) are active.
* FactorialHandler fh = new FactorialHandler();
*
* {@link ChannelPipeline} p1 = {@link Channels}.pipeline();
* p1.addLast("f1", fh);
* p1.addLast("f2", fh);
*
* {@link ChannelPipeline} p2 = {@link Channels}.pipeline();
* p2.addLast("f3", fh);
* p2.addLast("f4", fh);
结论
- 当一个共享的handler的同一个实例被多次添加到pipeline的时候,pipeline中会有多个与之对应的context,该handler会在整个pipeline中执行多次
- 当一个非共享的handler的同一个实例被多次添加到pipeline的时候,会抛出ChannelPipelineException