• tomcat 组件研究二--请求过程


    上一篇博客大概总结了tomcat 的组件以及其组织方式,对于tomcat 的启动过程也进行进行了简单的总结,下面这篇博客,继续研究tomcat 处理请求的相关组件,其实就是主要研究Connectors 以及Container 的工作过程,以此加深对tomcat 工作过程的理解,不足之处,请各路大神指正哈!

    下面是这篇博客的主要内容:

     1、Connectors的基本组件以及作用

     2、Connectors 的工作机制

     3、Container 的基本组件以及作用

     4、tomcat 的管道以及以及责任链模型介绍

     5、最后的感悟

    一、Connectors 的初步认识

      在上一篇博客中,我有说到,Connectors 是负责专门处理外部请求,并请求封装为对应的request 对象,然后给Container 容器进行处理的,可以理解为它利用java 实现 了web中的http 协议。

      1、Connects的基本组件

      下面先粗略介绍下Connectors 下的组件各个组件的作用以及他们之间的关系。首先看下下面这张个人画的丑图,这张图大体说明了Connector 处理请求的过程:

       在Connectors 处理请求的过程,其主要利用了一个叫做protocolhandler 的组件,不同的连接类型有不同的protocolHandler (例如,普通socket 就是Http11Protocal,NioSocket 就是Http11NioProtocal),该组件处理过程又主要涉及到以下组件:endpoint 、process以及adaptor 三个组件,他们工作的过程大概如下:当客户端发起请求的时候,endpoint 通过底层的Socket 机制进行端口监听,它负责监听客户端的请求,处理对应请求的socket 对象,并把Socekt 对象传给processor 对象;当processor 对象接收到socket 对象的时候,会利用把请求封装为request 对象(HttpRequest),当封装好request 对象之后,processor 吧request 对象传给一个适配器,该适配器负责连接Container 和adaptor 对象,最后,Container 得到的是一个已经封装好的request 对象。

      总的来说,可以这样理解,endpoint 利用socket 处理了tcp 层面的协议,而processor 则在java 层面处理了http 协议,最后,adaptor 将Connectors 和 Container 连接起来,实现请求转发。下面,就针对protocolhandler 的这三个组件,看看,protocolhandler 到底是如何进行请求处理的。

      

      2、Connectors 中处理tcp 协议的组件--endpoint 

      其实在endpoint 内部,它又是主要靠一下组件进行请求处理的,具体的话可以参考下面这张图:(不过在正式总结之前,读者注意了,其实如果你纯粹的看我这篇博客来了解endpoint 工作过程的话,会感觉好头大的,推荐自己下一份源码,自己点开源码看看才能正真理解的,因为不同组件之间都相互调用,方法之间调用的关系也是挺复杂的(反正我一开始是头都大了的),对着源码才更好理解,废话不多说,上图)

      首先,我们看Acceptor,从名字其实我们可以看出来,它是一个接受器,就是专门负责接收请求,然后开启Socket ,其实,Acceptor是 AbstractEndpoint的内部类,具体的实现又是由子类NioEndpoint 实现的,下面看看AbstractEndpoint的源码和NioEndpoint 源码,看卡Acceptor是如何实现请求监听的:

      这个是AbstractEndpoint的内部类,这个类其实不难理解:它是一个继承了Runnable 接口的抽象类,但是并没有实现run方法,另外定义了一些比较常规的方法例如获取运行状态等,具体不说

    public abstract static class Acceptor implements Runnable {
            public enum AcceptorState {
                NEW, RUNNING, PAUSED, ENDED
            }
    
            protected volatile AcceptorState state = AcceptorState.NEW;
            public final AcceptorState getState() {
                return state;
            }
    
            private String threadName;
            protected final void setThreadName(final String threadName) {
                this.threadName = threadName;
            }
            protected final String getThreadName() {
                return threadName;
            }
        }
    View Code

      下面,主要看看AbstractEndpoint的子类是怎样工作的,下面简单粘上AbstractEndpoint的内部类Acceptor源码,它继承了AbstractEndpoint的Acceptor这个内部类:

    protected class Acceptor extends AbstractEndpoint.Acceptor {
    
            @Override
            public void run() {
    
                int errorDelay = 0;
    
                // Loop until we receive a shutdown command
                while (running) {
    
                    // Loop if endpoint is paused
                    while (paused && running) {
                        state = AcceptorState.PAUSED;
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }
    
                    if (!running) {
                        break;
                    }
                    state = AcceptorState.RUNNING;
    
                    try {
                        //if we have reached max connections, wait
                        countUpOrAwaitConnection();
    
                        SocketChannel socket = null;
                        try {
                            // Accept the next incoming connection from the server
                            // socket
                            socket = serverSock.accept();
                        } catch (IOException ioe) {
                            //we didn't get a socket
                            countDownConnection();
                            // Introduce delay if necessary
                            errorDelay = handleExceptionWithDelay(errorDelay);
                            // re-throw
                            throw ioe;
                        }
                        // Successful accept, reset the error delay
                        errorDelay = 0;
    
                        // setSocketOptions() will add channel to the poller
                        // if successful
                        if (running && !paused) {
                            if (!setSocketOptions(socket)) {
                                countDownConnection();
                                closeSocket(socket);
                            }
                        } else {
                            countDownConnection();
                            closeSocket(socket);
                        }
                    } catch (SocketTimeoutException sx) {
                        // Ignore: Normal condition
                    } catch (IOException x) {
                        if (running) {
                            log.error(sm.getString("endpoint.accept.fail"), x);
                        }
                    } catch (OutOfMemoryError oom) {
                        try {
                            oomParachuteData = null;
                            releaseCaches();
                            log.error("", oom);
                        }catch ( Throwable oomt ) {
                            try {
                                try {
                                    System.err.println(oomParachuteMsg);
                                    oomt.printStackTrace();
                                }catch (Throwable letsHopeWeDontGetHere){
                                    ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                                }
                            }catch (Throwable letsHopeWeDontGetHere){
                                ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                            }
                        }
                    } catch (Throwable t) {
                        ExceptionUtils.handleThrowable(t);
                        log.error(sm.getString("endpoint.accept.fail"), t);
                    }
                }
                state = AcceptorState.ENDED;
            }
        }
    View Code

      其实这个类,主要就是实现了Runnable 的run方法而已,其实它的核心代码主要就是下面这两句(其他例如判断运行状态是否是在运行以及关闭Socket 等管理工作等等代码略过了,这部分就请读者自己看看源码啦):

    socket = serverSock.accept();
    // setSocketOptions() will add channel to the poller
    setSocketOptions(socket);

       上面两句代码:首先监听端口,获取socket ,然后调用setSocketOptions把socket 对应的channel 传递给poller,这样,就完成了请求的监听,超简单有木有。OK,现在请求到了poller了,下面再看看源码看看poller 又是怎么工作的;

      Poller 也实现了Runnable 接口,但是由于Poller 的方法太多,下面就粘上主要Poller代码中run方法相关的代码就好了,先看看Poller 这个线程类启动之后干什么:

    public void run() {
                // Loop until destroy() is called
                while (true) {
                    try {
                        // Loop if endpoint is paused
                        while (paused && (!close) ) {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                // Ignore
                            }
                        }
    
                        boolean hasEvents = false;
    
                        // Time to terminate?
                        if (close) {
                            events();
                            timeout(0, false);
                            try {
                                selector.close();
                            } catch (IOException ioe) {
                                log.error(sm.getString(
                                        "endpoint.nio.selectorCloseFail"), ioe);
                            }
                            break;
                        } else {
                            hasEvents = events();
                        }
                        try {
                            if ( !close ) {
                                if (wakeupCounter.getAndSet(-1) > 0) {
                                    //if we are here, means we have other stuff to do
                                    //do a non blocking select
                                    keyCount = selector.selectNow();
                                } else {
                                    keyCount = selector.select(selectorTimeout);
                                }
                                wakeupCounter.set(0);
                            }
                            if (close) {
                                events();
                                timeout(0, false);
                                try {
                                    selector.close();
                                } catch (IOException ioe) {
                                    log.error(sm.getString(
                                            "endpoint.nio.selectorCloseFail"), ioe);
                                }
                                break;
                            }
                        } catch ( NullPointerException x ) {
                            //sun bug 5076772 on windows JDK 1.5
                            if ( log.isDebugEnabled() ) log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5",x);
                            if ( wakeupCounter == null || selector == null ) throw x;
                            continue;
                        } catch ( CancelledKeyException x ) {
                            //sun bug 5076772 on windows JDK 1.5
                            if ( log.isDebugEnabled() ) log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5",x);
                            if ( wakeupCounter == null || selector == null ) throw x;
                            continue;
                        } catch (Throwable x) {
                            ExceptionUtils.handleThrowable(x);
                            log.error("",x);
                            continue;
                        }
                        //either we timed out or we woke up, process events first
                        if ( keyCount == 0 ) hasEvents = (hasEvents | events());
    
                        Iterator<SelectionKey> iterator =
                            keyCount > 0 ? selector.selectedKeys().iterator() : null;
                        // Walk through the collection of ready keys and dispatch
                        // any active event.
                        while (iterator != null && iterator.hasNext()) {
                            SelectionKey sk = iterator.next();
                            KeyAttachment attachment = (KeyAttachment)sk.attachment();
                            // Attachment may be null if another thread has called
                            // cancelledKey()
                            if (attachment == null) {
                                iterator.remove();
                            } else {
                                attachment.access();
                                iterator.remove();
                                processKey(sk, attachment);
                            }
                        }//while
    
                        //process timeouts
                        timeout(keyCount,hasEvents);
                        if ( oomParachute > 0 && oomParachuteData == null ) checkParachute();
                    } catch (OutOfMemoryError oom) {
                        try {
                            oomParachuteData = null;
                            releaseCaches();
                            log.error("", oom);
                        }catch ( Throwable oomt ) {
                            try {
                                System.err.println(oomParachuteMsg);
                                oomt.printStackTrace();
                            }catch (Throwable letsHopeWeDontGetHere){
                                ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                            }
                        }
                    }
                }//while
    
                stopLatch.countDown();
            }
    View Code

      run方法大概就是干了下面事情:从selector中,选择一个key,该key代表这一个已经准备好的Channel 的输入输出流(听说说叫管道更合适?),这里的Nio具体工作机制不展开了(汗颜,其实我对这部分也不太了解,后面必须研究下),然后,下面就是核心代码了:

    processKey(sk, attachment);

      这个方法就是,调用processKey方法把获取到的请求对应的channel ,传递给process 这个方法,然后,查看这个方法的代码,我们可以发现,它会将channel 以及socket 传递给下一个对象:SocketProcessor,下面看看这个方法的源码吧:

     protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {
                boolean result = true;
                try {
                    if ( close ) {
                        cancelledKey(sk, SocketStatus.STOP, attachment.comet);
                    } else if ( sk.isValid() && attachment != null ) {
                        attachment.access();//make sure we don't time out valid sockets
                        sk.attach(attachment);//cant remember why this is here
                        NioChannel channel = attachment.getChannel();
                        if (sk.isReadable() || sk.isWritable() ) {
                            if ( attachment.getSendfileData() != null ) {
                                processSendfile(sk,attachment, false);
                            } else {
                                if ( isWorkerAvailable() ) {
                                    unreg(sk, attachment, sk.readyOps());
                                    boolean closeSocket = false;
                                    // Read goes before write
                                    if (sk.isReadable()) {
                                        if (!processSocket(channel, SocketStatus.OPEN_READ, true)) {
                                            closeSocket = true;
                                        }
                                    }
                                    if (!closeSocket && sk.isWritable()) {
                                        if (!processSocket(channel, SocketStatus.OPEN_WRITE, true)) {
                                            closeSocket = true;
                                        }
                                    }
                                    if (closeSocket) {
                                        cancelledKey(sk,SocketStatus.DISCONNECT,false);
                                    }
                                } else {
                                    result = false;
                                }
                            }
                        }
                    } else {
                        //invalid key
                        cancelledKey(sk, SocketStatus.ERROR,false);
                    }
                } catch ( CancelledKeyException ckx ) {
                    cancelledKey(sk, SocketStatus.ERROR,false);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error("",t);
                }
                return result;
            }
    View Code

      好吧,其实这个这么长的代码,我们只要看下面这行代码就行了(当然,中间很多编码技巧的确值得我们仔细琢磨),下面代码主要是调用一个processSocket的方法:

    processSocket(channel, SocketStatus.OPEN_READ, true)

      下面我们看看processSocket又是干嘛的,这个是方法源码:

    public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
            try {
                KeyAttachment attachment = (KeyAttachment)socket.getAttachment();
                if (attachment == null) {
                    return false;
                }
                attachment.setCometNotify(false); //will get reset upon next reg
                SocketProcessor sc = processorCache.poll();
                if ( sc == null ) sc = new SocketProcessor(socket,status);
                else sc.reset(socket,status);
                if ( dispatch && getExecutor()!=null ) getExecutor().execute(sc);
                else sc.run();
            } catch (RejectedExecutionException rx) {
                log.warn("Socket processing request was rejected for:"+socket,rx);
                return false;
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                // This means we got an OOM or similar creating a thread, or that
                // the pool and its queue are full
                log.error(sm.getString("endpoint.process.fail"), t);
                return false;
            }
            return true;
        }
    View Code

      核心代码看下面:

     attachment.setCometNotify(false); //will get reset upon next reg
     SocketProcessor sc = processorCache.poll();
    if ( sc == null ) sc = new SocketProcessor(socket,status);
     else sc.reset(socket,status);
     if ( dispatch && getExecutor()!=null ) getExecutor().execute(sc);
    else sc.run();
      

      这个方法可以看到,processSocket方法把Socket  相关的类传递给了SocketProcessor ,然后利用并发框架Executor进行管理SocketProcessor ,继续往下处理请求,此时,请求已经到达了SocketProcessor 这个类。

      经历重重困难,请求终于到了SocketProcessor 这个类了,这个类是也实现了Runnable 接口,下面再研究下这个类的源码,主要就是研究两个方法,一个是run方法,一个是doRun方法:

    public void run() {
                SelectionKey key = socket.getIOChannel().keyFor(
                        socket.getPoller().getSelector());
                KeyAttachment ka = null;
    
                if (key != null) {
                    ka = (KeyAttachment)key.attachment();
                }
    
                // Upgraded connections need to allow multiple threads to access the
                // connection at the same time to enable blocking IO to be used when
                // NIO has been configured
                if (ka != null && ka.isUpgraded() &&
                        SocketStatus.OPEN_WRITE == status) {
                    synchronized (ka.getWriteThreadLock()) {
                        doRun(key, ka);
                    }
                } else {
                    synchronized (socket) {
                        doRun(key, ka);
                    }
                }
            }
    
            private void doRun(SelectionKey key, KeyAttachment ka) {
                try {
                    int handshake = -1;
    
                    try {
                        if (key != null) {
                            // For STOP there is no point trying to handshake as the
                            // Poller has been stopped.
                            if (socket.isHandshakeComplete() ||
                                    status == SocketStatus.STOP) {
                                handshake = 0;
                            } else {
                                handshake = socket.handshake(
                                        key.isReadable(), key.isWritable());
                                // The handshake process reads/writes from/to the
                                // socket. status may therefore be OPEN_WRITE once
                                // the handshake completes. However, the handshake
                                // happens when the socket is opened so the status
                                // must always be OPEN_READ after it completes. It
                                // is OK to always set this as it is only used if
                                // the handshake completes.
                                status = SocketStatus.OPEN_READ;
                            }
                        }
                    }catch ( IOException x ) {
                        handshake = -1;
                        if ( log.isDebugEnabled() ) log.debug("Error during SSL handshake",x);
                    }catch ( CancelledKeyException ckx ) {
                        handshake = -1;
                    }
                    if ( handshake == 0 ) {
                        SocketState state = SocketState.OPEN;
                        // Process the request from this socket
                        if (status == null) {
                            state = handler.process(ka, SocketStatus.OPEN_READ);
                        } else {
                            state = handler.process(ka, status);
                        }
                        if (state == SocketState.CLOSED) {
                            // Close socket and pool
                            close(ka, socket, key, SocketStatus.ERROR);
                        }
                    } else if (handshake == -1 ) {
                        close(ka, socket, key, SocketStatus.DISCONNECT);
                    } else {
                        ka.getPoller().add(socket, handshake);
                    }
                } catch (CancelledKeyException cx) {
                    socket.getPoller().cancelledKey(key, null, false);
                } catch (OutOfMemoryError oom) {
                    try {
                        oomParachuteData = null;
                        log.error("", oom);
                        if (socket != null) {
                            socket.getPoller().cancelledKey(key,SocketStatus.ERROR, false);
                        }
                        releaseCaches();
                    }catch ( Throwable oomt ) {
                        try {
                            System.err.println(oomParachuteMsg);
                            oomt.printStackTrace();
                        }catch (Throwable letsHopeWeDontGetHere){
                            ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                        }
                    }
                } catch (VirtualMachineError vme) {
                    ExceptionUtils.handleThrowable(vme);
                }catch ( Throwable t ) {
                    log.error("",t);
                    if (socket != null) {
                        socket.getPoller().cancelledKey(key,SocketStatus.ERROR,false);
                    }
                } finally {
                    socket = null;
                    status = null;
                    //return to cache
                    if (running && !paused) {
                        processorCache.offer(this);
                    }
                }
            }
    View Code

      核心代码就这句:

    state = handler.process(ka, SocketStatus.OPEN_READ);

      恩,就是那么简单,吧socket 相关的类传给一个Handler 类进行处理。

      总的来说,endpoint 的工作过程分为这几个部分:首先是Acceptor进行请求的监听,当监听到请求的时候,会将代表该次请求的socket 转发给poller 进行处理,poller 主要负责Socket中 流(Nio中的通道)的处理,当然,期间还有涉及到很多其他的额外工作,这里不详细展开;最后,SocketProcess会吧poller的处理结果,以队里的形式传递给Handler(不知道这里理解是否有偏差,欢迎指正),最终吧处理好的请求发送到Processor进行下一步处理,封装为request 对象。

      3、Processor 和Adaptor 

      Processor 对象主要实现了tcp到http层面的数据转换,它主要是吧socket输入输出流封装为request、response 对应对象,可以先看看该接口的源码:

    public interface Processor<S> {
        Executor getExecutor();
    
        SocketState process(SocketWrapper<S> socketWrapper) throws IOException;
    
        SocketState event(SocketStatus status) throws IOException;
    
        SocketState asyncDispatch(SocketStatus status);
        SocketState asyncPostProcess();
    
        /**
         * @deprecated  Will be removed in Tomcat 8.0.x.
         */
        @Deprecated
        org.apache.coyote.http11.upgrade.UpgradeInbound getUpgradeInbound();
        /**
         * @deprecated  Will be removed in Tomcat 8.0.x.
         */
        @Deprecated
        SocketState upgradeDispatch() throws IOException;
    
        HttpUpgradeHandler getHttpUpgradeHandler();
        SocketState upgradeDispatch(SocketStatus status) throws IOException;
        
        void errorDispatch();
    
        boolean isComet();
        boolean isAsync();
        boolean isUpgrade();
    
        Request getRequest();
    
        void recycle(boolean socketClosing);
    
        void setSslSupport(SSLSupport sslSupport);
    }
    View Code

      从代码中我们可以知道:这个接口主要定义了一些错误处理方法errorDispatch、异步处理的方法等等,当然,其实实现tcp到http 转换的过程以及封装request 对象的过程主要是由process这个方法 完成的,不过本人看了下源码实在太复杂了,在这里详细展开的估计要搞好久(囧,原谅我,其实我也看不懂process这个方法内部实现,感觉实在有点复杂)

      当Processor处理完后,会得到对应的request 对象,也就是我们熟悉的HttpRequest对象了,这个时候,request 对象便会由一个适配器传递给Container 容器,该容器会进一步处理request 对象(感觉到这里这篇博客写崩了,本来是想好好看看Processor怎么处理tcp请求的,无奈看到源码头都大了,感觉功力不够就没敢看了,尴尬)

      好吧,下面继续硬着头皮,研究下Container 的工作过程。

    二、Container 处理请求的过程

      1、Container 的组件

      Container 是一个接口,其实它的下面有这是个字接口:Engine,Host,Context,Wrapper 。下面网上盗的继承关系图:

       有点乱是吧,我也觉得。下面简单解释下各个接口(类)的意义:Engine,是一个service 下的管理站点的一个引擎,Host 就是主机,是的,我也是才知道,原来一个tomcat 还可以管理不同主机的,当然,这里的主机是虚拟的,并不是一个tomcat 可以管理不同服务器,不同host 仅仅代表不同站点而已,Engine 和host 的关系大概可以理解为:Engine 管理着不同的host ,一个service 可以对应多个host 却仅仅有一个Engine;而Context 代表的是一个引用程序,狭隘地理解,其实就是一个可运行的web项目,反正一个项目中的web.xml就可以理解为对应一个context 对象;最后就是Wrapper了,这个是一个处理Servlet 的类,其实就是相当于在Servlet 包一层东西的类,不同Servlet 对应不同的Wrapper 类,Wrappper专门用来处理Servlet。Ok,他们之间的关系如果你还是觉得有点模糊的话,可以看下图:

      好吧,图是丑了点,不过估计都能看懂吧。下面详细说说请求又是怎么真正从最初的Engine 入口处到最后的Servlet ,这里需要引出一个概念:tomcat 中的管道。

      2、tomcat 中的pipeline - value

      相信,用过tomcat 的对过滤器应该不陌生?我们只要实现Filter 对应的方法,然后再server.xml中配置filter 即可让filter 起作用,其实这个过程就是利用到了管道。在tomcat 中的管道,利用责任链模式进行实现的,具体的过程是这样的:Engine,host ,context 以及Wrapper 都是一个管道,在每个管道中,会存放不同的Value ,这些Value 代表的是各个类对请求的处理,就像一个车间中的生产线一样,整条生产线可以理解为一个管道,生产线上每个工人就是一value ,每个工人加工完产品之后,会把加工之后的产品交给下一个工人,直到所有工人都加完工过产品。不过,在tomcat 的管道中,有一个baseValue ,这个Value 是肯定会执行的,而且是在所有value 执行完再执行,这就类似于工厂中质检的机器,最后一定会检查产品的质量。value 对应的是某个处理请求的类,而BaseValue在四个组件中对应的分别是下面这几个类:StandardEngineValue、StandardHostValue、StandardContextValue、StandardWrapperValue,也就是说,这四个类对请求的处理是一定会被管道锁执行的。如果还是无法理解,可以看下面的示意图理解管道:

      下面我们就以一个StandardEngin以及它对应的value StandardEngineValue研究下,什么是value ,管道又是如何进行工作的,首先,我们看看StandardEngineValue的源码(主要是看其中的invoke 方法,该方法被调用时,对应的StandardEngine的invoke 方法将会被执行 ):

    public final void invoke(Request request, Response response)
            throws IOException, ServletException {
    
            // Select the Host to be used for this Request
            Host host = request.getHost();
            if (host == null) {
                response.sendError
                    (HttpServletResponse.SC_BAD_REQUEST,
                     sm.getString("standardEngine.noHost", 
                                  request.getServerName()));
                return;
            }
            if (request.isAsyncSupported()) {
                request.setAsyncSupported(host.getPipeline().isAsyncSupported());
            }
    
            // Ask this Host to process this request
            host.getPipeline().getFirst().invoke(request, response);
    
        }
    View Code

      可以看到,作为Engine管道中最后执行的Value ,StandardEngineValue的invoke 会获取Host管道中第一个HostValue,然后调用对应的invoke ,当第一个HostValue的invoke 方法执行完的时候,会继续获取Host管道中下一个HostValue,然后一直这个过程,直到所有Value 执行完,到Host的StandardHostValue的时候,StandardHostValue的invoke方法会调用context 通道的第一个Value,进行执行,context通道重复这个过程,直到请求到达Wrapper 通道中时,Wrapper 会调用FilterChain ,我们的Filter 便会在其中被执行了。总而言之,这个过程,BaseValue的作用便是调用下一个组件的通道,让整个责任链可以继续执行下去。

      

    三、感悟

      在写这篇博客的时候,其实感觉蛮痛苦的,毕竟很多源码的确看不懂,很多都只是只能了解个大概,好像前面的关于Connectors 的总结,自己很多都是走马观花地进行大体的梳理,对于组件工作的具体原理还是不了解,而且,其实很多代码是可以看懂是什么意思,但是却很难懂:为什么编码的人要这样做。确实,自己的功力还是不够,无论是线程并发、Socket 等基础,还是整体的框架思维层面,都有待提高。开源框架的魅力,其实不仅仅是让你有一个很好的工具可以用,更重要的是,可以让你知道自己和真正大牛的差距在哪,让你的思维方式不断靠近他们,让你有非常好的途径,去学习与模仿,最后转化为自己的东西。

      废话不多说,个人感觉这篇博客并写得有点水,归根到底还是很多核心的思想没有正真领悟,不过这篇博客也花了我挺长时间整理的,所以还是硬着头皮发出来吧,不足地方欢迎各位大神指正!

      秋招干巴爹!

      

      

      

      

  • 相关阅读:
    调整精力记录
    cucumber系列(四) RubyGems下载源更新的问题
    cucumber系列(三)BDD与相关测试框架资料收集
    cucumber系列(二) cucumber的基本操作命令
    cucumber系列(一) 如何让cucumber识别中文
    centos与ubuntu的区别 (转)
    Excel 导出组件,10W级数据5秒内导出
    如何本地调试测试环境的代码
    .net core 在Startup.cs 的Configure方法中扩展 IApplicationBuilder
    mysql 统计一周每天得数据
  • 原文地址:https://www.cnblogs.com/lcplcpjava/p/7422296.html
Copyright © 2020-2023  润新知