Netty源码分析——@Sharable以及handler被重复添加到pipline的分析

二刷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

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值