• Tomcat 源码解析一容器加载-大寂灭指(下)


    接着 Tomcat 源码解析一容器加载-大寂灭指(中) 继续 。

      之前有一个方法configureContext()有一行代码(下面加粗代码)没有分析完,这里接着继续分析 。

    public void configureContext(Context context) {
        ...
        // 遍历所有的servlet,创建wrapper
        for (ServletDef servlet : servlets.values()) {
            Wrapper wrapper = context.createWrapper();
            // Description is ignored
            // Display name is ignored
            // Icons are ignored
    
            // jsp-file gets passed to the JSP Servlet as an init-param
    
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
            wrapper.setName(servlet.getServletName());
            Map params = servlet.getParameterMap();
            for (Entry entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
            wrapper.setServletClass(servlet.getServletClass());
            MultipartDef multipartdef = servlet.getMultipartDef();
            if (multipartdef != null) {
                if (multipartdef.getMaxFileSize() != null &&
                        multipartdef.getMaxRequestSize()!= null &&
                        multipartdef.getFileSizeThreshold() != null) {
                    wrapper.setMultipartConfigElement(new MultipartConfigElement(
                            multipartdef.getLocation(),
                            Long.parseLong(multipartdef.getMaxFileSize()),
                            Long.parseLong(multipartdef.getMaxRequestSize()),
                            Integer.parseInt(
                                    multipartdef.getFileSizeThreshold())));
                } else {
                    wrapper.setMultipartConfigElement(new MultipartConfigElement(
                            multipartdef.getLocation()));
                }
            }
            if (servlet.getAsyncSupported() != null) {
                wrapper.setAsyncSupported(
                        servlet.getAsyncSupported().booleanValue());
            }
            wrapper.setOverridable(servlet.isOverridable());
            // 把wrapper添加到context中去
            context.addChild(wrapper);
        }
        
    	// 把mapping关系添加到context中去
    	for (Entry entry : servletMappings.entrySet()) {
    	    context.addServletMapping(entry.getKey(), entry.getValue());
    	}
    
    }
    

      看StandardContext的addChild()具体实现。

    public void addChild(Container child) {
    
        Wrapper oldJspServlet = null;
    
    	// StandardContext下的child只能是Wrapper ,否则抛出异常 
        if (!(child instanceof Wrapper)) {
            throw new IllegalArgumentException
                (sm.getString("standardContext.notWrapper"));
        }
    
        boolean isJspServlet = "jsp".equals(child.getName());
        if (isJspServlet) {
        	// 在一个StandardContext中只允许有一个名字为jsp的child
            oldJspServlet = (Wrapper) findChild("jsp");
            if (oldJspServlet != null) {
            	// 将旧的名字为jsp的child移除掉 
                removeChild(oldJspServlet);
            }
        }
    
        super.addChild(child);
    
        if (isJspServlet && oldJspServlet != null) {
        	// 如果存在两个名字为jsp的child 
        	// 将两者的映射合并,都指向jsp 
            String[] jspMappings = oldJspServlet.findMappings();
            for (int i=0; jspMappings!=null && iaddServletMapping(jspMappings[i], child.getName());
            }
        }
    }      
    

      其他的很多代码就略过了,一般StandardWrapper也没有监听器,因此直接到StandardWrapper的startInternal()方法 。

    protected synchronized void startInternal() throws LifecycleException {
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.starting",
                                                        this.getObjectName(),
                                                        sequenceNumber++);
            broadcaster.sendNotification(notification);
        }
        super.startInternal();
        // 此servlet将变为可用的日期和时间(从纪元开始以毫秒为单位),
        // 如果servlet可用,则为零。如果该值等于Long。MAX_VALUE,此servlet的不可用性被认为是永久性的。
        setAvailable(0L);
    
        if (this.getObjectName() != null) {
            Notification notification =
                new Notification("j2ee.state.running", this.getObjectName(),
                                sequenceNumber++);
            broadcaster.sendNotification(notification);
        }
    }
    

      在StandardWrapper的startInternal()方法中也并没有多少事情,只是发送一下广播事件,设置available为可用。当然接着调用父类的super.startInternal();方法。

    protected synchronized void startInternal() throws LifecycleException {
    	...
        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle) {
            ((Lifecycle) pipeline).start();
        }
    
        // 这个时候会触发START_EVENT事件,会进行deployApps
        setState(LifecycleState.STARTING);
    
    }
    

      而父类则是ContainerBase类,StandardWrapper中当然也存在管道,因此会启动StandardPipeline,而StandardPipeline默认是没有实现监听器的,StandardPipeline同样继承了LifecycleBase, 因此不会处理StandardPipeline启动过程中生命周期监听器事件,那就直接进入StandardPipeline的startInternal()方法吧。

    protected synchronized void startInternal() throws LifecycleException {
        // Start the Valves in our pipeline (including the basic), if any
        Valve current = first;
        if (current == null) {
            current = basic;
        }
        while (current != null) {
            if (current instanceof Lifecycle)
                ((Lifecycle) current).start();
            current = current.getNext();
        }
        setState(LifecycleState.STARTING);
    }
    

      此时大家可能比较迷惑,basic是哪里初始化值的,请看 StandardWrapper的构造方法 。

    public StandardWrapper() {
    
        super();
        // 【注意】StandardWrapper 类的构造函数将一个StandardWrapperValue 作为它的基本阀门
        // 本章关注的是一个servlet 被调用的时候发生的细节,因此我们需要自习看StandardWrapper 和StandardWrapperValue 类,在学习它们
        // 之前,我们需要首先关注下,javax.servlet.SignleThreaadModel ,理解该接口对于理解一个包装器是如何工作的非常重要 。
        swValve=new StandardWrapperValve();
        pipeline.setBasic(swValve);  //
        // 广播
        broadcaster = new NotificationBroadcasterSupport();
    
    }
    

      在StandardWrapper的构造方法中,初始化basic的值为StandardWrapperValve,到这里StandardWrapper被添加到StandardContext操作已经完成了。

    添加映射

      将StandardWrapper添加到StandardContext主要是提供服务,根据前端的url请求,tomcat最终将请求转发到不同的StandardWrapper中,StandardWrapper最后又调用我们开发的Servlet,因此光将Wrapper添加到Context还不行,还需要将映射也添加到Context中,当接收到请求时,才能找到相应的Servlet 。
      在分析方法之前先来看一下servletMappings的数据。
    在这里插入图片描述

    public void addServletMapping(String pattern, String name) {
        addServletMapping(pattern, name, false);
    }
    
    
    public void addServletMapping(String pattern, String name,
                                  boolean jspWildCard) {
        // Validate the proposed mapping
        //
        if (findChild(name) == null)
            throw new IllegalArgumentException
                (sm.getString("standardContext.servletMap.name", name));
        // 如果urlPattern以 *. 或 / 开头,则返回原urlPattern
        // 如果urlPattern不为空,但不以 / 开头,则帮urlPattern加上前缀 /  
        String decodedPattern = adjustURLPattern(RequestUtil.URLDecode(pattern));
        // 1. 如果url中有\n ,\r 或为null,则验证不通过
        // 2. 以*.开头并且还有/ 的url验证不通过,如*.xxx/ 
        // 3. 以/ 开头,并且还有*. 的url 验证不通过 如,/xxx*. 
        if (!validateURLPattern(decodedPattern))
            throw new IllegalArgumentException
                (sm.getString("standardContext.servletMap.pattern", decodedPattern));
    
        // Add this mapping to our registered set
        // 如果对某个servlet重复配置了urlPattern,那么将被覆盖
        synchronized (servletMappingsLock) {
            String name2 = servletMappings.get(decodedPattern);
            if (name2 != null) {
                // Don't allow more than one servlet on the same pattern
                Wrapper wrapper = (Wrapper) findChild(name2);
                wrapper.removeMapping(decodedPattern);
                mapper.removeWrapper(decodedPattern);
            }
            servletMappings.put(decodedPattern, name);
        }
        Wrapper wrapper = (Wrapper) findChild(name);
        // 将pattern添加到StandardWrapper的mappings属性中
        wrapper.addMapping(decodedPattern);
    
        // Update context mapper
        // 在Mapper中添加映射关系
        mapper.addWrapper(decodedPattern, wrapper, jspWildCard,
                resourceOnlyServlets.contains(name));
    
        fireContainerEvent("addServletMapping", decodedPattern);
    
    }
    

      接下来看向mapper添加映射关系 。

    public void addWrapper(String path, Object wrapper, boolean jspWildCard,
            boolean resourceOnly) {
        addWrapper(context, path, wrapper, jspWildCard, resourceOnly);
    }
    
    protected void addWrapper(ContextVersion context, String path,
            Object wrapper, boolean jspWildCard, boolean resourceOnly) {
        synchronized (context) {
            if (path.endsWith("/*")) {  // 比如/test/*
                // Wildcard wrapper 通配符Wrapper
                String name = path.substring(0, path.length() - 2);
                // 这里的两个Wrapper都表示Servlet包装器,不同的是,一个是只用来记录映射关系,一个是真正的StandardWrapper
                Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                        resourceOnly);
    
                // 如果在老的wildcardWrappers中不存在相同name的,则把新的通配符wrapper插入到数组中
                Wrapper[] oldWrappers = context.wildcardWrappers;
                Wrapper[] newWrappers =
                    new Wrapper[oldWrappers.length + 1];
                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                    context.wildcardWrappers = newWrappers;
                    int slashCount = slashCount(newWrapper.name);   // 一个url被"/"切分为几个部分
                    // 记录当前Context中url按"/"切分后长度最大的长度
                    if (slashCount > context.nesting) {
                        context.nesting = slashCount;
                    }
                }
            } else if (path.startsWith("*.")) { // 比如*.jsp
                // Extension wrapper  扩展匹配
                String name = path.substring(2);
                Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                        resourceOnly);
                Wrapper[] oldWrappers = context.extensionWrappers;
                Wrapper[] newWrappers =
                    new Wrapper[oldWrappers.length + 1];
                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                    context.extensionWrappers = newWrappers;
                }
            } else if (path.equals("/")) {
                // Default wrapper
                Wrapper newWrapper = new Wrapper("", wrapper, jspWildCard,
                        resourceOnly);
                context.defaultWrapper = newWrapper;
            } else {
                // Exact wrapper   精确匹配
                final String name;
                if (path.length() == 0) {
                    // Special case for the Context Root mapping which is
                    // treated as an exact match
                    // 我们可以在web.xml中配置一个mapping关系是,url-pattern设置为空,那么就表示可以通过应用跟路径来访问
                    name = "/";
                } else {
                    name = path;
                }
                Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                        resourceOnly);
                Wrapper[] oldWrappers = context.exactWrappers;
                Wrapper[] newWrappers =
                    new Wrapper[oldWrappers.length + 1];
                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                    context.exactWrappers = newWrappers;
                }
            }
        }
    }
    
    private static final boolean insertMap
        (MapElement[] oldMap, MapElement[] newMap, MapElement newElement) {
        int pos = find(oldMap, newElement.name);    // 先在oldMap中找是否存在newElement
        if ((pos != -1) && (newElement.name.equals(oldMap[pos].name))) {
            // 如果存在则不插入了
            return false;
        }
        // 从oldMap的第0个位置开始,复制pos + 1个元素到newMap中,newMap中从第0个位置开始
        System.arraycopy(oldMap, 0, newMap, 0, pos + 1);
        newMap[pos + 1] = newElement; // 修改newMap中的pos+1位置的元素为新元素
        // 从oldMap的第pos+1个位置开始,复制oldMap剩下的元素到newMap中
        System.arraycopy
            (oldMap, pos + 1, newMap, pos + 2, oldMap.length - pos - 1);
        return true;
    }
    

      看上去addWrapper代码那么多,其实也就分三种情况,以url 以xxx/结尾,以.xxx开头,否则就是精确匹配了,关于添加映射就分析到这里了。

    TldConfig的configure_start事件

      之前只分析过ContextConfig的configure_start事件,但StandardContext还有一个监听器TldConfig,也注册了configure_start事件,接下来分析它。

    public void execute() {
        long t1=System.currentTimeMillis();
    
        /*
         * Priority order of URIs required by spec is:
         * 1. J2EE platform taglibs - Tomcat doesn't provide these
         * 2. web.xml entries
         * 3. JARS in WEB-INF/lib & TLDs under WEB-INF (equal priority)
         * 4. Additional entries from the container
         *
         * Keep processing order in sync with o.a.j.compiler.TldLocationsCache
         */
    
        // Stage 2 - web.xml entries
        tldScanWebXml();
    
        // Stage 3a - TLDs under WEB-INF (not lib or classes)
        tldScanResourcePaths("/WEB-INF/");
    
        // Stages 3b & 4
        JarScanner jarScanner = context.getJarScanner();
    
        TldJarScannerCallback tldCallBack = new TldJarScannerCallback();
        // 扫描StandardContext 项目目录下的所有WEB-INF/lib下的jar包
        jarScanner.scan(context.getServletContext(), context.getLoader().getClassLoader(),
                tldCallBack, noTldJars);
        if(tldCallBack.scanFoundNoTLDs()){
            log.info(sm.getString("tldConfig.noTldSummary"));
        }
        // Now add all the listeners we found to the listeners for this context
        String list[] = getTldListeners();
    
        if( log.isDebugEnabled() )
            log.debug(sm.getString("tldConfig.addListeners",
                    Integer.valueOf(list.length)));
    
        for( int i=0; i// 将taglib的监听器类添加到StandardContext的applicationListeners属性中
            context.addApplicationListener(
                    new ApplicationListener(list[i], true));
        }
    
        long t2=System.currentTimeMillis();
        if( context instanceof StandardContext ) {
            ((StandardContext)context).setTldScanTime(t2-t1);
        }
    
    }
    
    1. 在web.xml中声明TLD文件的路径。
    private void tldScanWebXml() {
        if (log.isTraceEnabled()) {
            log.trace(sm.getString("tldConfig.webxmlStart"));
        }
    
    	// 默认情况下 JspConfigDescripto是ApplicationJspConfigDescriptor
        Collection descriptors =
            context.getJspConfigDescriptor().getTaglibs();
    
        for (TaglibDescriptor descriptor : descriptors) {
            String resourcePath = descriptor.getTaglibLocation();
            // Note: Whilst the Servlet 2.4 DTD implies that the location must
            // be a context-relative path starting with '/', JSP.7.3.6.1 states
            // explicitly how paths that do not start with '/' should be
            // handled.
            if (!resourcePath.startsWith("/")) {
                resourcePath = "/WEB-INF/" + resourcePath;
            }
            if (taglibUris.contains(descriptor.getTaglibURI())) {
                log.warn(sm.getString("tldConfig.webxmlSkip", resourcePath,
                        descriptor.getTaglibURI()));
            } else {
                if (log.isTraceEnabled()) {
                    log.trace(sm.getString("tldConfig.webxmlAdd", resourcePath,
                            descriptor.getTaglibURI()));
                }
                InputStream stream = null;
                try {
                    stream = context.getServletContext().getResourceAsStream(
                            resourcePath);
                    if (stream != null) {
                        XmlErrorHandler handler = tldScanStream(stream);
                        handler.logFindings(log, resourcePath);
                        taglibUris.add(descriptor.getTaglibURI());
                        webxmlTaglibUris.add(descriptor.getTaglibURI());
                    } else {
                        log.warn(sm.getString("tldConfig.webxmlFailPathDoesNotExist", resourcePath,
                                descriptor.getTaglibURI()));
                    }
                } catch (IOException ioe) {
                    log.warn(sm.getString("tldConfig.webxmlFail", resourcePath,
                            descriptor.getTaglibURI()), ioe);
                } finally {
                    if (stream != null) {
                        try {
                            stream.close();
                        } catch (Throwable t) {
                            ExceptionUtils.handleThrowable(t);
                        }
                    }
                }
            }
        }
    }
    

      先来看一个例子再来理解这段代码 。

    1. 配置MyTaglib.tld
    
    
    
        学习如何使用 Tag 接口
        1.0
        
        tools
        
        /learnTag
    
    
    
        
        
            基于 Tag 接口的自定义标签
            
            showTime
            
            com.luban.MyTaglib
            
            empty
        
    
    
    
    1. 创建MyTaglib类
    public class MyTaglib implements Tag {
     
        private PageContext pageContext;
        private Tag parent;
     
        public MyTaglib() {
            super();
        }
     
        @Override
        public void setPageContext(PageContext pageContext) {
            this.pageContext = pageContext;
        }
     
        @Override
        public void setParent(Tag tag) {
            this.parent = tag;
        }
     
        @Override
        public Tag getParent() {
            return this.parent;
        }
     
        @Override
        public int doStartTag() throws JspException {
            //返回 SKIP_BODY,表示不计算标签体
            return SKIP_BODY;
        }
     
        @Override
        public int doEndTag() throws JspException {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat();
                sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
                Date date = new Date();// 获取当前时间
                pageContext.getOut().write(sdf.format(date));
            } catch (IOException e) {
                throw new JspTagException(e.getMessage());
            }
            return EVAL_PAGE;
        }
        
        @Override
        public void release() {
     
        }
    }
    
    1. 在web.xml中添加taglib标签 ,但要区分一下,如果是Servlet2.4之前的版本,在web.xml中不需要加标签
    
    
        
            
            
                
                /learnTag
                
                /WEB-INF/MyTaglib.tld
            
        
        
    
    

    在这里插入图片描述

    1. 添加MyTaglib.jsp文件
    <%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
    <%@ page import="java.util.*" %>
    <%@ taglib prefix="tools" uri="/learnTag" %>
    
    
    
    
    
    
    
    

      有了这个例子后,再来看tldScanWebXml()源码 。
    在这里插入图片描述

    private XmlErrorHandler tldScanStream(InputStream resourceStream)
            throws IOException {
    
        InputSource source = new InputSource(resourceStream);
    
        XmlErrorHandler result = new XmlErrorHandler();
    
        synchronized (tldDigester) {
            try {
                tldDigester.setErrorHandler(result);
                tldDigester.push(this);
                tldDigester.parse(source);
            } catch (SAXException s) {
                // Hack - makes exception handling simpler
                throw new IOException(s);
            } finally {
                tldDigester.reset();
            }
            return result;
        }
    }
    

      在解析/WEB-INF/MyTaglib.tld文件时, 将this 也就是TldConfig添加到tldDigester中。 而在TldRuleSet中,使用了TaglibUriRule规则。

    在这里插入图片描述
    在这里插入图片描述

    当解析到下的 标签时,会将uri的body内容添加到TldConfig的taglibUris中 。

    在这里插入图片描述

      接下来看tldScanResourcePaths()方法的实现。

    private void tldScanResourcePaths(String startPath) {
    
        if (log.isTraceEnabled()) {
            log.trace(sm.getString("tldConfig.webinfScan", startPath));
        }
        
        ServletContext ctxt = context.getServletContext();
    
        Set dirList = ctxt.getResourcePaths(startPath);
        if (dirList != null) {
            Iterator it = dirList.iterator();
            while (it.hasNext()) {
            	// 如果路径以/WEB-INF/lib/或/WEB-INF/classes/ 开头,
            	// 并且不以.tld结构,则忽略掉
                String path = it.next();
                if (!path.endsWith(".tld")
                        && (path.startsWith("/WEB-INF/lib/")
                                || path.startsWith("/WEB-INF/classes/"))) {
                    continue;
                }
                // 如果文件路径以.tld结尾,但文件名以/WEB-INF/tags/开头
                // 并且文件名不是implicit.tld ,则忽略掉,也就是说默认情况下
                //  /WEB-INF/tags/目录下,只会扫描implicit.tld文件 ,当然其他目录
                // 下,只要以.tld文件结尾,还是会扫描解析的
                if (path.endsWith(".tld")) {
                    if (path.startsWith("/WEB-INF/tags/") &&
                            !path.endsWith("implicit.tld")) {
                        continue;
                    }
                    InputStream stream = ctxt.getResourceAsStream(path);
                    try {
                        XmlErrorHandler handler = tldScanStream(stream);
                        handler.logFindings(log, path);
                    } catch (IOException ioe) {
                        log.warn(sm.getString("tldConfig.webinfFail", path),
                                ioe);
                    } finally {
                        if (stream != null) {
                            try {
                                stream.close();
                            } catch (Throwable t) {
                                ExceptionUtils.handleThrowable(t);
                            }
                        }
                    }
                } else {
                	// 递归搜索
                    tldScanResourcePaths(path);
                }
            }
        }
    }
    

      看了tldScanResourcePaths()方法后,我们得出一个结论,如果非WEB-INF/tags目录下的文件,即使不配置在web.xml中,也会被扫描解析 。

    例1 ,注释掉web.xml中MyTaglib.tld配置
    在这里插入图片描述

    在这里插入图片描述

    例2 配置MyTaglib.tld

    在这里插入图片描述
      在web.xml配置taglib配置后, 虽然tldScanResourcePaths()方法依然没有扫描到/WEB-INF/tags/MyTaglib.tld文件,但配置有效,因为在tldScanWebXml()方法中已经处理过了。

      接下来看扫描StandardContext项目下的WEB-INF/lib下的所有jar包,并将扫描到的包通过TldJarScannerCallback类来处理。

    private class TldJarScannerCallback implements JarScannerCallback {
        boolean tldFound = true;
    
        @Override
        public void scan(JarURLConnection urlConn) throws IOException {
            tldFound = tldScanJar(urlConn);
        }
    
        @Override
        public void scan(File file) {
            File metaInf = new File(file, "META-INF");
            if (metaInf.isDirectory()) {
                tldFound = tldScanDir(metaInf);
            }
        }
    
        private boolean scanFoundNoTLDs() {
            return !tldFound;
        }
    }
    

      无论是URLConnection还是File类型,最终都由tldScanDir()方法来处理。

    private boolean tldScanDir(File start) {
        boolean isFound = false;
    
        if (log.isTraceEnabled()) {
            log.trace(sm.getString("tldConfig.dirScan", start.getAbsolutePath()));
        }
    
        File[] fileList = start.listFiles();
        if (fileList != null) {
            for (int i = 0; i < fileList.length; i++) {
                // Scan recursively
                if (fileList[i].isDirectory()) {
                	// 如果是目录,则递归查找
                    tldScanDir(fileList[i]);
                } else if (fileList[i].getAbsolutePath().endsWith(".tld")) {
                    InputStream stream = null;
                    isFound = true;
                    try {
                        stream = new FileInputStream(fileList[i]);
                        XmlErrorHandler handler = tldScanStream(stream);
                        handler.logFindings(log, fileList[i].getAbsolutePath());
                    } catch (IOException ioe) {
                        log.warn(sm.getString("tldConfig.dirFail",
                                fileList[i].getAbsolutePath()),
                                ioe);
                    } finally {
                        if (stream != null) {
                            try {
                                stream.close();
                            } catch (Throwable t) {
                                ExceptionUtils.handleThrowable(t);
                            }
                        }
                    }
                }
            }
        }
        if(!isFound){
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("tldConfig.noTldInDir", start.getAbsolutePath()));
            }
        }
        return isFound;
    }
    
    

      其实上面方法就是扫描jar包下所有META-INF下的.tld文件 。
      接下来看getTldListeners()方法 。

    public String[] getTldListeners() {
        String result[]=new String[listeners.size()];
        listeners.toArray(result);
        return result;
    }
    

      获取applicationListener监听器。
    在这里插入图片描述
      从上述源码中推出listener-class的使用。
    在这里插入图片描述
      添加类MyTaglibListener

    public class MyTaglibListener implements ServletContextListener {
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            System.out.println("====MyTaglibListener======");
        }
        @Override
        public void contextDestroyed(ServletContextEvent sce) {
        }
    }
    

      查看启动结果
    在这里插入图片描述

      到这里已经对TldConfig的configure_start事件分析完了。继续分析StandardContext中的其他方法 。

    欢迎页的设置

      向mapper中添加欢迎页。
    在这里插入图片描述
      而欢迎页默认来源于catalina.base/conf/web.xml
    在这里插入图片描述

    private void mergeParameters() {
        Map mergedParams = new HashMap();
    
        String names[] = findParameters();
        for (int i = 0; i < names.length; i++) {
            mergedParams.put(names[i], findParameter(names[i]));
        }
    
        ApplicationParameter params[] = findApplicationParameters();
        for (int i = 0; i < params.length; i++) {
            if (params[i].getOverride()) {
                if (mergedParams.get(params[i].getName()) == null) {
                    mergedParams.put(params[i].getName(),
                            params[i].getValue());
                }
            } else {
                mergedParams.put(params[i].getName(), params[i].getValue());
            }
        }
    
        ServletContext sc = getServletContext();
        for (Map.Entry entry : mergedParams.entrySet()) {
            sc.setInitParameter(entry.getKey(), entry.getValue());
        }
    }
    
    public String[] findParameters() {
        List parameterNames = new ArrayList(parameters.size());
        parameterNames.addAll(parameters.keySet());
        return parameterNames.toArray(new String[parameterNames.size()]);
    }
    
    
    
    public ApplicationParameter[] findApplicationParameters() {
        synchronized (applicationParametersLock) {
            return (applicationParameters);
        }
    }
    

      mergeParameters()方法本身业务不难, 但是要清楚parameters和applicationParameters是从哪里来的。 先来看parameters属性的由来 。

    在这里插入图片描述

      在catalina.base/webapps/servelet-test-1.0/WEB-INF/web.xml中配置context-param标签即可。

       aaa
       bbb

      接下来看applicationParameters参数是从哪里配置的。 有两个地方可以配置。

    在这里插入图片描述
    在这里插入图片描述

      接下来看执行mergeParameters()方法合并的效果。当然啦源于 applicationParameters 我们可以在context.xml文件中添加初始化参数,以实现所在Web 应用中的复用而不必每个Web 应用复制配置,当然在Web应用确实允许Tomcat 紧耦合的情况下,我们才推荐使用该方式进行配置,否则会导致Web 应用
    适应性非常差。
    在这里插入图片描述
      接下来看startInternal()的另外一段代码

    for (Map.Entry>> entry :
        initializers.entrySet()) {
        try {
            // Context容器启动时就会分别调用每个ServletContainerInitializer的onStartup()方法,将感兴趣的类作为参数传入
            entry.getKey().onStartup(entry.getValue(), getServletContext());
        } catch (ServletException e) {
            log.error(sm.getString("standardContext.sciFail"), e);
            ok = false;
            break;
        }
    }
    

      首先关于initializers属性值从哪里添加的。回顾之前的代码。之前在分析webConfig()方法中有一行这样的代码 。

    在这里插入图片描述

    public void addServletContainerInitializer(
             ServletContainerInitializer sci, Set> classes) {
         initializers.put(sci, classes);
    }
    

      现在已经对ServletContainerInitializer原理分析完了。

    实例管理器-InstanceManager

      Context容器中包含了一个实例管理器,它主要的作用是实现对Context容器中监听器过滤器及Servlet等实例管理,其中包括根据监听器Class对其进行实例化,对它们的Class的注解进行解析并处理, 对它们的Class实例化的访问权限的限制,销毁前统一的调用preDestory方法等。

      实例管理器的实现其实很简单,其中就使用了一些反射机制实例化出对象的,但这里面需要注意的地方是InstanceManager包含了两个类加载器, 一个是属于Tomcat容器内部的类加载器,而另外一个是Web应用的类加载器,Tomcat容器类加载器是Web就任类加载器的父类加载器, 且Tomcat容器类加载器在Tomcat整个生命周期中都存在,而Web应用加载器则不同,它可能在重启后则丢弃,最终被GC回收。

      所以,由不同的类加载器的类也会有不同的生命周期,于是,实例管理器中的loadClass方法中会有类似如下判断 。

    protected Class loadClass(String className, ClassLoader classLoader ) throws ClassNotFundException{
      if ( className.startWith(“org.apache.catalina”)){
        return containerClassLoader.loadClass(className);
      } else {
        return webClassLoader.loadClass(className);
      }
    }

      接下来看StandardContext的实例管理器的初始化 。

    if (ok ) {
        if (getInstanceManager() == null) {
            javax.naming.Context context = null;
            if (isUseNaming() && getNamingContextListener() != null) {
                context = getNamingContextListener().getEnvContext();
            }
            Map> injectionMap = buildInjectionMap(
                    getIgnoreAnnotations() ? new NamingResources(): getNamingResources());
            setInstanceManager(new DefaultInstanceManager(context,
                    injectionMap, this, this.getClass().getClassLoader()));
            getServletContext().setAttribute(
                    InstanceManager.class.getName(), getInstanceManager());
        }
    }
    

      接起来看实例管理器的应用 。

    public boolean listenerStart() {
        if (log.isDebugEnabled())
            log.debug("Configuring application event listeners");
    
        // Instantiate the required listeners
        ApplicationListener listeners[] = applicationListeners;
        Object results[] = new Object[listeners.length];
        boolean ok = true;
        for (int i = 0; i < results.length; i++) {
            if (getLogger().isDebugEnabled())
                getLogger().debug(" Configuring event listener class '" +
                    listeners[i] + "'");
            try {
                ApplicationListener listener = listeners[i];
                results[i] = getInstanceManager().newInstance(
                        listener.getClassName());
                if (listener.isPluggabilityBlocked()) {
                    noPluggabilityListeners.add(results[i]);
                }
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error
                    (sm.getString("standardContext.applicationListener",
                                  listeners[i].getClassName()), t);
                ok = false;
            }
        }
        if (!ok) {
            getLogger().error(sm.getString("standardContext.applicationSkipped"));
            return (false);
        }
    
        // Sort listeners in two arrays
        ArrayList eventListeners = new ArrayList();
        ArrayList lifecycleListeners = new ArrayList();
        for (int i = 0; i < results.length; i++) {
            if ((results[i] instanceof ServletContextAttributeListener)
                || (results[i] instanceof ServletRequestAttributeListener)
                || (results[i] instanceof ServletRequestListener)
                || (results[i] instanceof HttpSessionAttributeListener)) {
                eventListeners.add(results[i]);
            }
            if ((results[i] instanceof ServletContextListener)
                || (results[i] instanceof HttpSessionListener)) {
                lifecycleListeners.add(results[i]);
            }
        }
    
        // Listener instances may have been added directly to this Context by
        // ServletContextInitializers and other code via the pluggability APIs.
        // Put them these listeners after the ones defined in web.xml and/or
        // annotations then overwrite the list of instances with the new, full
        // list.
        for (Object eventListener: getApplicationEventListeners()) {
            eventListeners.add(eventListener);
        }
        setApplicationEventListeners(eventListeners.toArray());
        for (Object lifecycleListener: getApplicationLifecycleListeners()) {
            lifecycleListeners.add(lifecycleListener);
            if (lifecycleListener instanceof ServletContextListener) {
            	// 将不可插拔的Listeners存储于noPluggabilityListeners中
                noPluggabilityListeners.add(lifecycleListener);
            }
        }
        setApplicationLifecycleListeners(lifecycleListeners.toArray());
    
        // Send application start events
    
        if (getLogger().isDebugEnabled())
            getLogger().debug("Sending application start events");
    
        // Ensure context is not null
        getServletContext();
        context.setNewServletContextListenerAllowed(false);
    
        Object instances[] = getApplicationLifecycleListeners();
        if (instances == null || instances.length == 0) {
            return ok;
        }
    
        ServletContextEvent event = new ServletContextEvent(getServletContext());
        ServletContextEvent tldEvent = null;
        if (noPluggabilityListeners.size() > 0) {
            noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext());
            tldEvent = new ServletContextEvent(noPluggabilityServletContext);
        }
        for (int i = 0; i < instances.length; i++) {
            if (instances[i] == null)
                continue;
            if (!(instances[i] instanceof ServletContextListener))
                continue;
            ServletContextListener listener =
                (ServletContextListener) instances[i];
            try {
            	// 初始化实例listener的beforeContextInitialized事件 
                fireContainerEvent("beforeContextInitialized", listener);
                if (noPluggabilityListeners.contains(listener)) {
                	// 对不可插拔的Listeners调用,传入的是NoPluggabilityServletContext对象 
                    listener.contextInitialized(tldEvent);
                } else {
                    listener.contextInitialized(event);
                }
                // 初始化实例listener的afterContextInitialized事件 
                fireContainerEvent("afterContextInitialized", listener);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                fireContainerEvent("afterContextInitialized", listener);
                getLogger().error
                    (sm.getString("standardContext.listenerStart",
                                  instances[i].getClass().getName()), t);
                ok = false;
            }
        }
        return (ok);
    }
     
    

      listenerStart()方法的原理还是很复杂的,我们分4个部分来分析。

    1. 监听器实例化
    2. noPluggabilityListeners来源
    3. beforeContextInitialized 事件的应用
    4. contextInitialized()方法调用

      先来看监听器的实例化

    public Object newInstance(String className) throws IllegalAccessException,
    InvocationTargetException, NamingException, InstantiationException,
    ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
        Class clazz = loadClassMaybePrivileged(className, classLoader);
        return newInstance(clazz.getDeclaredConstructor().newInstance(), clazz);
    }
    
    
    private Object newInstance(Object instance, Class clazz) throws IllegalAccessException, InvocationTargetException, NamingException {
        if (!ignoreAnnotations) {
            Map injections = assembleInjectionsFromClassHierarchy(clazz);
            // 将class中属性及方法中配置的注解保存到annotationCache中
            populateAnnotationsCache(clazz, injections);
            // 对class中配置注解的属性及方法赋值
            processAnnotations(instance, injections);
            //  对配置了 @PostConstruct 注解的方法调用
            postConstruct(instance, clazz);
        }
        return instance;
    }
    

      在newInstance()方法中又分三步,第一步注解属性填充,第二步注解属性处理,第三步配置了 @PostConstruct 注解方法调用。先来看第一步。

    protected void populateAnnotationsCache(Class clazz,
            Map injections) throws IllegalAccessException,
    InvocationTargetException, NamingException {
        List annotations = null;
        Set injectionsMatchedToSetter = new HashSet();
        while (clazz != null) {
            AnnotationCacheEntry[] annotationsArray = annotationCache.get(clazz);
            if (annotationsArray == null) {
                if (annotations == null) {
                    annotations = new ArrayList();
                } else {
                    annotations.clear();
                }
                // Initialize methods annotations
                Method[] methods = Introspection.getDeclaredMethods(clazz);
                Method postConstruct = null;
                String postConstructFromXml = postConstructMethods.get(clazz.getName());
                Method preDestroy = null;
                String preDestroyFromXml = preDestroyMethods.get(clazz.getName());
                for (Method method : methods) {
                    if (context != null) {
                        // Resource injection only if JNDI is enabled
                        if (injections != null && Introspection.isValidSetter(method)) {
                            String fieldName = Introspection.getPropertyName(method);
                            injectionsMatchedToSetter.add(fieldName);
                            if (injections.containsKey(fieldName)) {
                                annotations.add(new AnnotationCacheEntry(
                                        method.getName(),
                                        method.getParameterTypes(),
                                        injections.get(fieldName),
                                        AnnotationCacheEntryType.SETTER));
                                continue;
                            }
                        }
                        Resource resourceAnnotation;
                        Annotation ejbAnnotation;
                        Annotation webServiceRefAnnotation;
                        Annotation persistenceContextAnnotation;
                        Annotation persistenceUnitAnnotation;
                        // 如果有方法配置了Resource注解
                        if ((resourceAnnotation = method.getAnnotation(Resource.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(
                                    method.getName(),
                                    method.getParameterTypes(),
                                    resourceAnnotation.name(),
                                    AnnotationCacheEntryType.SETTER));
                        } else if (EJB_PRESENT &&
                                (ejbAnnotation = method.getAnnotation(EJB.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(
                                    method.getName(),
                                    method.getParameterTypes(),
                                    ((EJB) ejbAnnotation).name(),
                                    AnnotationCacheEntryType.SETTER));
                        } else if (WS_PRESENT && (webServiceRefAnnotation =
                                method.getAnnotation(WebServiceRef.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(
                                    method.getName(),
                                    method.getParameterTypes(),
                                    ((WebServiceRef) webServiceRefAnnotation).name(),
                                    AnnotationCacheEntryType.SETTER));
                        } else if (JPA_PRESENT && (persistenceContextAnnotation =
                                method.getAnnotation(PersistenceContext.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(
                                    method.getName(),
                                    method.getParameterTypes(),
                                    ((PersistenceContext) persistenceContextAnnotation).name(),
                                    AnnotationCacheEntryType.SETTER));
                        } else if (JPA_PRESENT && (persistenceUnitAnnotation =
                                method.getAnnotation(PersistenceUnit.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(
                                    method.getName(),
                                    method.getParameterTypes(),
                                    ((PersistenceUnit) persistenceUnitAnnotation).name(),
                                    AnnotationCacheEntryType.SETTER));
                        }
                    }
                    postConstruct = findPostConstruct(postConstruct, postConstructFromXml, method);
                    preDestroy = findPreDestroy(preDestroy, preDestroyFromXml, method);
                }
    			// 如果class中有方法配置了PostConstruct注解
                if (postConstruct != null) {
                    annotations.add(new AnnotationCacheEntry(
                            postConstruct.getName(),
                            postConstruct.getParameterTypes(), null,
                            AnnotationCacheEntryType.POST_CONSTRUCT));
                } else if (postConstructFromXml != null) {
                    throw new IllegalArgumentException("Post construct method "
                            + postConstructFromXml + " for class " + clazz.getName()
                            + " is declared in deployment descriptor but cannot be found.");
                }
               	// 如果class中有方法配置了PreDestroy注解
                if (preDestroy != null) {
                    annotations.add(new AnnotationCacheEntry(
                            preDestroy.getName(),
                            preDestroy.getParameterTypes(), null,
                            AnnotationCacheEntryType.PRE_DESTROY));
                } else if (preDestroyFromXml != null) {
                    throw new IllegalArgumentException("Pre destroy method "
                            + preDestroyFromXml + " for class " + clazz.getName()
                            + " is declared in deployment descriptor but cannot be found.");
                }
    
                if (context != null) {
                    // Initialize fields annotations for resource injection if
                    // JNDI is enabled
                    Field[] fields = Introspection.getDeclaredFields(clazz);
                    for (Field field : fields) {
                        Resource resourceAnnotation;
                        Annotation ejbAnnotation;
                        Annotation webServiceRefAnnotation;
                        Annotation persistenceContextAnnotation;
                        Annotation persistenceUnitAnnotation;
                        String fieldName = field.getName();
                        if (injections != null && injections.containsKey(fieldName) && !injectionsMatchedToSetter.contains(fieldName)) {
                            annotations.add(new AnnotationCacheEntry(
                                    fieldName, null,
                                    injections.get(fieldName),
                                    AnnotationCacheEntryType.FIELD));
                        // 如果有属性配置了Resource注解
                        } else if ((resourceAnnotation =
                                field.getAnnotation(Resource.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(fieldName, null,
                                    resourceAnnotation.name(), AnnotationCacheEntryType.FIELD));
                        } else if (EJB_PRESENT &&
                                (ejbAnnotation = field.getAnnotation(EJB.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(fieldName, null,
                                    ((EJB) ejbAnnotation).name(), AnnotationCacheEntryType.FIELD));
                        } else if (WS_PRESENT && (webServiceRefAnnotation =
                                field.getAnnotation(WebServiceRef.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(fieldName, null,
                                    ((WebServiceRef) webServiceRefAnnotation).name(),
                                    AnnotationCacheEntryType.FIELD));
                        } else if (JPA_PRESENT && (persistenceContextAnnotation =
                                field.getAnnotation(PersistenceContext.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(fieldName, null,
                                    ((PersistenceContext) persistenceContextAnnotation).name(),
                                    AnnotationCacheEntryType.FIELD));
                        } else if (JPA_PRESENT && (persistenceUnitAnnotation =
                                field.getAnnotation(PersistenceUnit.class)) != null) {
                            annotations.add(new AnnotationCacheEntry(fieldName, null,
                                    ((PersistenceUnit) persistenceUnitAnnotation).name(),
                                    AnnotationCacheEntryType.FIELD));
                        }
                    }
                }
    
                if (annotations.isEmpty()) {
                    // Use common object to save memory
                    annotationsArray = ANNOTATIONS_EMPTY;
                } else {
                    annotationsArray = annotations.toArray(
                            new AnnotationCacheEntry[annotations.size()]);
                }
                synchronized (annotationCache) {
                    annotationCache.put(clazz, annotationsArray);
                }
            }
            clazz = clazz.getSuperclass();
        }
    }
    

      populateAnnotationsCache()方法的作用就是将属性中有Resource,WebServiceRef,PersistenceContext,PersistenceUnit注解封装成AnnotationCacheEntry 保存到annotationCache中,同样将方法中有Resource,WebServiceRef,PersistenceContext,PersistenceUnit,及PostConstruct,PreDestroy注解的方法封装成AnnotationCacheEntry保存于annotationCache中。
      接下来看processAnnotations()方法。

    protected void processAnnotations(Object instance, Map injections)
            throws IllegalAccessException, InvocationTargetException, NamingException {
    
        if (context == null) {
            // No resource injection
            return;
        }
        
        Class clazz = instance.getClass();
        while (clazz != null) {
            AnnotationCacheEntry[] annotations = annotationCache.get(clazz);
            for (AnnotationCacheEntry entry : annotations) {
            	// 对于方法上的注解处理
                if (entry.getType() == AnnotationCacheEntryType.SETTER) {
                    lookupMethodResource(context, instance,
                            getMethod(clazz, entry),
                            entry.getName(), clazz);
                // 对于属性上配置注解处理
                } else if (entry.getType() == AnnotationCacheEntryType.FIELD) {
                    lookupFieldResource(context, instance,
                            getField(clazz, entry),
                            entry.getName(), clazz);
                }
            }
            // 递归查找父类
            clazz = clazz.getSuperclass();
        }
    }
    

      先来看注解配置在方法中的处理逻辑

    protected static void lookupMethodResource(Context context,
            Object instance, Method method, String name, Class clazz)
                    throws NamingException, IllegalAccessException, InvocationTargetException {
        if (!Introspection.isValidSetter(method)) {
            throw new IllegalArgumentException(
                    sm.getString("defaultInstanceManager.invalidInjection"));
        }
    
        Object lookedupResource;
        boolean accessibility;
        String normalizedName = normalize(name);
        if ((normalizedName != null) && (normalizedName.length() > 0)) {
            lookedupResource = context.lookup(normalizedName);
        } else {
        	// 如果名称为空,则通过类名/方法名(
        	//如果是setXXX方法,则去除set前缀,取xXX)从JNDI中查找
            lookedupResource = context.lookup(
                    clazz.getName() + "/" + Introspection.getPropertyName(method));
        }
    
        synchronized (method) {
            accessibility = method.isAccessible();
            method.setAccessible(true);
            method.invoke(instance, lookedupResource);
            method.setAccessible(accessibility);
        }
    }
    

    在这里插入图片描述
      接下来看属性的赋值操作

    protected static void lookupFieldResource(Context context,
            Object instance, Field field, String name, Class clazz)
                    throws NamingException, IllegalAccessException {
        Object lookedupResource;
        boolean accessibility;
        String normalizedName = normalize(name);
        if ((normalizedName != null) && (normalizedName.length() > 0)) {
            lookedupResource = context.lookup(normalizedName);
        } else {
        	// 如果名称为空,则通过类名/方法名(
        	//如果是setXXX方法,则去除set前缀,取xXX)从JNDI中查找
            lookedupResource =
                    context.lookup(clazz.getName() + "/" + field.getName());
        }
    
        synchronized (field) {
            accessibility = field.isAccessible();
            field.setAccessible(true);
            // 从JNDI中查找到值后,反射为属性赋值
            field.set(instance, lookedupResource);
            field.setAccessible(accessibility);
        }
    }
    

      接下来看监听器中配置了@PostConstruct注解的方法调用 。

    protected void postConstruct(Object instance, final Class clazz)
            throws IllegalAccessException, InvocationTargetException {
        if (context == null) {
            // No resource injection
            return;
        }
    
        Class superClass = clazz.getSuperclass();
        if (superClass != Object.class) {
        	// 如果父类不是Object类,则递归查找父类有没有配置@PostConstruct注解,如果有,则调用它
            postConstruct(instance, superClass);
        }
    
        // At the end the postconstruct annotated
        // method is invoked
        AnnotationCacheEntry[] annotations = annotationCache.get(clazz);
        for (AnnotationCacheEntry entry : annotations) {
            if (entry.getType() == AnnotationCacheEntryType.POST_CONSTRUCT) {
                Method postConstruct = getMethod(clazz, entry);
                synchronized (postConstruct) {
                    boolean accessibility = postConstruct.isAccessible();
                    postConstruct.setAccessible(true);
                    // 反射调用配置了@PostConstruct注解方法
                    postConstruct.invoke(instance);
                    postConstruct.setAccessible(accessibility);
                }
            }
        }
    }
    

      监听器实例化过程还是简单的哈,无非就是从JNDI中查找变量为Class中配置了注解的属性赋值,相应的方法做调用,监听器中配置了@PostConstruct注解的方法进行调用。
      接下来看noPluggabilityListeners属性,这个属性的字面意思就是可插拔监听器。那什么是不可挺拔的监听器呢?我们来看一个例子。

    1. 创建一个ServletContainerInitializer
    @HandlesTypes(value = {AnnoContainerInitializer.class, Filter.class})
    public class MyServletContainerInitializer implements ServletContainerInitializer {
        @Override
        public void onStartup(Set> set, ServletContext servletContext) throws ServletException {
        
            final ServletTestServletContextListener lscl = new ServletTestServletContextListener();
            servletContext.addListener(lscl);
        }
    }
    
    1. 创建一个ServletContextListener,在MyServletContainerInitializer中将ServletTestServletContextListener添加到StandardContext的applicationLifecycleListenersObjects属性中。因为ServletTestServletContextListener实现了ServletContextListener接口,因此是添加到applicationLifecycleListenersObjects属性中,而如果ServletTestServletContextListener是实现了ServletContextAttributeListener,ServletRequestListener,ServletRequestAttributeListener,HttpSessionAttributeListener接口,则会将ServletTestServletContextListener添加到ServletContext的applicationEventListenersObjects属性中。
    public class ServletTestServletContextListener implements ServletContextListener
    {
        ContextAwareBase contextAwareBase;
        
        public ServletTestServletContextListener() {
            this.contextAwareBase = new ContextAwareBase();
        }
        
        public void contextInitialized(final ServletContextEvent sce) {
            System.out.println("contextInitialized");
        }
        
        public void contextDestroyed(final ServletContextEvent sce) {
            System.out.println("contextDestroyed");
        }
    }
    
    1. 配置MyServletContainerInitializer,将MyServletContainerInitializer类全名配置到/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0/WEB-INF/classes/META-INF/services/javax.servlet.ServletContainerInitializer文件下, 而文件内容为com.example.servelettest.MyServletContainerInitializer。
      在这里插入图片描述
      启动项目
      在这里插入图片描述
      在这里插入图片描述
      我们发现在lifecycleListeners中有4个Listener
      result = {Object[4]@2638}
      0 = {DataSourceMethodListener@2633}
      1 = {DataSourceFiledListener@2634}
      2 = {ServletTestServletContextListener@2234}
      3 = {LogbackServletContextListener@2620}
      其中ServletTestServletContextListener,LogbackServletContextListener在noPluggabilityListeners属性中存在,而DataSourceMethodListener和DataSourceFiledListener配置在/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0/WEB-INF/web.xml中。
      在这里插入图片描述
      有小伙伴可能好奇,为什么LogbackServletContextListener也会被添加到noPluggabilityListeners不可插拔属性中呢?打开logback-classic-1.2.3.jar包。
      在这里插入图片描述
      在这里插入图片描述
      因此得出结论,如果是我们自己在web.xml配置文件中配置的,则是可插拔的,如果在代码中通过调用ServletContext对象的addListener方法添加的,则是不可插拔的监听器。

      接下来看beforeContextInitialized事件的使用。

    public void fireContainerEvent(String type, Object data) {
    
        if (listeners.size() < 1)
            return;
    
        ContainerEvent event = new ContainerEvent(this, type, data);
        for (ContainerListener listener : listeners) {
            listener.containerEvent(event);
        }
    }
    

      首先listeners从哪里来的。
    在这里插入图片描述
      在代码中寻寻觅觅,找到了addContainerListener()容器监听器方法。
    在这里插入图片描述
      全局搜索时,也没有发现在解析XML时的规则调用addContainerListener方法
    在这里插入图片描述
      那允许在StandardContext标签下配置Listener吗?如
    在这里插入图片描述
      当然允许,那解析XML时,遇到Listener会调用StandardContext的哪个方法呢?
    在这里插入图片描述
      没有找到配置ContainerListener的方法,那怎么办呢?不用急,请看如下例子。

    1. 创建BeforeContextInitializedContainerListener实现ContainerListener接口。
    public class BeforeContextInitializedContainerListener  implements ContainerListener {
    
        @Override
        public void containerEvent(ContainerEvent event) {
            if("afterContextInitialized".equals(event.getType()) ||
                    "beforeContextInitialized".equals(event.getType())){
                System.out.println("容器监听事件 "+ event.getType());
            }
        }
    }
    
    1. 创建ServletContainerInitializer,在监听器中实现addContainerListener()方法调用
    @HandlesTypes(value = { Filter.class})
    public class MyServletContainerInitializer implements ServletContainerInitializer {
    
        @Override
        public void onStartup(Set> set, ServletContext servletContext) throws ServletException {
    
            if (servletContext instanceof ApplicationContextFacade) {
                try {
                    Field field = ApplicationContextFacade.class.getDeclaredField("context");
                    field.setAccessible(true);
                    ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
                    System.out.println(applicationContext);
                    Field contextF = ApplicationContext.class.getDeclaredField("context");
    
                    contextF.setAccessible(true);
                    StandardContext standardContext = (StandardContext) contextF.get(applicationContext);
    
                    System.out.println(standardContext);
                    standardContext.addContainerListener(new BeforeContextInitializedContainerListener());
    
    
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

      我们知道ServletContext实际上是ApplicationContextFacade对象,ApplicationContextFacade的context属性为ApplicationContext,ApplicationContext的context属性为StandardContext,拿到了StandardContext调用其addContainerListener()方法。看打印结果

    1. 结果输出

    在这里插入图片描述

    过滤器启动

      在看过滤器启动源码之前,先来看一个例子。

    1. 在项目的web.xml中添加过滤器
      在这里插入图片描述
      1、encoding:指定的字符集编码    
      2、ignore:表示是否忽略客户端请求设置的字符集编码,如果为true那么都会将请求字符集编码覆盖,如果为false,请求没有指定字符集编码时设置。默认为false
      这个过滤器的作用,提供了一种设置字符集编码的方式,通常情况下默认ISO-8859-1编码,但实际生产环境推荐使用UTF-8编码,而请求中的编码可以在未指定编码时使用,也可以强制覆盖。

      先看过滤器filterStart()启动方法,

    public boolean filterStart() {
        if (getLogger().isDebugEnabled())
            getLogger().debug("Starting filters");
        // Instantiate and record a FilterConfig for each defined filter
        boolean ok = true;
        synchronized (filterConfigs) {
            filterConfigs.clear();
            for (Entry entry : filterDefs.entrySet()) {
                String name = entry.getKey();
                if (getLogger().isDebugEnabled())
                    getLogger().debug(" Starting filter '" + name + "'");
                ApplicationFilterConfig filterConfig = null;
                try {
                    filterConfig =
                        new ApplicationFilterConfig(this, entry.getValue());
                    // 将创建的ApplicationFilterConfig保存到 filterConfigs属性中
                    filterConfigs.put(name, filterConfig);
                } catch (Throwable t) {
                    t = ExceptionUtils.unwrapInvocationTargetException(t);
                    ExceptionUtils.handleThrowable(t);
                    getLogger().error
                        (sm.getString("standardContext.filterStart", name), t);
                    ok = false;
                }
            }
        }
        return (ok);
    }
    

    在这里插入图片描述

      接下来继续看webConfig()方法 中的这一行代码webXml.configureContext(context),也就是webXml的configureContext方法。

    在这里插入图片描述

      在webXml的解析规则中,还需要注意addFilterMapping()方法的实现。
    在这里插入图片描述
    在这里插入图片描述
      有了这些基础知识,再来分析filterStart()方法的具体实现。重点看 ApplicationFilterConfig的创建。

    ApplicationFilterConfig(Context context, FilterDef filterDef)
            throws ClassCastException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, ServletException, InvocationTargetException, NamingException,
            IllegalArgumentException, NoSuchMethodException, SecurityException {
        super();
        this.context = context;
        this.filterDef = filterDef;
        if (filterDef.getFilter() == null) {
            getFilter();
        } else {
            this.filter = filterDef.getFilter();
            getInstanceManager().newInstance(filter);
            initFilter();
        }
    }
    

      如果filter不存在,则调用getFilter()初始化filter。

    /**    
     * Context 对象表示一个一个 web 应用而 FilterDef 表示一个过滤器定义。 ApplicationFilterConfig 的 getFilter 方法可以返回一个
     * javax.servlet.Filter 方法,该方法加载过滤器类并初始化它。
     */
    Filter getFilter() throws ClassCastException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, ServletException, InvocationTargetException, NamingException,
            IllegalArgumentException, NoSuchMethodException, SecurityException {
    
        if (this.filter != null)
            return (this.filter);
            
        String filterClass = filterDef.getFilterClass();
        // 实例化过滤器,之前分析过InstanceManager,这里不做具体分析
        this.filter = (Filter) getInstanceManager().newInstance(filterClass);
        // 初始化过滤器
        initFilter();
        
        return (this.filter);
    }
    

      接着看初始化过滤器方法。

    private void initFilter() throws ServletException {
        if (context instanceof StandardContext &&
                context.getSwallowOutput()) {
            try {
                SystemLogHandler.startCapture();
                // ApplicationFilterConfig Servlet规范提供了一个FilterConfig 接口访问的Filter的名称 ,初始化参数,及Servlet上下文
                // ApplicationFilterConfig 是FilterConfig接口的具体实现, 它的实现具体依赖Context接口, 而当初始化每个Filter时,
                // ApplicationFilter对象都会作为Filter的init方法的参数,所以我们在自定义Filter时可以在初始化方法中直接使用FilterConfig
                filter.init(this);
            } finally {
                String capturedlog = SystemLogHandler.stopCapture();
                if (capturedlog != null && capturedlog.length() > 0) {
                    getServletContext().log(capturedlog);
                }
            }
        } else {
            filter.init(this);
        }
        // Expose filter via JMX
        registerJMX();
    }
    
    

      调用过滤器自身的初始化方法,如果过滤器没有实现init()方法,则调用其父类的init()方法,看其父类的init()方法实现。

    public void init(FilterConfig filterConfig) throws ServletException {
        Enumeration paramNames = filterConfig.getInitParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            if (!IntrospectionUtils.setProperty(this, paramName,
                    filterConfig.getInitParameter(paramName))) {
                String msg = sm.getString("filterbase.noSuchProperty",
                        paramName, this.getClass().getName());
                if (isConfigProblemFatal()) {
                    throw new ServletException(msg);
                } else {
                    getLogger().warn(msg);
                }
            }
        }
    }
    
    public static boolean setProperty(Object o, String name, String value) {
        return setProperty(o,name,value,true);
    }
    
    @SuppressWarnings("null") // setPropertyMethodVoid is not null when used
    public static boolean setProperty(Object o, String name, String value,
            boolean invokeSetProperty) {
        if (log.isDebugEnabled())
            log.debug("IntrospectionUtils: setProperty(" +
                    o.getClass() + " " + name + "=" + value + ")");
    
        String setter = "set" + capitalize(name);
    
        try {
            Method methods[] = findMethods(o.getClass());
            Method setPropertyMethodVoid = null;
            Method setPropertyMethodBool = null;
    
            // First, the ideal case - a setFoo( String ) method
            for (int i = 0; i < methods.length; i++) {
                Class paramT[] = methods[i].getParameterTypes();
                if (setter.equals(methods[i].getName()) && paramT.length == 1
                        && "java.lang.String".equals(paramT[0].getName())) {
    
                    methods[i].invoke(o, new Object[] { value });
                    return true;
                }
            }
    
            // Try a setFoo ( int ) or ( boolean )
            for (int i = 0; i < methods.length; i++) {
                boolean ok = true;
                if (setter.equals(methods[i].getName())
                        && methods[i].getParameterTypes().length == 1) {
    
                    // match - find the type and invoke it
                    Class paramType = methods[i].getParameterTypes()[0];
                    Object params[] = new Object[1];
    
                    // Try a setFoo ( int )
                    if ("java.lang.Integer".equals(paramType.getName())
                            || "int".equals(paramType.getName())) {
                        try {
                            params[0] = Integer.valueOf(value);
                        } catch (NumberFormatException ex) {
                            ok = false;
                        }
                    // Try a setFoo ( long )
                    }else if ("java.lang.Long".equals(paramType.getName())
                                || "long".equals(paramType.getName())) {
                            try {
                                params[0] = Long.valueOf(value);
                            } catch (NumberFormatException ex) {
                                ok = false;
                            }
    
                        // Try a setFoo ( boolean )
                    } else if ("java.lang.Boolean".equals(paramType.getName())
                            || "boolean".equals(paramType.getName())) {
                        params[0] = Boolean.valueOf(value);
    
                        // Try a setFoo ( InetAddress )
                    } else if ("java.net.InetAddress".equals(paramType
                            .getName())) {
                        try {
                            params[0] = InetAddress.getByName(value);
                        } catch (UnknownHostException exc) {
                            if (log.isDebugEnabled())
                                log.debug("IntrospectionUtils: Unable to resolve host name:" + value);
                            ok = false;
                        }
    
                        // Unknown type
                    } else {
                        if (log.isDebugEnabled())
                            log.debug("IntrospectionUtils: Unknown type " +
                                    paramType.getName());
                    }
    
                    if (ok) {
                        methods[i].invoke(o, params);
                        return true;
                    }
                }
    
                // save "setProperty" for later
                if ("setProperty".equals(methods[i].getName())) {
                    if (methods[i].getReturnType()==Boolean.TYPE){
                        setPropertyMethodBool = methods[i];
                    }else {
                        setPropertyMethodVoid = methods[i];
                    }
    
                }
            }
    
            // Ok, no setXXX found, try a setProperty("name", "value")
            if (invokeSetProperty && (setPropertyMethodBool != null ||
                    setPropertyMethodVoid != null)) {
                Object params[] = new Object[2];
                params[0] = name;
                params[1] = value;
                if (setPropertyMethodBool != null) {
                    try {
                        return ((Boolean) setPropertyMethodBool.invoke(o,
                                params)).booleanValue();
                    }catch (IllegalArgumentException biae) {
                        //the boolean method had the wrong
                        //parameter types. lets try the other
                        if (setPropertyMethodVoid!=null) {
                            setPropertyMethodVoid.invoke(o, params);
                            return true;
                        }else {
                            throw biae;
                        }
                    }
                } else {
                    setPropertyMethodVoid.invoke(o, params);
                    return true;
                }
            }
    
        } catch (IllegalArgumentException ex2) {
            log.warn("IAE " + o + " " + name + " " + value, ex2);
        } catch (SecurityException ex1) {
            log.warn("IntrospectionUtils: SecurityException for " +
                    o.getClass() + " " + name + "=" + value + ")", ex1);
        } catch (IllegalAccessException iae) {
            log.warn("IntrospectionUtils: IllegalAccessException for " +
                    o.getClass() + " " + name + "=" + value + ")", iae);
        } catch (InvocationTargetException ie) {
            ExceptionUtils.handleThrowable(ie.getCause());
            log.warn("IntrospectionUtils: InvocationTargetException for " +
                    o.getClass() + " " + name + "=" + value + ")", ie);
        }
        return false;
    }
    

      细看 setProperty(Object o, String name, String value) 方法,还是很容易理解,在配置文件中配置的value肯定是字符串类型,像SetCharacterEncodingFilter

    public class SetCharacterEncodingFilter extends FilterBase {
        private String encoding = null;
        private boolean ignore = false;
    }
    

      对象的属性可能是int类型,可能是boolean类型,因此setProperty()方法的主要目的是将字符串类型转化为对象需要的类型,并通过反射初始化值,所以整个init()的逻辑就很简单了,就是将web.xml文件中Filter配置信息通过反射设置到Filter过滤器属性中。

      最终创建的ApplicationFilterConfig对象被保存到filterConfigs中,那filterConfigs和filterMaps他们之间有什么关系呢?看 filter.doFilter(request, response, this); 这一行代码,这就是过滤器调用的代码。
    在这里插入图片描述
      那filters从哪里来的呢?

    在这里插入图片描述
      当发送http请求时,创建过滤链,有一行代码

    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMaps[i].getFilterName());
    
                
    public FilterConfig findFilterConfig(String name) {
    
        return (filterConfigs.get(name));
    }
    

      此时能看出 filterMaps和filterConfigs之间的关系了吧,从filterMaps获取过滤器名,再从filterConfigs中取过过滤器配置,再调用过滤器链的addFilter()方法,将ApplicationFilterConfig作为参数传入。

    void addFilter(ApplicationFilterConfig filterConfig) {
        // Prevent the same filter being added multiple times
        // 排重
        for(ApplicationFilterConfig filter:filters)
            if(filter==filterConfig)
                return;
        // 扩容
        if (n == filters.length) {
            ApplicationFilterConfig[] newFilters =
                new ApplicationFilterConfig[n + INCREMENT];
            System.arraycopy(filters, 0, newFilters, 0, n);
            filters = newFilters;
        }
        // 添加到数组中
        filters[n++] = filterConfig;
    }
    

      此时将ApplicationFilterConfig添加到filters中,而ApplicationFilterConfig的filter保存了具体过滤器(如SetCharacterEncodingFilter),因此在实际调用时,取出ApplicationFilterConfig的filter,再调用其doFilter方法即可。

    StandardWrapper启动

      接下来看Wrapper的启动

    public boolean loadOnStartup(Container children[]) {
        // Collect "load on startup" servlets that need to be initialized
        // 以loadOnStartup为key,存储到TreeMap中
        // 为什么要这么做呢?因为Servlet启动本身有先后顺序,loadOnStartup越小,越先启动
        TreeMap> map =
            new TreeMapInteger, ArrayListWrapper>>();
        for (int i = 0; i  children.length; i++) {
            Wrapper wrapper = (Wrapper) children[i];
            int loadOnStartup = wrapper.getLoadOnStartup();
            if (loadOnStartup <  0)
            	// 自定义的servlet的loadOnStartup 小于0,不会在启动tomcat时
            	// 加载Servlet,只有在真正调用时才会加载servlet 
                continue;
            Integer key = Integer.valueOf(loadOnStartup);
            ArrayListWrapper> list = map.get(key);
            if (list == null) {
                list = new ArrayListWrapper>();
                map.put(key, list);
            }
            list.add(wrapper);
        }
    
        // Load the collected "load on startup" servlets
        for (ArrayListWrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                } catch (ServletException e) {
                    getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
                          getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
                    // NOTE: load errors (including a servlet that throws
                    // UnavailableException from the init() method) are NOT
                    // fatal to application startup
                    // unless failCtxIfServletStartFails="true" is specified
                    if(getComputedFailCtxIfServletStartFails()) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
    

    在这里插入图片描述
      为什么会出现loadOnStartup值这么大呢?请看getLoadOnStartup()方法。

    public int getLoadOnStartup() {
    	// 当如果是jsp的servlet或loadOnStartup 为-1 ,则取 Integer.MAX_VALUE 值作为loadOnStartup 
        if (isJspServlet && loadOnStartup < 0) {
            /*
             * JspServlet must always be preloaded, because its instance is
             * used during registerJMX (when registering the JSP
             * monitoring mbean)
             */
             return Integer.MAX_VALUE;
        } else {
            return (this.loadOnStartup);
        }
    }
    

    在这里插入图片描述

    现在明白 com.example.servelettest.AAAServlet 为什么他的loadOnStartup属性是 Integer.MAX_VALUE了吧。

      为什么MyHttpServlet的loadOnStartup是1呢?因为注解中配置的。
    在这里插入图片描述
      当然啦,也可以在xml中配置
    在这里插入图片描述
      当然还有一个问题需要注意,当我们自定义的Servlet,他的loadOnStartup 默认为-1,因此不会在启动tomcat时加载Servlet,而真正到Servlet调用时,才会加载Servlet,如
    在这里插入图片描述
    在这里插入图片描述

      现在应该知道loadOnStartup属性的作用了吧,这个属性能控制Servlet的加载顺序,值越小,越先加载。 接下来进入load方法。

    public synchronized void load() throws ServletException {
    	// 加载servlet
        instance = loadServlet();
    
        if (!instanceInitialized) {
            initServlet(instance);
        }
    
    	// 如果是jspServlet,则注册jsp监控器到jmx 
        if (isJspServlet) {
            StringBuilder oname =
                new StringBuilder(MBeanUtils.getDomain(getParent()));
    
            oname.append(":type=JspMonitor,name=");
            oname.append(getName());
    
            oname.append(getWebModuleKeyProperties());
    
            try {
                jspMonitorON = new ObjectName(oname.toString());
                Registry.getRegistry(null, null).registerComponent(instance, jspMonitorON, null);
            } catch (Exception ex) {
                log.warn("Error registering JSP monitoring with jmx " + instance);
            }
        }
    }
    

    在这里插入图片描述

    /**
     *    方法loadServlet负责加载servlet类,类名应该被分配给servletClass 变量,该方法被该值分配给一个String 类型的变量 actualClass
     *
     *  但是,由于Catalina 也是一个JSP容器,在请求的JSP页面的时候,loadServlet 必须也能工作,如果是jsp页面,则得到相应的Servlet类。
     *  如果JSP 页面的servlet 名字找不到,就用servletClass 变量的值,但是,如果访劁珠值没有使用StandardWrapper 类中的setServletClass方法
     *  设置会产生异常,剩余部分不会被执行。
     *  现在servlet的名字已经获得了,接下来,是loadServlet 方法获得加载器,如果找不到加载器,则产生异常并停止执行。
     *
     *
     */
    public synchronized Servlet loadServlet() throws ServletException {
        if (unloading) {
            throw new ServletException(
                    sm.getString("standardWrapper.unloading", getName()));
        }
    
        // Nothing to do if we already have an instance or an instance pool
        if (!singleThreadModel && (instance != null))
            return instance;
    
        PrintStream out = System.out;
        if (swallowOutput) {
            SystemLogHandler.startCapture();
        }
    
        Servlet servlet;
        try {
            long t1=System.currentTimeMillis();
            // Complain if no servlet class has been specified
            if (servletClass == null) {
                unavailable(null);
                throw new ServletException
                    (sm.getString("standardWrapper.notClass", getName()));
            }
    
            InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
            try {
                // 有了类加载器和要加载的 Servlet 名字,就可以使用 loadServlet 方法来加载类 了。
                // 1. 创建Servlet实例,如果添加了JNDI 注解,将进行依赖注入
                servlet = (Servlet) instanceManager.newInstance(servletClass);
            } catch (ClassCastException e) {
                unavailable(null);
                // Restore the context ClassLoader
                throw new ServletException
                    (sm.getString("standardWrapper.notServlet", servletClass), e);
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                unavailable(null);
    
                // Added extra log statement for Bugzilla 36630:
                // https://bz.apache.org/bugzilla/show_bug.cgi?id=36630
                if(log.isDebugEnabled()) {
                    log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
                }
    
                // Restore the context ClassLoader
                throw new ServletException
                    (sm.getString("standardWrapper.instantiate", servletClass), e);
            }
    
            if (multipartConfigElement == null) {
                // 2. 读取javax.servlet.annotation.MultipartConfig配置,以用于multipart/form-data请求处理,包括临时文件存储路径 。
                // 上传文件最大字节数,请求最大字节数,文件大小阈值。
                MultipartConfig annotation =
                        servlet.getClass().getAnnotation(MultipartConfig.class);
                if (annotation != null) {
                    multipartConfigElement =
                            new MultipartConfigElement(annotation);
                }
            }
    
            // Special handling for ContainerServlet instances
            // 如果通过了安全性检查,接下来检查该 Servlet 是否是一个 ContainerServlet。 ContainerServlet 是实现了 org.apache.catalina.ContainerServlet
            // 接口的 Servlet,它可以访问 Catalina 的内部函数。如果该 Servlet 是 ContainerServlet,loadServlet 方法调用 ContainerServlet 的 setWrapper
            // 方法,传递该 StandardWrapper 实例。
            if ((servlet instanceof ContainerServlet) &&
                    // isContainerProvidedServlet 方法返回 true 值。classLoader 会获得另一个 ClassLoader 的实例,这样就可以访问 Catalina 的内部了。
                    (isContainerProvidedServlet(servletClass) ||
                            ((Context) getParent()).getPrivileged() )) {
                ((ContainerServlet) servlet).setWrapper(this);
            }
    
            classLoadTime=(int) (System.currentTimeMillis() -t1);
    
            // 实现了SingleThreadModel接口
            if (servlet instanceof SingleThreadModel) {
                if (instancePool == null) {
                    instancePool = new Stack();
                }
                singleThreadModel = true;
            }
    
            // 4. 初始化servlet
            initServlet(servlet);
    
            fireContainerEvent("load", this);
    
            loadTime=System.currentTimeMillis() -t1;
        } finally {
            if (swallowOutput) {
                String log = SystemLogHandler.stopCapture();
                if (log != null && log.length() > 0) {
                    if (getServletContext() != null) {
                        getServletContext().log(log);
                    } else {
                        out.println(log);
                    }
                }
            }
        }
        return servlet;
    
    }
    
    

      接着继续看initServlet()接口。

    private synchronized void initServlet(Servlet servlet)
            throws ServletException {
    
        if (instanceInitialized && !singleThreadModel) return;
    
        // Call the initialization method of this servlet
        try {
            // 接下来 loadServlet 方法触发 BEFORE_INIT_EVENT 事件,并调用发送者的 init 方法。
            instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,
                                              servlet);
    
            if( Globals.IS_SECURITY_ENABLED) {
                boolean success = false;
                try {
                    Object[] args = new Object[] { facade };
                    SecurityUtil.doAsPrivilege("init",
                                               servlet,
                                               classType,
                                               args);
                    success = true;
                } finally {
                    if (!success) {
                        // destroy() will not be called, thus clear the reference now
                        SecurityUtil.remove(servlet);
                    }
                }
            } else {
                //因此,当 StandardWrapper 对象调用 Servlet 实例的 init 方法的时候,它传递 的是一个 StandardWrapperFacade 对象。
                // 在 Servlet 内部调用 ServletConfig 的 getServletName, getInitParameter, 和 getInitParameterNames 方法只需
                // 要调用它们在 StandardWrapper 的实现就行。
                servlet.init(facade);
            }
    
            instanceInitialized = true;
    
            instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                              servlet);
        } catch (UnavailableException f) {
            instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                              servlet, f);
            unavailable(f);
            throw f;
        } catch (ServletException f) {
            instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                              servlet, f);
            // If the servlet wanted to be unavailable it would have
            // said so, so do not call unavailable(null).
            throw f;
        } catch (Throwable f) {
            ExceptionUtils.handleThrowable(f);
            getServletContext().log("StandardWrapper.Throwable", f );
            instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                              servlet, f);
            // If the servlet wanted to be unavailable it would have
            // said so, so do not call unavailable(null).
            throw new ServletException
                (sm.getString("standardWrapper.initException", getName()), f);
        }
    }
    

      Servlet的初始化分为三种情况,第一种,默认的Servlet,第二种,我们自定义的servlet, 第三种,Jsp Servlet。先来看第一种,默认的jsp初始化。

    public void init() throws ServletException {
    
        if (getServletConfig().getInitParameter("debug") != null)
            debug = Integer.parseInt(getServletConfig().getInitParameter("debug"));
    
        if (getServletConfig().getInitParameter("input") != null)
            input = Integer.parseInt(getServletConfig().getInitParameter("input"));
    
        if (getServletConfig().getInitParameter("output") != null)
            output = Integer.parseInt(getServletConfig().getInitParameter("output"));
    
        listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings"));
    
        if (getServletConfig().getInitParameter("readonly") != null)
            readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly"));
    
        if (getServletConfig().getInitParameter("sendfileSize") != null)
            sendfileSize =
                Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024;
    
        fileEncoding = getServletConfig().getInitParameter("fileEncoding");
    
        globalXsltFile = getServletConfig().getInitParameter("globalXsltFile");
        contextXsltFile = getServletConfig().getInitParameter("contextXsltFile");
        localXsltFile = getServletConfig().getInitParameter("localXsltFile");
        readmeFile = getServletConfig().getInitParameter("readmeFile");
    
        if (getServletConfig().getInitParameter("useAcceptRanges") != null)
            useAcceptRanges = Boolean.parseBoolean(getServletConfig().getInitParameter("useAcceptRanges"));
    
        // Sanity check on the specified buffer sizes
        if (input < 256)
            input = 256;
        if (output < 256)
            output = 256;
    
        if (debug > 0) {
            log("DefaultServlet.init:  input buffer size=" + input +
                ", output buffer size=" + output);
        }
    
        // Load the proxy dir context.
        resources = (ProxyDirContext) getServletContext()
            .getAttribute(Globals.RESOURCES_ATTR);
        if (resources == null) {
            try {
                resources =
                    (ProxyDirContext) new InitialContext()
                    .lookup(RESOURCES_JNDI_NAME);
            } catch (NamingException e) {
                // Failed
                throw new ServletException("No resources", e);
            }
        }
    
        if (resources == null) {
            throw new UnavailableException(sm.getString("defaultServlet.noResources"));
        }
    
        if (getServletConfig().getInitParameter("showServerInfo") != null) {
            showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo"));
        }
    }
    

      默认的servlet初始化还是很简单的,就是将xml中的配置转化为DefaultServlet配置即可。 如
    在这里插入图片描述
      当然第二种自定义的servlet初始化处理,我们下一篇博客专门分析servlet请求时再来分析,看第三种情况,当是jsp的Servlet时怎样处理。

    public void init(ServletConfig config) throws ServletException {
    
        super.init(config);
        this.config = config;
        this.context = config.getServletContext();
    
        // Initialize the JSP Runtime Context
        // Check for a custom Options implementation
        // 允许指定的类来配置 Jasper。如果没有指定,则使用默认的 Servlet 内置参数(EmbeddedServletOptions)。
        String engineOptionsName = config.getInitParameter("engineOptionsClass");
        if (Constants.IS_SECURITY_ENABLED && engineOptionsName != null) {
            log.info(Localizer.getMessage(
                    "jsp.info.ignoreSetting", "engineOptionsClass", engineOptionsName));
            engineOptionsName = null;
        }
        if (engineOptionsName != null) {
            // Instantiate the indicated Options implementation
            try {
                ClassLoader loader = Thread.currentThread().getContextClassLoader();
                Class engineOptionsClass = loader.loadClass(engineOptionsName);
                Class[] ctorSig = { ServletConfig.class, ServletContext.class };
                Constructor ctor = engineOptionsClass.getConstructor(ctorSig);
                Object[] args = { config, context };
                options = (Options) ctor.newInstance(args);
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                // Need to localize this.
                log.warn("Failed to load engineOptionsClass", e);
                // Use the default Options implementation
                options = new EmbeddedServletOptions(config, context);
            }
        } else {
            // Use the default Options implementation 默认值
            options = new EmbeddedServletOptions(config, context);
        }
        // 创建jsp编译时运行时上下文 
        rctxt = new JspRuntimeContext(context, options);
        // 如果配置了标签
        if (config.getInitParameter("jspFile") != null) {
            jspFile = config.getInitParameter("jspFile");
            try {
                if (null == context.getResource(jspFile)) {
                    throw new ServletException("missing jspFile: [" + jspFile + "]");
                }
            } catch (MalformedURLException e) {
                throw new ServletException("Can not locate jsp file", e);
            }
            try {
                if (SecurityUtil.isPackageProtectionEnabled()){
                   AccessController.doPrivileged(new PrivilegedExceptionAction(){
                        @Override
                        public Object run() throws IOException, ServletException {
                            serviceJspFile(null, null, jspFile, true);
                            return null;
                        }
                    });
                } else {
                    serviceJspFile(null, null, jspFile, true);
                }
            } catch (IOException e) {
                throw new ServletException("Could not precompile jsp: " + jspFile, e);
            } catch (PrivilegedActionException e) {
                Throwable t = e.getCause();
                if (t instanceof ServletException) throw (ServletException)t;
                throw new ServletException("Could not precompile jsp: " + jspFile, e);
            }
        }
        if (log.isDebugEnabled()) {
            log.debug(Localizer.getMessage("jsp.message.scratch.dir.is",
                    options.getScratchDir().toString()));
            log.debug(Localizer.getMessage("jsp.message.dont.modify.servlets"));
        }
    }
     
    

      如果JspServlet配置了jsp-file标签,则会对jsp预编译。
    在这里插入图片描述

    private void serviceJspFile(HttpServletRequest request,
                                HttpServletResponse response, String jspUri,
                                boolean precompile)
        throws ServletException, IOException {
    
        // 做一个并发的控制,当有两个请求并发访问同一个jsp页面时,只生成唯一的一个JspServletWrapper,并缓存
        JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
        if (wrapper == null) {
            synchronized(this) {
                wrapper = rctxt.getWrapper(jspUri);
                if (wrapper == null) {
                    // Check if the requested JSP page exists, to avoid
                    // creating unnecessary directories and files.
                    if (null == context.getResource(jspUri)) {
                        handleMissingResource(request, response, jspUri);
                        return;
                    }
                    wrapper = new JspServletWrapper(config, options, jspUri,
                                                    rctxt);
                    // 将jsw存入JspRuntimeContext的jsps中, 一个jsp页面对应一个jsw
                    rctxt.addWrapper(jspUri,wrapper);
                }
            }
        }
    
        try {
            // JspServletWrapper来处理请求
            wrapper.service(request, response, precompile);
        } catch (FileNotFoundException fnfe) {
            handleMissingResource(request, response, jspUri);
        }
    
    }
    

      接下来看jsp的service方法调用。

    public void service(HttpServletRequest request,
                        HttpServletResponse response,
                        boolean precompile)
            throws ServletException, IOException, FileNotFoundException {
    
        Servlet servlet;
    
        try {
    
            if (ctxt.isRemoved()) {
                throw new FileNotFoundException(jspUri);
            }
    
            if ((available > 0L) && (available < Long.MAX_VALUE)) {
                if (available > System.currentTimeMillis()) {
                    response.setDateHeader("Retry-After", available);
                    response.sendError
                        (HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                         Localizer.getMessage("jsp.error.unavailable"));
                    return;
                }
    
                // Wait period has expired. Reset.
                available = 0;
            }
    
            /*
             * (1) Compile
             *  将jsp文件编译成servlet
             */
            if (options.getDevelopment() || mustCompile) {
                synchronized (this) {
                    if (options.getDevelopment() || mustCompile) {
                        // The following sets reload to true, if necessary
                        ctxt.compile();
                        mustCompile = false;
                    }
                }
            } else {
                if (compileException != null) {
                    // Throw cached compilation exception
                    throw compileException;
                }
            }
    
            /*
             * (2) (Re)load servlet class file
             * 生成Servlet文件对应的对象实例
             */
            servlet = getServlet();
    
            // If a page is to be precompiled only, return.
            // 一个页面仅仅只需要编译,因为是预编译,jspServlet初始化执行到这里终止
            if (precompile) {
                return;
            }
    
        } catch (ServletException ex) {
            if (options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw ex;
        } catch (FileNotFoundException fnfe) {
            // File has been removed. Let caller handle this.
            throw fnfe;
        } catch (IOException ex) {
            if (options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw ex;
        } catch (IllegalStateException ex) {
            if (options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw ex;
        } catch (Exception ex) {
            if (options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw new JasperException(ex);
        }
    
        try {
    
            /*
             * (3) Handle limitation of number of loaded Jsps
             */
            if (unloadAllowed) {  // 是否要卸载jsp-servlet
                synchronized(this) {
                    if (unloadByCount) {
                        // 如果配置了限制的数量,则表示ctxt.getRuntimeContext()中只能容纳固定的jsw
                        // 那么如果超过了限制则将队尾jsw移除掉
                        // 当然,就算没有配置限制的数量,background线程会定时执行,将超过jspIdleTimeout时间的移除掉
                        if (unloadHandle == null) {
                            unloadHandle = ctxt.getRuntimeContext().push(this);
                        } else if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) {
                            // lastUsageTime表示当前jsw上次使用时间
                            // ctxt.getRuntimeContext().getLastJspQueueUpdate()这个时间会由background线程定时更新一次
                            // 如果lastUsageTime 大于 ctxt.getRuntimeContext().getLastJspQueueUpdate()不需要做什么操作
    
                            // 第一种情况
                            // 1. jsw被访问
                            // 2. background线程执行
                            // 3. jsw再次被访问
                            // 4. 符合当前条件,jsw被移动至队首
    
                            // 第二种情况
                            // 1. background线程执行
                            // 2. jsw第一次被访问
                            // 3. 不符合条件,而此时应该符合unloadHandle == null
    
                            // 将最近访问的jsw移动至队首
                            ctxt.getRuntimeContext().makeYoungest(unloadHandle); // 将unloadHandle移到队首
                            lastUsageTime = System.currentTimeMillis();
                        }
                    } else {
                        // 更新最近使用的时间
                        if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) {
                            lastUsageTime = System.currentTimeMillis();
                        }
                    }
                }
            }
            /*
             * (4) Service request
             */
            if (servlet instanceof SingleThreadModel) {
               // sync on the wrapper so that the freshness
               // of the page is determined right before servicing
               synchronized (this) {
                   servlet.service(request, response);
                }
            } else {
                servlet.service(request, response);
            }
        } catch (UnavailableException ex) {
            String includeRequestUri = (String)
                request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
            if (includeRequestUri != null) {
                // This file was included. Throw an exception as
                // a response.sendError() will be ignored by the
                // servlet engine.
                throw ex;
            }
    
            int unavailableSeconds = ex.getUnavailableSeconds();
            if (unavailableSeconds <= 0) {
                unavailableSeconds = 60;        // Arbitrary default
            }
            available = System.currentTimeMillis() +
                (unavailableSeconds * 1000L);
            response.sendError
                (HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                 ex.getMessage());
        } catch (ServletException ex) {
            if(options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw ex;
        } catch (IOException ex) {
            if (options.getDevelopment()) {
                throw new IOException(handleJspException(ex).getMessage(), ex);
            }
            throw ex;
        } catch (IllegalStateException ex) {
            if(options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw ex;
        } catch (Exception ex) {
            if(options.getDevelopment()) {
                throw handleJspException(ex);
            }
            throw new JasperException(ex);
        }
    }
    

      上面有一行极其重要的代码 ctxt.compile(); jsp编译,之前写的 Tomcat 源码解析一JSP编译器Jasper-佛怒火莲 就是从这里开始分析,现在终于将所有的源码串联起来了,如果对jsp编译成servlet代码感兴趣,可以去看看 Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(上)Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(下) 这两篇博客,Servlet的加载又告一段落了,到这里,整个StandardContext的启动已经分析完毕 。 因此总结一下。

    StandardContext 启动过程总结
    1. 发布正在启动的JMX通知,这样可以通过添加NotificationListener 来监听Web应用的启动。
    2. 启动当前Context维护的JNDI资源 。
    3. 初始化当前Context 使用的WebResourceRoot 并启动,WebResourceRoot 维护了Web 应用的所有的资源集合(Class 文件,Jar 包 以及其他资源文件 )
      主要用于类加载和按照路径查找资源文件 。
      【注意】:WebResourceRoot是Tomcat8 新增的资源接口,旧版本采用FileDirContext管理目录资源,采用WARDirContext 管理WAR 包资源 。
      WebResourceRoot表示组成Web 应用的所有资源的集合,在WebResourceRoot 中,一个Web 应用的资源又可以按照分类划分为多个集合(WebResourceSet),
      当查找资源时,将按照指定的顺序处理,其分类和顺序如下 。
      Pre资源 :即在Context.xml通过 配置资源,这些资源将按照它们配置的顺序查找 。
      Main资源:即Web应用目录,WAR 包或者WAR 包解压目录包含的文件,这些资源的查找顺序为WEB-INF/classes,WEB-INF/lib
      Jar 资源:即在context.xml 中通过 配置的资源,这些资源将按照它们的配置顺序查找 。
      Post资源:即在context.xml中通过 配置资源,这些资源将按照它们的配置顺序查找 。
      由WebResourceRoot支持的资源集合以及配置方式,我们可以发现,从Tomcat 8 版本开始,Context 不仅可以加载Web 应用内部资源,还可以加载
      位置其外部的资源,而且通过PreResources ,JarResources ,PostResources 这3类资源集合控制其在资源查找时的优先级,通过这种方式,我们
      可以实现对某些资源的复用,如1.5节所述,提供一个公用的Web 应用包, 然后其他的Web应用均以此为基础,添加相关的定制化功能 。
    4. 创建Web应用类加载器(WebappLoader),WebappLoader 继承自LifecycleMBeanBase ,在其启动时创建Web应用类加载器(WebappClassLoader),
      此外,该类还提供了backgroundProcess, 用于Context 后台处理,当检查到Web应用的类文件,Jar 包发生变更时,重新加载Context 。
    5. 如果没有设置Cookie处理器, 则创建默认的 Rfc6265CookieProcessor
    6. 设置字符集映射(CharsetMapper),该映射主要用于根据Locale获取字符集编码 。
    7. 初始化临时目录,默认为$CATALINA_BASE/work///
    8. Web 应用依赖检测,主要检测依赖扩展点的完整性。
      9 如果当前Context 使用了JNDI ,则为其添加NamingContextListener。
      10 启动Web应用类加载器(WebappLoader.start),此时才真正的创建WebappClassLoader实例。
    9. 启动安全组件(Realm)
    10. 发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件以完成Servlet的创建,具体下一节将讲解 。
    11. 启动Context 子节点(Wrapper)
    12. 启动Context 维护的Pipeline
    13. 创建会话管理器,如果配置了集群组件,则由集群组件创建,否则使用标准的会话管理器(StandardManager),在集群环境下,需要将会话管理器注册
      到集群组件 。
    14. 将Context的Web资源集合(org.apache.catalina.WebResourceSet)添加到ServletContext属性,属性名为org.apache.catalina.resources
    15. 创建实例管理器(InstanceManager),用于创建对象的实例, 例如 Servlet,Filter等。
    16. 将Jar 包扫描器(JarScanner)添加到ServletContext属性,属性名为org.apache.tomcat.JarScanner
    17. 合并ServletContext的初始化参数和Context组件中的ApplicationParameter ,合并原则,ApplicationParameter配置则可以覆盖,那么只有
      当ServletContext 没有相关的参数或者相关的参数为空时添加,如果配置为不可覆盖 ,则强制添加 ,此时即ServletContext没有相关的参数或者相关的参数也不
      会生效。
    18. 启动添加到当前Context的ServletContainerInitializer ,该类的实例具体由ContextConfig查找并添加,具体过程见下一节讲解,该类主要
      用于可编程的方式添加到Web应用的配置,如Servlet ,Filter 等。
    19. 实例化应用监听器(ApplicationListener),分为事件监听器(ServletContextAttributeListener,ServletRequestAttributeListener,
      ServletRequestListener,HttpSessionIdListener,HttpSessionAttributeListener)以及生命周期监听器(HttpSessionListener,ServletContextListener)
      这些监听器可以通过Context部署描述文件,可编程方式(ServletContainerInitializer)或者Web.xml添加,并且触发ServletContextListener.contextInitialized 。
    20. 检测未覆盖的HTTP方法的安全约束
    21. 启动会话管理器。
    22. 实例化FilterConfig(ApplicationFilterConfig),Filter,并调用Filter.init初始化 。
    23. 对于loadOnStartup >= 的Wapper ,调用Wrapper.load() ,该方法负责实例化Servlet,并调用Servlet.init进行初始化 。
    24. 启动后台定时处理线程,只有当backgroundProcessorDelay > 0 时启动,用于监控守护文件的变更等,当backgroundProcessorDelay <= 0 时
      表示Context的后台任务由上线容器(Host)调度
    25. 发布正在运行的JMX通知
    26. 调用WebResourceRoot.gc()释放资源 (WebResourceRoot加载资源时,为了提高性能会缓存某些信息,该方法用于清理这些资源,如关闭JAR 文件 )
    27. 设置Context状态,如果启动成功,设置STARTING (其父类 LifecycleBase会自动将状态转换为STARTED),否则设置为FAILED 。

      通过上面的讲述,我们已经知道了StandardContext的整个启动过程 , 但如何解析Web.xml中的Servlet,请求映射,Filter等
    相关配置,这部分工作的具体由ContextConfig完成的。

      我们之前基本上将下图中红框里部分的启动过程分析完了,但Connector,Http11Protocol, JioEndPoint ,NioEndPoint 的启动又是怎样子的呢?
    在这里插入图片描述
      先来分析Server组件与Service组件

    Server组件与Service组件

      Server组件和Service组件是Tomcat 核心组件中最外层级的两个组件,Server组件可以看成Tomcat 的运行实例的抽象,而Service组件则可以看成Tomcat内的不同服务的抽象,如图5.1所示 ,Server组件包含若干Listener组件,GlobalNamingResources组件及若干个Service组件,Services组件则包含若干Connector 组件和Executor组件,由它们的结构相对比较简单且关系密切,因此将两者放到一起讲解
    在这里插入图片描述

    Server组件

      Server组件是代表整个Tomcat的Servlet容器,从server.xml配置文件中也可以看出它属于最外层组件,它的结构如图5.2所示,默认配置了个监听器组件,每个监听器负责各自的监听任务处理, GlobalNamingResource组件通过JNDI提供统的命名对象访问接口,它的使用范围是整个Server,ServerSocket组件监听某个端口是否有SHUTDOWN命令,一旦接收到则关闭Server, 即关闭Tomcat,Service 组件是另外一个核心组件,它的结构很复杂,也包含了更多的其他的核心组件。
      作为Tomcat最外层的核心组件,Server组件的作用主要有以下的几个。

    1. 提供了监听机制,用于在Tomcat整个生命周期中对不同的事件进行处理。
    2. 提供了Tomcat容器全局的命名资源实现。
    3. 监听某个端口以接收SHUTDOWN命令 。

    在这里插入图片描述
    看一下tomcat关于Server的xml结构

    
      
      
      
      
      
      
      
      
    
    
    
    
    生命周期监听器

      Tomcat整个生命周期存在很多的阶段,比如初始化前,初始化中, 初始化后,启动前,启动中,启动后,停止前,停止中,停止后,销毁前,销毁中,销毁后等, 为了在Server组件中的某个阶段执行某些逻辑,于是提供了监听器机制同,在Tomcat中实现一个生命周期监听器很简单,只要实现了LifecycleListener接口即可,在LifecycleEvent方法对感兴趣的生命周期事件进行处理。

    1. AprLifecycleListener监听器
      有时候,Tomcat会使用ApR本地库进行优化,通过JNI方式调用本地库能大幅度的对静态文件 处理能力,AprLifecycleListener监听器对初始化前的事件和销毁后的事件感兴趣,在Tomcat初始化前, 该监听器会尝试初始化APR库 , 假如能初始化成功,则会使用APR接收客户端处理请求,在Tomcat销毁后,衰亡人监听器会做APR的清理工作 。

    2. JasperListener监听器
      在Tomcat初始化前该监听器会初始化Jasper组件,Jasper是Tomcat的JSP编译器核心引擎,用于在Web应用启动前初始化Jasper。

    3. JreMemoryLeakPreventionListener
      该监听器主要提供解决JRE内存泄漏和锁文件的一种措施,该监听器会在Tomcat初始化时使用系统类加载器先加载一些类和设置缓存的属性,以避免内存泄漏和锁文件 。 先看JRE内存泄漏问题,内存泄漏的根本原因在于当垃圾回收器要回收时无法回收该被回收的对象,假如一个待回收的对象被另外一个生命周期很长的对象引用,那么这个对象将无法回收。 其中一种JRE内存泄漏是因为上下文类加载器导致的内存泄漏,在JRE库中某些类在运行时会以单例对象的形式存在,并且它们会存在很长的一段时间,基本上是从Java程序启动到关闭,JRE库的这些类使用上下文类加载器进行加载,并且保留了上下文类加载器的引用,所以将导致被引用的类加载器无法被回收,而Tomcat在重加载一个Web应用时正是通过实例化一个新的类加载器来实现的,旧的类加载器无法被垃圾回收器回收,导致内存泄漏,如图5.3 所示 , 某上下文类加载器为WebappClassLoader的线程加载JRE的DriverManager类, 此过程将导致WebappClassLoader被引用,后面该WebappClassLoader将无法被回收,发生内存泄漏。
      在这里插入图片描述
      另外一种JRE内存泄漏是因为线程启动另外一个线程并且新线程无止境的执行,在JRE库中存在某些类,当线程加载它时,它会创建一个新线程并且执行无限循环,新线程上下文类加载器会继承你线程上下文类加载器, 所以新线程包含了上下文类加载器应用,导致该类无法回收,最终导致内存泄漏,如图5.4 所示,某上下文类加载器为WebappclassLoader的线程加载JRE的Disposer类,此时该线程会创建一个新的线程,新线程的上下文类加载器为Webappclassloader,随后新线程将进入一个无限循环的执行中,最终将Webappclassloader将无法回收发生内存泄漏 。
      在这里插入图片描述
      可以看到,JRE内存泄漏与线程上下文类加载器有很大的关系,为了解决JRE内存泄漏,尝试让系统类加载器加载这些特殊的JRE库类, Tomcat中即使用了JreMemoryLeakPreventionListener监听器来做这些事情,主要代码如下 。

    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    
        try
        {
            // Use the system classloader as the victim for all this
            // ClassLoader pinning we're about to do.
            Thread.currentThread().setContextClassLoader(
                    ClassLoader.getSystemClassLoader());
     if (appContextProtection && !JreCompat.isJre8Available()) {
                ImageIO.getCacheDirectory();
            }
    
            // Trigger the creation of the AWT (AWT-Windows, AWT-XAWT,
            // etc.) thread
            if (awtThreadProtection && !JreCompat.isJre9Available()) {
              java.awt.Toolkit.getDefaultToolkit();
            }
    
            // Trigger the creation of the "Java2D Disposer" thread.
            // See https://bz.apache.org/bugzilla/show_bug.cgi?id=51687
            if(java2dDisposerProtection) {
                try {
                    Class.forName("sun.java2d.Disposer");
                }
                catch (ClassNotFoundException cnfe) {
                    // Ignore this case: we must be running on a
                    // non-Sun-based JRE.
                }
            }
    
    

      在Tomcat启动时, 先将当前线程的上下文类加载器设置为系统类加载器, 再执行DriverManager.getDrivers()和Class.forName(“sun.java2d.Disposer”),即会加载这些类, 此时的线程上下文为系统类加载器,加载完这些特殊的类后再将上下文类加载器还原,此时,如果Web应用使用到这些类,由于它们已经加载到系统类加载器中, 因此重启Web应用时不会存在内存泄漏,除了上面两个类之外,JRE还有其他类也存在内存泄漏的可能,如javax.imageio.ImageIO,java.awt.Toolkit,sun.misc.GC,javax.security.auth.Policy, javax.security.auth.login.Configuration,java.security.Security,javax.xml.parsers.DocumentBuilderFactory,com.sun.jndi.ldap.LdapPoolManager等。

      接着讨论锁文件问题,锁文件的情景主要由URLConnection默认的缓存机制导致的, 在Windows系统下当使用URLConnection的方式读取本地Jar包里面的资源时, 它会将资源内存缓存起来,这也就导致了该Jar包被锁,此时,如果进行重新部署将会失败,因为被锁的文件无法删除 。
    URL url = new URL(“jar:file//dummy.jar!/”);
    URLConnection conn = url.openConnection();
    conn.setDefaultUseCaches(false);

      在Tomcat启动时, 实例化一个URLConnection,然后通过setDefaultUseCaches(false)设置成默认不缓存,这样后面使用URLConnection将不会因为缓存而锁问题。

    1. GlobalResourcesLifecycleListener监听器。
      该监听器主要负责实例化Server组件时面的JNDI资源的MBean, 并提交给JMX管理器,此时监听器对生命周期内的启动事件和停止事件感兴趣,它会在启动时为JNDI创建MBean, 而在停止时销毁MBean .

    2. TheadLocalLeakPreventionListener监听器。
      该监听器主要解决TheadLocal的使用可能带来的内存泄漏问题, 该监听器会在Tomcat启动后将监听器的Web应用重新加载到监听器注册到每个Web应用上,当Web应用重新加载时,该监听器会将所有的工作线程销毁并再创建,以避免TheadLocal引起的内存泄漏。
      ThreadLocal引起的内存泄漏的经典场景是Web应用的重新加载,如图5.5所示 。 当Tomcat启动后,对客户端请求处理由专门的线程负责,线程池中的线程生命周期一般都比较长,假如Web应用中使用了ThreadLocal保存AA对象,而且AA类由Webappclassloader加载,那么它就可以看成线程引用了,AA对象,Web应用重加载是通过重新实例化一个WebappclassLoader类加载器来实现, 由于线程一直未销毁,旧的WebappClassLoader将无法被回收,导致内存泄漏 。
      在这里插入图片描述
      解决了ThreadLocal 内存泄漏最彻底的就是当Web应用加载时, 把线程池内的所有线程销毁并重新创建,这样就不会发生线程引用某些对象的问题了, 如图5.6所示, Tomcat中处理ThreadLocal内存泄漏的工作其实主要就是销毁线程池原来的线程,然后创建新的线程,这分两步做,第一步先将任务队列堵住,不让新的任务进来,第二步将线程池中所有的线程停止 。
      在这里插入图片描述
      TheadLocalLeakPreventionListener监听器的工作就是当应用Web应用重新加载时销毁线程池中所有的线程并重新创建新的线程,以避免ThreadLocal内存泄漏 。

    3. NamingContextListener监听器
      该监听器主要负责Server组件内全局命名资源在不同的生命周期的不同的操作,在Tomcat启动时创建命名资源,绑定命名资源,在Tomcat停止前绑定命名资源,反注册MBean。

    全局命名资源

      Server组件包含了一个全局命名资源,它提供了命名对象通过ResourceLink可以给所有的Web应用使用,它可以包含以下对象描述命名资源 。

    1. ContextResources
    2. ContextEjb
    3. ContextEnvironment
    4. ContextLocalEjb
    5. MessageDestinationRef
    6. ContextResourceEnvRef
    7. ContextResourceLink
    8. ContextService。

      在Tomcat启动初始化时, 通过Digester框架将server.xml的描述映射到对象,在Server组件中创建NamingResources和NamingContextListener两个对象,监听器将在启动初始化时利用ContextResources里面的属性创建命名上下文,并且组织成树状。
      Tomcat启动初始化时, 通过Digester框架将server.xml的描述映射到对象,在Server组件中创建NamingResources和NamingContextListener两个对象,监听器将在启动初始化时利用ContextResources里面的属性创建命名上下文,并且组织成树状。

      如图5.7所示不,Tomcat启动时将server.xml配置文件里面的GlobalNamingResources节点通过Digester框架映射到一个NamingContextListener监听器,这个监听器负责在Tomcat初始化启动期间完成对命名资源的创建,组织,绑定等工作,使之符合JNDI的标准,而创建,组织,绑定是根据NamingResources对象描述的资源属性进行处理的,绑定的路径由配置文件的Resource节点的Naming属性决定,name即为JNDI对象的分支节点,例如,name为"jdbc/myDB",那么此对象就可以通过"java:jdbc/myDB",访问,而树的位置应该是jdbc/myDB,但是Web应用中无法直接访问全局命名资源的, 因为要访问全局命名资源,所以这些资源必须都放到Server组件中。
    在这里插入图片描述

    Service组件

      Service组件是一个简单的组件,如图5.9所示 , Service组件是若干个Connector组件和Executor组件组合而成的一个概念, Connector 组件负责监听某个端口的客户端请求, 不同的端口对应不同的Connector,Executor组件在Service抽象层提供了线程池,让Service下的组件可以共用线程池,默认情况下,不同的Conector组件会自己创建线程池来使用, 而通过Service组件下的Executor组件则可以实现线程池的共享,每个Connector 组件都使用Service组件下的线程池。 每个Connector组件都可以使用Service组件下的线程池, 除了Connector组件之外,其他组件都可以使用。
      既然提及线程池, 就来看看Tomcat中池的实现。
      池技术对我们来说非常的熟悉的一个概念, 它的引入是为了在某些场景下提高系统某些关键节点性能和效率,最典型的例子就是数据库连接池,数据库连接的建立和销毁是很耗时资源的操作,为了查询数据库中某些记录,最原始的一个过程的建立连接,发送查询语句,返回查询结果,销毁连接,例如仅仅是一个很简单的查询语句,那么建立连接与销毁链接两个步骤已经占用所有的时间消耗绝大部分,效果显然让人无法接受,于是想尽可能的减少创建和销毁连接的操作,连接相对于查询是无状态的, 不必每次查询都重新生成和销毁连接,我们可以以维护这些通道以供下一次查询或其他操作使用,维护这些管道的工作就交给了池。
    在这里插入图片描述
      线程池也是类似于数据库连接的一种池, 而仅仅是把池里的对象换成了线程,线程是为了多任务而引入的概念,每个线程在任意的时刻执行一个任务,例如多个任务要并发执行,则要用到多线程技术,每个线程都有自己的生命周期,以创建为始, 销毁为未, 如图5.10所示,两个线程的运行阶段占整个生命周期的比重不同,第一种情形是运行阶段所占比重小的线程,如图5.10所示 , 可以认为其运行效率低, 第二种情形是运行阶段所占的比重大的线程,如图5.10(b),可以认为其运行效率高,如不使用线程池, 则大多数的情况下比较符合第一种线程运行模式,为了提高运行效率,引入了线程池,它的核心思想就是把运行阶段尽量拉长,对于每个任务的到来 , 不是重复建立,销毁线程, 而是重复利用之前建立的线程执行任务 。

    在这里插入图片描述

      其中一种方案是在系统启动时建立一定数量的线程并做好线程维护工作,一旦有任务到来,即从线程池中取出一条空闲的线程执行任务,原理听起来比较清晰,但现实中对一条线程一旦调用了start方法,就将运行任务直到任务fpdn,随后JVM将线程对象进行GC回收,如此一来线程不就销毁了不? 是的,所以需要换种思维角度,让这些线程启动后通过一个无限循环来执行指定的任务, 下面将所有的重点讲解如何实现线程池。
      一个线程池的属性的起码包含初始化线程数量,线程数组,任务队列,初始化线程数量线程池初始化的线程数,线程数组保存了线程中的所有线程,任务队列指添加到线程池中等待处理的所有任务,如图5.11所示,线程池里有两个线程,池里的线程的工作就是不断的循环检测任务队列中所需要执行的任务,如果有, 则处理并移出任务队列,于是,可以说线程池中的所有线程任务就是不断的检测任务队列中并不断的执行队列中的任务 。
    在这里插入图片描述

      看一个简陋的线程池的实现, 使用线程池时只须要实例化一个对象,构造函数就会创建一相相应的数量的线程并启动线程,启动线程无限循环的检测任务队列,执行方法execute()仅仅把任务添加到任务队列中, 需要注意的一点是, 所有的任务都必须实现Runnable接口,这是线程池的任务队列与工作线程的约定,JUC工具包的作者Doug Lea当时如此规定,工作线程检测任务队列并调用队列的run()方法,假如你自己重写一个线程池, 就完全可以自定义一个不一样任务接口,一个完善的线程池需要提供启动,销毁,增加工作线程的策略, 对最大工作线程数,各种状态的获取等操作,而且工作线程不可能始终做无用的循环,需要对任务队列使用wait() ,notify优化,或者将任务队列改用为阻塞队列 。

    Connector组件

      Connector(连接器)组件是Tomcat最核心的两个组件之一,主要的职责是负责接收客户端连接和客户端请求的处理加工,每个Connector都将指定一个商品进行监听,分别负责对请求报文解析和对响应报文进行组装,解析过程生成Request对象,而组装过程则涉及Response对象,如果将Tomcat整体比作是一个巨大的城堡,那么Connector组件就是城堡的大门, 每个人要进入城堡就必须通过城堡的大门, 每个城门代表了不同的通道,将深入剖析Tomcat 的Connector组件

    在这里插入图片描述
       Protocol组件是协议的抽象,它将不同的通信协议处理进行了封装,比如Http协议和AJP协议,EndPoint是接收端的抽象,由于使用了不同的I/O模式,因此存在多种类型的EndPoint,如Bio模式,使用JIoEndPoint,Nio模式则使用NioEndPoint和本地库I/O 模式AprEndPoint ,Acceptor是专门用于接收客户端连接的接收器组件,Executor则是处理客户端请求的线程池, Connector可能是使用Service组件的共享线程池, 也可能是Connector自己的私有线程池, Processor组件是处理客户端请求的处理器, 不同的协议和不同的I/O 模式都有不同的处理方式,所以存在不同的类型的Processor 。
      Mapper组件可以称为路由器,它提供了对客户端请求URL的映射功能,即可以通过它将请求转发到对应的Host组件,Context组件,Wrapper组件以进行处理并响应客户端,也就是我们常常说的将某客户端请求发送到虚拟主机上的某个Web应用的某个Servlet。
      CoyoteAdaptor组件是一个适配器, 它负责将Connector组件和Engine容器适配连接起来,把接收到客户端请求报文解析生成的请求和响应对象Response传递到Engine容器,交由容器处理。
      目前Tomcat支持两种Connector,分别支持Http协议的Connector ,用于接收和发送Http ,AJP 协议请求, Connector 组件的不同体现在其协议及I/O模式的不同,所以Connector 包含的Protocol组件类型为Http11Protocol, Http11NioProtocol,Http11AprProtocol,AjpProtocol,AjpNioProtocol和AjpAprProtocol。
      HTTP Connector 所支持的协议版本为HTTP/1.1 和HTTP/1.0无须显式配置HTTP的版本,Connector 会自动适配版本,每个Connector 实例对应一个端口,在同个Service实例内可以配置若干Connector实例,端口必须不同,但协议可以相同,HTTP Connector 包含的协议处理组件有Http11Protocol(Java BIO 模式 ),Http11NioProtocol(Java NIO 械)和Http11AprProtocol(APR/native模式 ),Tomcat 启动时根据server.xml 的 节点配置I/O模式,BIO模式org.apache.coyote.http11protocol ,NIO模式为org.apache.coyote.http11.Http11NioProtocol,APR/native模式为org.apache.coyote.http11.Http11AprProtocol。
      AJP Connector 组件用于支持AJP 协议通信,当我们想将Web 应用中包含的静态内容交给Apache 处理时, Apache 与Tomcat 之间的通信则可以使用AJP 协议,目前标准的协议为AJP/1.3 ,AJP Connector 包含的协议处理组件有AjpProtocol(Java BIO模式 ),AjpNioProtocol(Java NIO模式)和AjpAprProtocol(APR/native模式),Tomcat启动时根据server.xml的节点配置I/O模式,BIO模式为org.apache.coyote.ajp.AjpProtocol,NIO模式为org.apache.coyote.ajp.AjpNioProtocol,APR/native模式为org.apache.coyote.ajp.AjpAprProtocol。

      Connector也在服务端提供了SSL安全通道的支持,用于客户端以HTTPS方式访问,可以通过配置server.xml的节点SSLEnabled 属性开启。
      在BIO 模式下, 对于每个客户端的请求连接都将消耗线程池里面的一条连接,直到整个请求响应完毕,此时,如果有很多的请求几乎同时到达Connector,当线程池中的空闲线程用完后,则会创建新的线程,直到达到线程池的最大线程数,但如果此时还有更多的请求到来,虽然线程池已经处理不过来, 直到达到线程池的最大线程数, 但如果此时还有更多的请求到来,虽然线程池已经处理不过来, 担操作系统还是会将客户端接收到一个队列里, 这个队列的大小通过SocketServer设置backlog而来, 如果还是有再多的请求过来, 队列已经超过SocketServer的backlog大小,那么连接直接被拒绝掉,客户端接收到Connector Refused报错。
      在NIO模式下, 则是所有客户端的请求连接先由一个接收线程接收,然后由若干(一般为CPU 个数)线程轮询读写事件,最后将具体的读写操作交由线程池处理, 可以看到,以这种方式,客户端连接不会在整个请求响应过程中占用连接池内的连接,它可以同时处理比BIO模式多得多的客户端连接数,此种模式能承受更大的并发,机器资源使用效率高得多,另外,APR/native模式也是NIO模式,它直接用本地代码实现NIO模式 。

      接下来看Connector的初始化,启动相关处理。
    在这里插入图片描述
    在这里插入图片描述
      解析到connector标签时,处理方法如下

    public void begin(String namespace, String name, Attributes attributes)
            throws Exception {
        // 先获取上层父节点Service
        // 判断Connector节点是否配置了executor属性,如果配置了,则根据executor名字从父节点service中获取到Executor对象。
        // 根据Connector节点上配置的protocol协议名来初始化Connector
        // Connector初始化完成之后,获取对应的ProtocolHandler对象,将executor对象设置进去
    
        // 从这里可以看出来,如果有多个Connector节点,每个节点可以使用不同的executor,也就是线程池,也可以公用,根据名字来。
    
        Service svc = (Service)digester.peek();
        Executor ex = null;
        if ( attributes.getValue("executor")!=null ) {
            ex = svc.getExecutor(attributes.getValue("executor"));
        }
        Connector con = new Connector(attributes.getValue("protocol"));
        if ( ex != null )  _setExecutor(con,ex);
    
        digester.push(con);
    }
    

      看Connector的构造方法

    public Connector(String protocol) {
    	// 设置协议 
        setProtocol(protocol);
        // Instantiate protocol handler
        try {
            Class clazz = Class.forName(protocolHandlerClassName);
            this.protocolHandler = (ProtocolHandler) clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        }
    }
    

      看设置协议处理类方法

    public void setProtocol(String protocol) {
    	// 因为我的是mac环境
    	// 所以在加载"tcnative-1", "libtcnative-1" 两个文件时报错,所以
    	// 我的环境 AprLifecycleListener.isAprAvailable() = false
        if (AprLifecycleListener.isAprAvailable()) {
            if ("HTTP/1.1".equals(protocol)) {
                setProtocolHandlerClassName
                ("org.apache.coyote.http11.Http11AprProtocol");
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                ("org.apache.coyote.ajp.AjpAprProtocol");
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
            } else {
                setProtocolHandlerClassName
                ("org.apache.coyote.http11.Http11AprProtocol");
            }
        } else {
            if ("HTTP/1.1".equals(protocol)) {
                setProtocolHandlerClassName
                ("org.apache.coyote.http11.Http11Protocol");  // BIO
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                ("org.apache.coyote.ajp.AjpProtocol");
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol); // org.apache.coyote.http11NIOProxot
            }
        }
    }
    

      因此,的协议处理类是org.apache.coyote.http11.Http11Protocol,因此在Connector的构造函数中,protocolHandler为Http11Protocol对象。看Http11Protocol的构造函数。

    HTTP 阻塞模式协议,Http11Protocol

      Http11Protocol 表示阻塞式的HTTP协议的通信,它包含从套接字的连接接收,处理,响应,客户端的整个过程 , 它主要包含JIoEndPoint组件和Http11Processor组件,启动时,JIoEndpoint组件将启动某个商品的监听,一个请求到来后将被扔进线程池,线程池进行任务处理,处理过程中将通过协议解析器Http11Processor组件对HTTP协议解析,并且通过适配器Adapter匹配到指定的容器进行处理,以及响应客户端,当然,整个过程相对比较复杂,涉及很多的组件,下面会对此深入的分析,HTTP 阻塞模式和协议整体结构如图6.2所示 。
    在这里插入图片描述

    public Http11Protocol() {
        endpoint = new JIoEndpoint();
        cHandler = new Http11ConnectionHandler(this);
        ((JIoEndpoint) endpoint).setHandler(cHandler);
        setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
        setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
        setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
    }
    

      这里有一个非常重要的对象endpoint的初始化,也就是说,Endpoint是什么呢? Tomcat的IO监听由Endpoint完成, 具体BIO是具体到BIO是JIoEndpoint实现。

    套接字接收终端-JIOEndpoint

      需要一个组件负责启动某个端口监听客户端的请求,负责接收套接字连接,负责提供一个线程池供系统处理接收到套接字连接,负责对连接数控制,负责安全与非安全套接字连接实现等, 这个组件就是JioEndpoint,它所包含的组件可以用图6.3所示,其中包含连接控制器LimitLatch,Socket接收器的Acceptor,套接字工厂ServerSocketFactory,任务执行器Executor,任务处理器SocketProcessor,下面将对每个组件结构也作用进行解析。
    在这里插入图片描述

    6.1.2 HTTP阻塞处理器 Http11Processor
      Http11Processor组件提供了对HTTP协议通信的处理, 包括对套接字的读写和过滤,对HTTP协议的解析以及封装成请求对象,HTTP协议响应对象的生成等操作,其整体结构如图6.11所示 。 其中涉及更多的细节将在下面展开介绍 。

    在这里插入图片描述

      NIO是由NioEndpoint实现, 默认Http11Protocol协议处理器用的就是JIoEndpoint,而如果要使用NioEndpoint 怎么办呢?可以在server.xml中对   org.apache.coyote.http11.Http11NioProtocol”
    connectionTimeout=“20000”
    redirectPort=“8443”/>

      而Http11NioProtocol的构建函数实现如下

    public Http11NioProtocol() {
        endpoint=new NioEndpoint();
        cHandler = new Http11ConnectionHandler(this);
        ((NioEndpoint) endpoint).setHandler(cHandler);
        setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
        setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
        setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
    }
    
    HTTP非阻塞模式协议-Http11NioProtocol

      Http11NioProtocol表示非阻塞模式的HTTP协议通信,它包含从套接字连接接收,处理请求, 响应客户端整个过程, 它主要包含NioEndPoint组件和Http11NioProcessor组件,启动时NioEndPoint组件将启动某个端口的监听,一个连接到来后将被注册到NioChannel队列中, 由Poller(轮询器)负责检测通道的读写事件,并在创建任务后扔进线程池中,线程池进行任务处理,线程池进行任务处理,处理过程中将通过协议解析器Http11NioProcessor组件对Http协议解析,同时通过适配器(Adapter) 匹配到指定的容器进行处理响应客户端,整体结构如图6.32所示 。

      因为Connector同样实现了Lifecycle接口,因此和其他容器一样,遵循生命周期事件,这里就不赘述,都是先init()方法,再启动。先来看Connector的初始化方法 。

    protected void initInternal() throws LifecycleException {
    
        super.initInternal();
    
        // Initialize adapter 
     	// 创建连接器与容器的桥梁
        adapter = new CoyoteAdapter(this);
        // 协议处理器中设置adapter
        protocolHandler.setAdapter(adapter);
    
        // Make sure parseBodyMethodsSet has a default 
        if (null == parseBodyMethodsSet) {
            setParseBodyMethods(getParseBodyMethods());
        }
    
        if (protocolHandler.isAprRequired() &&
                !AprLifecycleListener.isAprAvailable()) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerNoApr",
                            getProtocolHandlerClassName()));
        }
    
        try {
        	// 协议初始化
            protocolHandler.init();
        } catch (Exception e) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
        }
    
        // Initialize mapper listener 
        mapperListener.init();
    }
    

      看协议处理器初始化做了哪些事情 。

    public void init() throws Exception {
        if (getLog().isInfoEnabled()) {
            getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
        }
    
        if (oname == null) {
        	// 将协议处理器注册到jmx 
            // Component not pre-registered so register it
            oname = createObjectName();
            if (oname != null) {
                Registry.getRegistry(null, null).registerComponent(this, oname, null);
            }
        }
    
        if (this.domain != null) {
            try {
            	// 将endpoit注册到jmx 
                tpOname = new ObjectName(domain + ":" +
                        "type=ThreadPool,name=" + getName());
                Registry.getRegistry(null, null).registerComponent(endpoint,
                        tpOname, null);
            } catch (Exception e) {
                getLog().error(sm.getString(
                        "abstractProtocolHandler.mbeanRegistrationFailed",
                        tpOname, getName()), e);
            }
            rgOname=new ObjectName(domain +
                    ":type=GlobalRequestProcessor,name=" + getName());
            Registry.getRegistry(null, null).registerComponent(
                    getHandler().getGlobal(), rgOname, null );
        }
    
        String endpointName = getName();
        endpoint.setName(endpointName.substring(1, endpointName.length()-1));
    
        try {
        	// endpoint启动
            endpoint.init();
        } catch (Exception ex) {
            getLog().error(sm.getString("abstractProtocolHandler.initError",
                    getName()), ex);
            throw ex;
        }
    }
    

      接下来看endpoint启动又做了哪些事情 。

    public abstract void bind() throws Exception;
    
    public final void init() throws Exception {
        testServerCipherSuitesOrderSupport();
        if (bindOnInit) {
            bind();
            bindState = BindState.BOUND_ON_INIT;
        }
    }
    

      感觉调用其绑定方法。但因为绑定方法是抽象方法,因此需要根据不同的endpoint做区分,先进入JIoEndpoint的bind()方法 。

    public void bind() throws Exception {
    	// Initialize thread count defaults for acceptor
    	// 使用的Acceptor线程的个数
        if (acceptorThreadCount == 0) {
            acceptorThreadCount = 1;
        }
        // Initialize maxConnections
        if (getMaxConnections() == 0) {
            // User hasn't set a value - use the default
            // 本来maxConnections默认值是10000的,但是因为是bio,所以需要取线程池最大线程数,默认为200
            //  maxThreads :Tomcat是一款多线程Servlet容器,每个请求都会分配一个线程进行处理,因此Tomcat采用线程池来提高处理性能,maxThreads
         	// 用于指定Connector 创建的请求处理线程的最大数目,该属性决定了可以并行处理的请求最大数目,即并发上限,当并发请求数超过maxThreads
         	// 时,多余的请求只能排队等待,增大该属性可以提高Tomcat的并发处理能力,但是这也意味着会占用更多的系统资源,因此如果非常大反而会降低
         	// 性能,甚至导致Tomcat崩溃。
            setMaxConnections(getMaxThreadsWithExecutor());
        }
    
        if (serverSocketFactory == null) {
            if (isSSLEnabled()) {
                serverSocketFactory =
                    handler.getSslImplementation().getServerSocketFactory(this);
            } else {
                serverSocketFactory = new DefaultServerSocketFactory(this);
            }
        }
        if (serverSocket == null) {
            try {
                if (getAddress() == null) {
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog());
                } else {
                    // serverSocket会不停的接收客户端连接,getBacklog()
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog(), getAddress());
                }
            } catch (BindException orig) {
                String msg;
                if (getAddress() == null)
                    msg = orig.getMessage() + " :" + getPort();
                else
                    msg = orig.getMessage() + " " +
                            getAddress().toString() + ":" + getPort();
                BindException be = new BindException(msg);
                be.initCause(orig);
                throw be;
            }
        }
    }
    

      接下来看NioEndpoint的bind()方法

    public void bind() throws Exception {
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
        serverSock.socket().bind(addr,getBacklog());
        serverSock.configureBlocking(true); //mimic APR behavior
        serverSock.socket().setSoTimeout(getSocketProperties().getSoTimeout());
    
        // Initialize thread count defaults for acceptor, poller
        if (acceptorThreadCount == 0) {
            // FIXME: Doesn't seem to work that well with multiple accept threads
            acceptorThreadCount = 1;
        }
        if (pollerThreadCount = 0) {
            //minimum one poller thread
            pollerThreadCount = 1;
        }
        stopLatch = new CountDownLatch(pollerThreadCount);
    
        // Initialize SSL if needed
        if (isSSLEnabled()) {
            SSLUtil sslUtil = handler.getSslImplementation().getSSLUtil(this);
    
            sslContext = sslUtil.createSSLContext();
            sslContext.init(wrap(sslUtil.getKeyManagers()),
                    sslUtil.getTrustManagers(), null);
    
            SSLSessionContext sessionContext =
                sslContext.getServerSessionContext();
            if (sessionContext != null) {
                sslUtil.configureSessionContext(sessionContext);
            }
            // Determine which cipher suites and protocols to enable
            enabledCiphers = sslUtil.getEnableableCiphers(sslContext);
            enabledProtocols = sslUtil.getEnableableProtocols(sslContext);
        }
    
        if (oomParachute>0) reclaimParachute(true);
        selectorPool.open();
    }
    

      关于绑定方法,也只是初始化一些变量,接下来看Connector的start()方法 。

    Connector启动
    1. Bootstrap通过反射调用了Catalina的start()方法
      在这里插入图片描述
    2. StandardServer启动
      在这里插入图片描述
    3. StandardEngine启动后,接着Connector启动。
      在这里插入图片描述
      Connector的启动方法
    protected void startInternal() throws LifecycleException {
        // Validate settings before starting
        if (getPort() < 0) {
            throw new LifecycleException(sm.getString(
                    "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
        }
    
        setState(LifecycleState.STARTING);
    
        try {
            protocolHandler.start();
        } catch (Exception e) {
            String errPrefix = "";
            if(this.service != null) {
                errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
            }
    
            throw new LifecycleException
            (errPrefix + " " + sm.getString
                    ("coyoteConnector.protocolHandlerStartFailed"), e);
        }
    
        mapperListener.start();
    }
    

      而Connector启动分两部分,第一部分是协议处理器启动,第二部分为mapperListener启动,之前我们知道协议处理器为org.apache.coyote.http11.Http11Protocol,接下来看协议处理类的启动。

    public void start() throws Exception {
        if (getLog().isInfoEnabled())
            getLog().info(sm.getString("abstractProtocolHandler.start",
                    getName()));
        try {
            endpoint.start();
        } catch (Exception ex) {
            getLog().error(sm.getString("abstractProtocolHandler.startError",
                    getName()), ex);
            throw ex;
        }
    }
    

      协议的启动又是endpoint启动,endpoint有三个实现类。
    在这里插入图片描述
      先看JIoEndPoint启动

    public void startInternal() throws Exception {
    
        if (!running) {
            running = true;
            paused = false;
    
            // Create worker collection
            // 如果配置文件里没有配置线程池,那么将创建一个默认的
            if (getExecutor() == null) {
                createExecutor();
            }
    
            initializeConnectionLatch();
    
            startAcceptorThreads();
    
            // Start async timeout thread
            Thread timeoutThread = new Thread(new AsyncTimeout(),
                    getName() + "-AsyncTimeout");
            timeoutThread.setPriority(threadPriority);
            timeoutThread.setDaemon(true);
            timeoutThread.start();
        }
    }
    

      看初始化LimitLatch代码 。

    protected LimitLatch initializeConnectionLatch() {
        if (maxConnections==-1) return null;
        if (connectionLimitLatch==null) {
            connectionLimitLatch = new LimitLatch(getMaxConnections());
        }
        return connectionLimitLatch;
    }
    
    1. 连接数控制器-LimitLatch

      作为Web服务器,Tomcat对于每个客户端的请求将给予处理响应,但对于一台机器而言,访问请求的总流量有高峰期且服务器有物理极限,为了保证Web 服务器不被冲垮,我们需要采取一些保护措施,其中一种有效的方法就是采取流量控制,需要稍微的说明的是,此处的流量更多的是指套接字的链接数, 通过控制套接字连接数来控制流量,如图6.4所示,它就像在流量入口增加了一道闸门,闸门的大小决定了流量的大小,一旦流量达到最大限制,将关闭闸门,停止接收,直到有空闲的通道 。
    在这里插入图片描述
      Tomcat流量控制器通过AQS并发框架来实现的, 通过AQS实现起来更加具有灵活性和定制性, 思路是先初始化同步器的最大限制值,然后每接收一个套接字就将计数变量累加1,每关闭一个套接字将计数变量减1,如此一来, 一旦计数变量值大于最大限制,则AQS机制将会接收线程阻塞,而停止对套接字的接收,直到某些套接字处理完关闭后重新唤起接收线程住下接收套接字,我们把思路拆成两部分,一是使用AQS创建一个支持计数的控制器,另外一个是将此控制器嵌入处理流程中。

    1. 控制同步器, 整个过程根据AQS推荐的自定义同步器做法进行,但并没有使用AQS 自带的状态变量,而是另外引入一个AtomicLong类型的count变量用于计数,具体代码如下,控制器的主要通过countUpOrAwait和countDown两个方法实现计数器的加减操作,countUpOrAwait方法中,当计数超过最大限制值时则会阻塞线程,countDown方法则负责递减数字和唤醒线程。
    public class LimitLatch {
    
        private static final Log log = LogFactory.getLog(LimitLatch.class);
    
        private class Sync extends AbstractQueuedSynchronizer {
            private static final long serialVersionUID = 1L;
    
            public Sync() {
            }
    
            @Override
            protected int tryAcquireShared(int ignored) {
                long newCount = count.incrementAndGet();
                if (!released && newCount > limit) {
                    count.decrementAndGet();
                    return -1;
                } else {
                    return 1;
                }
            }
    
            @Override
            protected boolean tryReleaseShared(int arg) {
                count.decrementAndGet();
                return true;
            }
        }
    
        private final Sync sync;
        private final AtomicLong count;
        private volatile long limit;
        private volatile boolean released = false;
    
    
        public LimitLatch(long limit) {
            this.limit = limit;
            this.count = new AtomicLong(0);
            this.sync = new Sync();
        }
    
        public long getCount() {
            return count.get();
        }
    
        public long getLimit() {
            return limit;
        }
    
    
        public void setLimit(long limit) {
            this.limit = limit;
        }
    
    
        public void countUpOrAwait() throws InterruptedException {
            if (log.isDebugEnabled()) {
                log.debug("Counting up["+Thread.currentThread().getName()+"] latch="+getCount());
            }
            sync.acquireSharedInterruptibly(1);
        }
    
        public long countDown() {
            sync.releaseShared(0);
            long result = getCount();
            if (log.isDebugEnabled()) {
                log.debug("Counting down["+Thread.currentThread().getName()+"] latch="+result);
            }
            return result;
        }
    
        public boolean releaseAll() {
            released = true;
            return sync.releaseShared(0);
        }
    
    
        public void reset() {
            this.count.set(0);
            released = false;
        }
    
        public boolean hasQueuedThreads() {
            return sync.hasQueuedThreads();
        }
    
    
        public Collection getQueuedThreads() {
            return sync.getQueuedThreads();
        }
    }
    
    
    
    1. 对于流程嵌入控制器,伪代码如下,其中,首先初始化一个最大的限制数为200的连接控制器(LimitLatch),然后在接收套接字前尝试累加计数器或进入阻塞状态,接着接收套接字,对套接字的数据处理则交由线程池中的线程,它处理需要一段时间,假如这段时间又有了200个请求套接字,则200个请求会导致线程阻塞状态,而不再执行接收动作,唤醒的条件是线程中的工作线程处理完其中一个套接字并执行countDown操作,需要额外说明的是,当到达最大连接数时, 操作系统底层还是会继续接收客户端连接的, 但用户层已经不再接收,操作系统的接收队列长度默认为100,可以通过server.xml的节点的maxConnections属性进行调节,Tomcat BIO 模式下的LimitLatch 的限制数量与线程池的最大线程数密切相关,它们之间的比例是1:1 。
      LimitLatch limitLatch = new LimitLatch(200);
      创建serverSocket实例
      while(true){
      limitLatch.countUpOrAwait();
      Socket socket = serverSocket.accpet();
      socket 交由线程池处理, 处理完执行limitLatch.countDown();
      }

      接下来看启动Acceptor的实现

    protected final void startAcceptorThreads() {
        // 3. 根据acceptorThreadCount配置的数量,创建并启动org.apache.tomcat.util.net.JIoEndpoint.Acceptor线程,Acceptor实现
        // 了Runnable接口,负责轮询接收客户端请求(Socket.accept()) ,这些线程是单独启动的,并未放到线程池中,因此不会影响到请求并发处理
        // Acceptor还会检测Endpoint状态(如果处于暂停状态,则等待)以及最大链接数 
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];
    
        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());
            t.start();
        }
    }
    
    
    1. Socket接收器-Acceptor

      Acceptor主要职责就是监听是否有客户套接字连接并接收套接字,再将套接字交由任务执行(Executor)执行,它不断的从系统底层读取套接字,接收做尽可能少的处理, 最后扔进线程池, 由于接收线程默认只有一条, 因此这里强调做尽量少的处理,它对每次接收处理的时间长短可能会整个性能产生影响 。

      于是,接收器所做的工作都非常少且简单,仅仅维护了几个状态变量,负责流量的控制闸门的累加操作和ServerSocket的接收操作,设置接收的套接字的一些属性, 将接收到的套接字放入到线程池以及一些异常处理,其他需要较长的时间处理逻辑交给线程池, 例如对套接字底层的数据的读取,对HTTP 协议报文的解析及响应客户端的一些操作,这样处理有助于提升系统的处理响应性能,此过程如图6.5所示 。

    在这里插入图片描述
    用一段简化的伪代码表示接收器的处理过程 。
    public class Acceptor implements Runnable{
    public void run(){
    while(true){
      limitLatch.countUpOrAwait() ;//流量控制闸门信息量加1
      Socket socket = serverSocket.accept();
      设置一些套接字属性
      将接收到的套接字扔进线程池
    }
    }
    }

      具体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
                // 如果Endpoint仍然在运行,但是被暂停了,那么就无限循环,从而不能接受请求
                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();
    
                    Socket socket = null;  // bio,nio
                    try {
                        // Accept the next incoming connection from the server
                        // bio socket
                        // 此处是阻塞的,那么running属性就算已经被改成false,那么怎么进入到下一次循环呢?
                        socket = serverSocketFactory.acceptSocket(serverSocket);//
                        System.out.println("接收到了一个socket连接");
    
                    } catch (IOException ioe) {
                        countDownConnection();
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;
    
                    // Configure the socket
                    // 如果Endpoint正在运行并且没有被暂停,那么就处理该socket
                    if (running && !paused && setSocketOptions(socket)) {
                        // Hand this socket off to an appropriate processor
                        // socket被正常的交给了线程池,processSocket就会返回true
                        // 如果没有被交给线程池或者中途Endpoint被停止了,则返回false
                        // 返回false则关闭该socket
                        if (!processSocket(socket)) {
                            countDownConnection();
                            // Close socket right away
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        // Close socket right away
                        closeSocket(socket);
                    }
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (NullPointerException npe) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), npe);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }
    }
    
    1. 套接字工厂,ServerSocketFactory

      接收器Acceptor在接收连接的过程中, 根据不同的使用场合可能需要不同的安全级别,例如安全性较高的场景需要对消息加密后传输,而在另外一些安全性要求较低的场合则无须对消息加密,反映到应用层则是使用HTTP和HTTPS的问题。
      图6.6为HTTPS的组成层次图, 它在应用层添加了一个SSL/TLS协议,于是组成了HTTPS,简单来说, SSL/TLS协议为通信提供了以下服务 。

    a. 提供验证服务,验证会话内实体身份的合法性。
    b. 提供加密服务,强加密机制能保证通信过程中的消息不会被破译。
    c. 提供防篡改服务,利用Hash算法对消息进行签名,通过验证签名保证通信通信内容不被篡改。

    在这里插入图片描述
      Java为开发者提供了方便手段实现SSL/TLS协议,即安全套接字,它是套接字的安全版本,Tomcat作为Web服务器必须满足不同的安全级别的通道,HTTP使用了套接字,而HTTPS则使用SSLSocket,由于接收终端根据不同的安全配置需要产生不同的类别套接字,于是,引入了工厂模式处理套接字,即ServerSocketFactory工厂类, 另外,不同的厂商可以自己定制SSL的实现。

      ServerSocketFactory是Tomcat接收端的重要组件,先看看它的运行逻辑,Tomcat中有两个工厂类DefaultServerSocketFactory和JSSESocketFactory,它们都实现了ServerSocketFactory工厂,分别对应HTTP套接字通道与HTTPS套接字通道,假如机器的某商品使用了加密通道,则由JSSESocketFactory作为套接字工厂,反之则使用 DefaultSocketFactory作为套接字工厂,于是Tomcat中存在一个变量SSLEnabled用于标识是否使用加密通道,通过对此变量的定义就可以决定使用哪个工厂类, Tomcat提供了外部配置文件供用户自定义 。

      实际上,我们通过对server.xml进行配置就可以定义某个端口开放并指出是否使用安全通道,例如:


      

    HTTPS的协议对应的安全通道配置如下 。


    SSLEnabled=“true” />

      第一种配置告诉Tomcat开放8080端口并使用HTTP1.1协议进行非安全通信,第二种配置告诉Tomcat开放8443端口并使用HTTP1.1协议进行安全通信,其中使用安全协议是TLS协议,需要注意的是加粗字体SSLEnabled=“true”, 此变量值会在Tomcat启动初始化时读入自身的程序中, 运行时也正是通过此变量判断使用哪个套接字工厂,DefaultServerSocketFactory还是JSSESocketFactory。
      把ServerSocketFactory工厂组件引入后,整个结构图如图6.7所示 。

    在这里插入图片描述
      processSocket()方法的具体细节要在下一篇博客再来分析了。

    NioEndpoint启动

      接下来看NioEndpoint的启动

    public void startInternal() throws Exception {
    
        if (!running) {
            running = true;
            paused = false;
    
            // Create worker collection
            if ( getExecutor() == null ) {
                createExecutor();
            }
    
            // 初始化limitlatch,NioEndpoint默认最大连接数为10000
            initializeConnectionLatch();
    
            // Start poller threads
            // 用Poller线程来处理来处理io读写事件,线程的个数,如果是多核就是两个线程,如果单核就是一个线程
            // 1. NioEndpoint中的ServerSocketChannel是阻塞的,因此,仍然采用多线程并发接收客户端链接 。
            // 2. NioEndpoint根据pollerThreadCount配置的数量创建Poller线程,与Acceptor相同,Poller线程也是单独启动,不会占用请求处理的
            // 线程池,默认的Poller线程个数与JVM可使用的处理器相关。上限为2
            // 3. Accepor接收到新的链接后,将获得SocketChannel置为非阻塞,构造NioChannel对象,按照轮转法(Round-Robin)获取Poller
            // 实例,并将NioChannel注册到PollerEvent事件队列
            // 4. Poller负责为SocketChannel 注册读事件,接收到读事件后,由SocketProccesor完成客户端请求处理,SocketProcessor完成
            // 客户端请求处理,SocketProcessor的处理过程具体参见4.2节的说明
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; istartAcceptorThreads();
        }
    }
    

      先来看看线程池的创建逻辑

    public void createExecutor() {
        // 是否使用的是内部默认的线程池
        internalExecutor = true;
        TaskQueue taskqueue = new TaskQueue();
        TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
        // 默认的线程数量为10, max200
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
        taskqueue.setParent( (ThreadPoolExecutor) executor);
    }
    
    

    如果想了解线程池相关源码,可以去看我的这篇博客
    ThreadPoolExecutor&ScheduledThreadPoolExecutor 源码解析

    1. 任务执行器-Executor

      上一节提到的接收器Acceptor在接收到套接字后会有一系列的简单处理,其中将套接字放入线程池是最重要的一步,这里重点讲Tomcat 中用于处理客户端请求线程池-Executor,为了确保整个Web服务器的性能,应该在接收到请求后以最快的速度把它转交给其他线程去处理, 在接收到客户端的请求后这些请求被交给任务执行器Executor,它是一个拥有最大最小线程数限制
    在这里插入图片描述
      任务执行器的实现使用JUC工具包ThreadPoolExecutor类, 它提供了线程池的多种机制,例如最大线程数限制,多余线程回收时间,超出最大线程数时线程池数做出拒绝动作等, 继承此类并重写一些方法基本上就能满足Tomcat的修改化需求 。

      Connector 组件的Executor分为两种类型, 共享Executor和私有的Executor .
      所谓的共享Executor则指直接使用Service组件的线程池, 多个Connector可以共用这些线程池, 可以在server.xml文件中通过如下配置进行配置, 先在节点下配置一个 ,它表示该任务 执行最大线程数为150,最小线程数为4,线程名前缀为catalina-exec-,并且命名为tomcatThreadPool,节点中指定以tomcatThreadPool作为任务执行器, 对于多个Connector ,如图6.9所示,可以同时指定同一个Executor,以达到共享的目的 。
    在这里插入图片描述

    
        
            
               executor="tomcatThreadPool" 
                   connectionTimeout="20000"
                   redirectPort="8443"/>
    
    
    
    
    

      所谓私有的Executor是指未使用共享线程池,而是自己创建线程池, 如下面的配置所示,第一个Connector 配置未引用共享线程池, 所以它会为该Connector 创建一个默认的Executor,它的最小的线程数为10,最大线程池数为200,线程名字为前缀为IP-exec-,线程池里面的线程全部为守护线程,线程数超过10时等待60秒, 如果还没有任务执行将销毁此线程, 第二个Connector 配置未引用的共享线程池,但它声明了maxThreads和minSpareThreads属性,表示私有线程池的最小线程数为minSpareThreads,而最大线程数为maxThreads,第一个Connector 和第二个Connector各自使用使用自己的线程池,这便是私有的Executor。

    
    
        
    

      上面有一个重要的组件,连接轮询器Poller,接下来看连接轮询器的相关分析 。

    连接轮询器-Poller

      NIO模型需要同时对很多的连接进行管理,管理方式则是不断的遍历事件列表,对相应的连接的相应事件做出处理,而遍历的工作正是交给了Poller负责,Poller负责的工作可以用图6.39简单的表示出来,在Java层面上来看,它不断的轮询事件列表,一旦发现相应的事件则封装成任务定义器SocketProcessor ,进而扔进线程池中执行任务,当然,由于 NioEndPoint组件内有一个Poller池,因此如果不存在线程池,任务将由Poller直接执行。

    在这里插入图片描述
      Poller内部依赖JDK的Selector对象轮询,Selector会选择出待处理的事件,每次轮询一次就选出若干需要处理的通道,例如从通道中读取字节,将字节写入Channel等,在NIO 模式下,因为每次读取的数据是不确定的, 对于HTTP协议来说, 每次读取的数据可能既包含了请求行也包含了请求头部, 也可能不包含请求头部,所以每次只能尝试去解析报文,若解析不成功则等待下次轮询读取更多的数据再尝试解析,如果解析报文成功则做一些逻辑处理后对客户端响应,而这些报文解析,逻辑处理,响应等都是在任务定义器中定义的。

    在这里插入图片描述
      实现代码请看下图中加粗代码

    public class Poller implements Runnable {
        public void run() {
            // Loop until destroy() is called
            while (true) {
                try {
                   	... 
                    Iterator 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();
        }
       
        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
    
                    // 当前就绪事件对应的channel
                    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()) {
                                    // 从channel中读取数据
                                    if (!processSocket(channel, SocketStatus.OPEN_READ, true)) {
                                        closeSocket = true;
                                    }
                                }
                                // 读完数据之后可能就要写数据
                                if (!closeSocket && sk.isWritable()) {
                                    // 将数据写入到channel中
                                    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;
        }
    
    
    
           public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
        // 该方法是用来从socket中读数据或写数据的,dispatch表示是不是要把这个任务派发给线程池,也就是要不要异步
        try {
            KeyAttachment attachment = (KeyAttachment)socket.getAttachment();
            if (attachment == null) {
                return false;
            }
            attachment.setCometNotify(false); //will get reset upon next reg
    
            // 获取一个SocketProcessor对象
            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;
    }
    
    
    
    6.Poller池

      在NIO模式下, 对于客户端连接的管理都是基于事件驱动的, 上一节提到了NioEndPoint组件包含了Poller组件,Poller组件负责的就是检测事件并处理事件,但假如整个Tomcat的所有客户端连接都交给一个线程来处理, 那么即使这个线程是不阻塞的, 整个处理性能也可能无法达到最佳或较佳的状态,为了提升处理性能,Tomcat设置成由多个Poller共同处理所有客户端连接,所有的连接均摊给每个Poller处理,而这些Poller组成了Poller池。
      整个结构如图6.40所示 , 客户端连接由Acceptor组件接收到按照一定的算法放到通道队列上,这里使用的是轮询调度算法,从第1个队列的N个队列循环分配,假如这里有3个Poller,则第1个连接分配给第1个Poller对应的通道列表,第2个连接分配给第2个Poller对应的通道列表,以此类推,到第4个连接又分配到第1个Poller对应的通道列表,这种算法基本上保证了每个Poller所对应的处理连接数均匀的,每个Poller各自轮询检测自己对应的事件列表,一旦发现需要处理连接则对其进行处理, 这时如果NioEndPoint组件包含任务执行器(Executor),则将任务处理交给它,但假如没有Executor组件,则Poller则自己处理任务 。
    在这里插入图片描述
    轮询算法代码如下
    在这里插入图片描述

    BIO 请求过程

      看了那么多的理论知识,是不是还是感觉有点晕,那就从整体来分析BIO或NIO的请求过程吧,先看BIO的实现。

    public class SocketServer1 {
        public static void main(String[] args) throws IOException {
            ServerSocket serverSocket = new ServerSocket(9000);
            while (true) {
                System.out.println("等待连接。。");
                //阻塞方法 
                Socket clientSocket = serverSocket.accept();
                System.out.println("有客户端连接了。。");
                byte[] bytes = new byte[1024];
                System.out.println("准备read。。");
                //接收客户端的数据,阻塞方法,没有数据可读时就阻塞 
                int read = clientSocket.getInputStream().read(bytes);
                System.out.println("read完毕。。");
                if (read != -1) {
                    System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
                }
            }
        }
    
    }
    

      整个BIO就3行有用的代码,如上面加粗代码所示,那看Tomcat如何帮我们实现上面3行代码的呢?

    1. JIoEndpoint的bind()方法
     public void bind() throws Exception {
    
        ... 
    
        if (serverSocketFactory == null) {
            if (isSSLEnabled()) {
                serverSocketFactory =
                    handler.getSslImplementation().getServerSocketFactory(this);
            } else {
                serverSocketFactory = new DefaultServerSocketFactory(this);
            }
        }
    
        if (serverSocket == null) {
            try {
                if (getAddress() == null) {
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog());
                } else {
                    // serverSocket会不停的接收客户端连接,getBacklog()
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog(), getAddress());
                }
            } catch (BindException orig) {
                String msg;
                if (getAddress() == null)
                    msg = orig.getMessage() + " :" + getPort();
                else
                    msg = orig.getMessage() + " " +
                            getAddress().toString() + ":" + getPort();
                BindException be = new BindException(msg);
                be.initCause(orig);
                throw be;
            }
        }
    }
    

      第一行加粗代码绑定端口已经实现。
    2. serverSocket.accept();这一行代码的实现,先看JIoEndpoint的startInternal()方法 。

    public void startInternal() throws Exception {
        ...
        startAcceptorThreads();                                                           
        ...
    }
    
                                                    
    protected final void startAcceptorThreads() {
        // 3. 根据acceptorThreadCount配置的数量,创建并启动org.apache.tomcat.util.net.JIoEndpoint.Acceptor线程,Acceptor实现
        // 了Runnable接口,负责轮询接收客户端请求(Socket.accept()) ,这些线程是单独启动的,并未放到线程池中,因此不会影响到请求并发处理
        // Acceptor还会检测Endpoint状态(如果处于暂停状态,则等待)以及最大链接数
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];
    
        for (int i = 0; i < count; i++) {
            acceptors[i] = new Acceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());
            t.start();
        }
    }
    

      创建了一个Acceptor对象,而Acceptor实现了Runnable接口,最终在startAcceptorThreads()方法中启动了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
                // 如果Endpoint仍然在运行,但是被暂停了,那么就无限循环,从而不能接受请求
                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();
    
                    Socket socket = null;  // bio,nio
                    try {
                        // Accept the next incoming connection from the server
                        // bio socket
                        // 此处是阻塞的,那么running属性就算已经被改成false,那么怎么进入到下一次循环呢?
                        // serverSocketFactory的默认值为DefaultServerSocketFactory
                        socket = serverSocketFactory.acceptSocket(serverSocket);//
                        System.out.println("接收到了一个socket连接");
    
                    } catch (IOException ioe) {
                        countDownConnection();
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;
    
                    // Configure the socket
                    // 如果Endpoint正在运行并且没有被暂停,那么就处理该socket
                    if (running && !paused && setSocketOptions(socket)) {
                        // Hand this socket off to an appropriate processor
                        // socket被正常的交给了线程池,processSocket就会返回true
                        // 如果没有被交给线程池或者中途Endpoint被停止了,则返回false
                        // 返回false则关闭该socket
                        if (!processSocket(socket)) {
                            countDownConnection();
                            // Close socket right away
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        // Close socket right away
                        closeSocket(socket);
                    }
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (NullPointerException npe) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), npe);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }
    }
    
    
    public Socket acceptSocket(ServerSocket socket) throws IOException {
        return socket.accept();
    }
    

      现在第二行加粗代码已经实现。 但上面有一个问题,socket = serverSocketFactory.acceptSocket(serverSocket); , 这行代码是阻塞的,那么running属性就算已经被改成false,那么怎么进入到下一次循环呢?那我们来看JIoEndpoint的stopInternal()方法 。

    public void stopInternal() {
        releaseConnectionLatch();
        if (!paused) {
            pause();
        }
        if (running) {
            running = false;
            unlockAccept();
        }
        shutdownExecutor();
    }
    
    protected void unlockAccept() {
        // Only try to unlock the acceptor if it is necessary
        boolean unlockRequired = false;
        for (Acceptor acceptor : acceptors) {
            if (acceptor.getState() == AcceptorState.RUNNING) {
                unlockRequired = true;
                break;
            }
        }
        if (!unlockRequired) {
            return;
        }
    
        java.net.Socket s = null;
        InetSocketAddress saddr = null;
        try {
            // Need to create a connection to unlock the accept();
            if (address == null) {
                saddr = new InetSocketAddress("localhost", getLocalPort());
            } else if (address.isAnyLocalAddress()) {
                saddr = new InetSocketAddress(getUnlockAddress(address), getLocalPort());
            } else {
                saddr = new InetSocketAddress(address, getLocalPort());
            }
            s = new java.net.Socket();
            int stmo = 2 * 1000;
            int utmo = 2 * 1000;
            if (getSocketProperties().getSoTimeout() > stmo)
                stmo = getSocketProperties().getSoTimeout();
            if (getSocketProperties().getUnlockTimeout() > utmo)
                utmo = getSocketProperties().getUnlockTimeout();
            s.setSoTimeout(stmo);
            // TODO Consider hard-coding to s.setSoLinger(true,0)
            s.setSoLinger(getSocketProperties().getSoLingerOn(),getSocketProperties().getSoLingerTime());
            if (getLog().isDebugEnabled()) {
                getLog().debug("About to unlock socket for:"+saddr);
            }
            
            s.connect(saddr,utmo);
            if (getDeferAccept()) {
                /*
                 * In the case of a deferred accept / accept filters we need to
                 * send data to wake up the accept. Send OPTIONS * to bypass
                 * even BSD accept filters. The Acceptor will discard it.
                 */
                OutputStreamWriter sw;
    
                sw = new OutputStreamWriter(s.getOutputStream(), "ISO-8859-1");
                sw.write("OPTIONS * HTTP/1.0\r\n" +
                         "User-Agent: Tomcat wakeup connection\r\n\r\n");
                sw.flush();
            }
            if (getLog().isDebugEnabled()) {
                getLog().debug("Socket unlock completed for:"+saddr);
            }
    
            // Wait for upto 1000ms acceptor threads to unlock
    	    // 默认等待一秒钟 
            long waitLeft = 1000;
            for (Acceptor acceptor : acceptors) {
                while (waitLeft > 0 &&
                        acceptor.getState() == AcceptorState.RUNNING) {
                    Thread.sleep(5);
                    waitLeft -= 5;
                }
            }
        } catch(Exception e) {
            if (getLog().isDebugEnabled()) {
                getLog().debug(sm.getString("endpoint.debug.unlock", "" + getPort()), e);
            }
        } finally {
            if (s != null) {
                try {
                    s.close();
                } catch (Exception e) {
                    // Ignore
                }
            }
        }
    }
    

      现在大家明白了没有,如果将running改为false,但socket = serverSocketFactory.acceptSocket(serverSocket);这行代码是阻塞的,此时tomcat服务器模拟客户端链接请求,将服务端阻塞的Socket唤醒。从而达到停止Acceptor的目的。 可以来测试一下。

    1. 创建服务端代码
    public class SocketServer {
        public static void main(String[] args) throws IOException {
            ServerSocket serverSocket = new ServerSocket(9000);
            while (true) {
                System.out.println("等待连接。。");
                //阻塞方法
                Socket clientSocket = serverSocket.accept();
                System.out.println("有客户端连接了。。");
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            handler(clientSocket);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        }
    
        private static void handler(Socket clientSocket) throws IOException {
            byte[] bytes = new byte[1024];
            System.out.println("准备read。。");
            //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
            int read = clientSocket.getInputStream().read(bytes);
            System.out.println("read完毕。。");
            if (read != -1) {
                System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
            }
            clientSocket.getOutputStream().write("HelloClient".getBytes());
            clientSocket.getOutputStream().flush();
        }
    }
        
    
    1. 客户端代码
    public class SocketClient {
    
        public static void main(String[] args) throws IOException, InterruptedException {
            InetSocketAddress saddr =   new InetSocketAddress("127.0.0.1",9000);
            Socket s = new Socket();
            int stmo = 2 * 1000;
            int utmo = 2 * 1000;
            s.setSoTimeout(stmo);
            s.connect(saddr,utmo);
    
            //向服务端发送数据
            OutputStreamWriter sw;
            sw = new OutputStreamWriter(s.getOutputStream(), "ISO-8859-1");
            sw.write("OPTIONS * HTTP/1.0\r\n" +
                    "User-Agent: Tomcat wakeup connection\r\n\r\n");
            sw.flush();
    
            System.out.println("向服务端发送数据结束");
            byte[] bytes = new byte[1024];
            //接收服务端回传的数据
            s.getInputStream().read(bytes);
            System.out.println("接收到服务端的数据:" + new String(bytes));
            s.close();
        }
    } 
    
    1. 服务端数据打印
      在这里插入图片描述
      接下来继续分析在tomcat中BIO第3行clientSocket.getInputStream().read(bytes) 加粗代码实现。
     protected boolean processSocket(Socket socket) {
            // Process the request from this socket
            try {
                SocketWrapper wrapper = new SocketWrapper(socket);
                ... 
                // bio, 一个socket连接对应一个线程
                // 一个http请求对应一个线程?
                getExecutor().execute(new SocketProcessor(wrapper));
            } catch (RejectedExecutionException x) {
                log.warn("Socket processing request was rejected for:"+socket,x);
                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;
        }
    }
    
    protected class SocketProcessor implements Runnable {
    
        @Override
        public void run() {
            boolean launch = false;
            synchronized (socket) {
                // 开始处理socket
                // Socket默认状态为OPEN
                try {
                    
                    ... 
                    // 当前socket没有关闭则处理socket
                    if ((state != SocketState.CLOSED)) {
                        // SocketState是Tomcat定义的一个状态,这个状态需要处理一下socket才能确定,因为跟客户端,跟具体的请求信息有关系
                        if (status == null) {
                            state = handler.process(socket, SocketStatus.OPEN_READ);
                        } else {
                            // status表示应该读数据还是应该写数据
                            // state表示处理完socket后socket的状态
                            state = handler.process(socket,status);
                        }
                    }
                    ... 
                } finally {
                   
            }
            socket = null;
            // Finish up this request
        }
    
    }
    
    public SocketState process(SocketWrapper wrapper,
            SocketStatus status) {
            ... 
            do {
                ... 
                } else {
                    // 大多数情况下走这个分支
                    state = processor.process(wrapper);
                }
    
               ...            
            } while (state == SocketState.ASYNC_END ||
                    state == SocketState.UPGRADING ||
                    state == SocketState.UPGRADING_TOMCAT);
    
            ... 
        return SocketState.CLOSED;
    }
    
    
    
    public SocketState process(SocketWrapper socketWrapper)
        throws IOException {
        RequestInfo rp = request.getRequestProcessor();
        rp.setStage(org.apache.coyote.Constants.STAGE_PARSE);   // 设置请求状态为解析状态
    
        // Setting up the I/O
        setSocketWrapper(socketWrapper);
        // 将socket的InputStream与InternalInputBuffer进行绑定
        getInputBuffer().init(socketWrapper, endpoint);  
        
        ...
    
                // 解析请求行
                if (!getInputBuffer().parseRequestLine(keptAlive)) {
                    // 下面这个方法在NIO时有用,比如在解析请求行时,如果没有从操作系统读到数据,则上面的方法会返回false
                    // 而下面这个方法会返回true,从而退出while,表示此处read事件处理结束
                    // 到下一次read事件发生了,就会从小进入到while中
                    if (handleIncompleteRequestLineRead()) {
                        break;
                    }
                }
            ...
            } catch (IOException e) {
                if (getLog().isDebugEnabled()) {
                    getLog().debug(
        ... 
    }
    
    
    
    
    
    protected void init(SocketWrapper socketWrapper,
            AbstractEndpoint endpoint) throws IOException {
        inputStream = socketWrapper.getSocket().getInputStream();
    }
    
    
    
    public boolean parseRequestLine(boolean useAvailableDataOnly)
    
       throws IOException {
    
       int start = 0;
    
       //
       // Skipping blank lines
       //
    
       byte chr = 0;
       do {
           // 把buf里面的字符一个个取出来进行判断,遇到非回车换行符则会退出
    
           // Read new bytes if needed
           // 如果一直读到的回车换行符则再次调用fill,从inputStream里面读取数据填充到buf中
           if (pos >= lastValid) {
               if (!fill())
                   throw new EOFException(sm.getString("iib.eof.error"));
           }
           // Set the start time once we start reading data (even if it is
           // just skipping blank lines)
           if (request.getStartTime() < 0) {
               request.setStartTime(System.currentTimeMillis());
           }
           chr = buf[pos++];
       } while ((chr == Constants.CR) || (chr == Constants.LF));
    
       pos--;
    
       // Mark the current buffer position
       start = pos;
    
    }
    
    
    protected boolean fill() throws IOException {
        return fill(true);
    }
    
    @Override
    protected boolean fill(boolean block) throws IOException {
    
        int nRead = 0;
    
        if (parsingHeader) {
    
            // 如果还在解析请求头,lastValid表示当前解析数据的下标位置,如果该位置等于buf的长度了,表示请求头的数据超过buf了。
            if (lastValid == buf.length) {
                throw new IllegalArgumentException
                    (sm.getString("iib.requestheadertoolarge.error"));
            }
    
            // 从inputStream中读取数据,len表示要读取的数据长度,pos表示把从inputStream读到的数据放在buf的pos位置
            // nRead表示真实读取到的数据
            nRead = inputStream.read(buf, pos, buf.length - lastValid);
            if (nRead > 0) {
                lastValid = pos + nRead; // 移动lastValid
            }
    
        } else {
            // 当读取请求体的数据时
            // buf.length - end表示还能存放多少请求体数据,如果小于4500,那么就新生成一个byte数组,这个新的数组专门用来盛放请求体
            if (buf.length - end < 4500) {
                // In this case, the request header was really large, so we allocate a
                // brand new one; the old one will get GCed when subsequent requests
                // clear all references
                buf = new byte[buf.length];
                end = 0;
            }
            pos = end;
            lastValid = pos;
            nRead = inputStream.read(buf, pos, buf.length - lastValid);
            if (nRead > 0) {
                lastValid = pos + nRead;
            }
        }
        return (nRead > 0);
    }
    

      在getInputBuffer().init(socketWrapper, endpoint); 这一行代码中完成了clientSocket.getInputStream() 的调用,而在fill()方法中nRead = inputStream.read(buf, pos, buf.length - lastValid),不就是.read(bytes)的实现不?合起来不就是clientSocket.getInputStream().read(bytes) 从socket中获取inputStream,再调用其read()方法,从流上获取字节的操作,关于BIO的请求过程中的细节,在下一篇博客中再来具体分析。

    Nio服务端实现

      如果自己写一个Nio的服务端实现,那怎样写呢?请看下面例子。

    public class NioSelectorServer {
    
        public static void main(String[] args) throws IOException {
    
            // 创建NIO ServerSocketChannel
            ServerSocketChannel serverSocket = ServerSocketChannel.open();
            serverSocket.socket().bind(new InetSocketAddress(9000));
            // 设置ServerSocketChannel为非阻塞
            serverSocket.configureBlocking(false);
            // 打开Selector处理Channel,即创建epoll
            Selector selector = Selector.open();
            // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
            SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务启动成功");
    
            while (true) {
                // 阻塞等待需要处理的事件发生
                selector.select();
    
                // 获取selector中注册的全部事件的 SelectionKey 实例
                Set selectionKeys = selector.selectedKeys();
                Iterator iterator = selectionKeys.iterator();
    
                // 遍历SelectionKey对事件进行处理
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = server.accept();
                        socketChannel.configureBlocking(false);
                        // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                        SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("客户端连接成功");
                    } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128);
                        int len = socketChannel.read(byteBuffer);
                        // 如果有数据,把数据打印出来
                        if (len > 0) {
                            System.out.println("接收到消息:" + new String(byteBuffer.array()));
                        } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                            System.out.println("客户端断开连接");
                            socketChannel.close();
                        }
                    }
                    //从事件集合里删除本次处理的key,防止下次select重复处理
                    iterator.remove();
                }
            }
        }
    }
    

      接下来看Tomcat中如何实现上面这段代码的逻辑。

    1. 先看NioEndpoint的bind方法
    public void bind() throws Exception {
    
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
        serverSock.socket().bind(addr,getBacklog());
        serverSock.configureBlocking(true); //mimic APR behavior
        ...
    }
    
    1. 再来看NioEndpoint的startInternal方法
     public void startInternal() throws Exception {
    
        if (!running) {
            running = true;
            paused = false;
    
            // Create worker collection
            if ( getExecutor() == null ) {
                createExecutor();
            }
    
            // 初始化limitlatch,NioEndpoint默认最大连接数为10000
            initializeConnectionLatch();
    
            // Start poller threads
            // 用Poller线程来处理来处理io读写事件,线程的个数,如果是多核就是两个线程,如果单核就是一个线程
            // 1. NioEndpoint中的ServerSocketChannel是阻塞的,因此,仍然采用多线程并发接收客户端链接 。
            // 2. NioEndpoint根据pollerThreadCount配置的数量创建Poller线程,与Acceptor相同,Poller线程也是单独启动,不会占用请求处理的
            // 线程池,默认的Poller线程个数与JVM可使用的处理器相关。上限为2
            // 3. Accepor接收到新的链接后,将获得SocketChannel置为非阻塞,构造NioChannel对象,按照轮转法(Round-Robin)获取Poller
            // 实例,并将NioChannel注册到PollerEvent事件队列
            // 4. Poller负责为SocketChannel 注册读事件,接收到读事件后,由SocketProccesor完成客户端请求处理,SocketProcessor完成
            // 客户端请求处理,SocketProcessor的处理过程具体参见4.2节的说明
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; ipollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true);
                pollerThread.start();
            }
    
            startAcceptorThreads();
        }
    }
    
    1. 看Poller的构造方法实现
    public Poller() throws IOException {
        synchronized (Selector.class) {
            // Selector.open() isn't thread safe
            // http://bugs.sun.com/view_bug.do?bug_id=6427854
            // Affects 1.6.0_29, fixed in 1.7.0_01
            this.selector = Selector.open();
        }
    }
    

    每一个Poller中都有一个Selector对象,接下来看如何将ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣。

    1. startAcceptorThreads()方法中创建了org.apache.tomcat.util.net.NioEndpoint.Acceptor 类。 看Acceptor的具体实现。
    protected class Acceptor extends AbstractEndpoint.Acceptor {
    
        @Override
        public void run() {
    
            int errorDelay = 0;
    
            while (running) {
    				... 
                    try {
                        // Accept the next incoming connection from the server
                        socket = serverSock.accept(); //
                    } catch (IOException ioe) {
                        ... 
                    }
                  
                    if (running && !paused) {
                        if (!setSocketOptions(socket)) {
                            countDownConnection();
                            closeSocket(socket);		
                        }
                    }
                    ... 
            state = AcceptorState.ENDED;
        }
    }
    
    
    1. 看setSocketOptions()方法的实现
    protected boolean setSocketOptions(SocketChannel socket) {
            // Process the connection
        try {
            //disable blocking, APR style, we are gonna be polling it
            // 从该channel上读取数据不阻塞
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);
    
            ... 
            // 每接收到一个新socket连接,就会生成一个
            getPoller0().register(channel);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            try {
                log.error("",t);
            } catch (Throwable tt) {
                ExceptionUtils.handleThrowable(tt);
            }
            // Tell to close the socket
            return false;
        }
        return true;
    }
    
    public Poller getPoller0() {
        int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
        return pollers[idx];
    }
    
    
    1. 看register()方法实现
    public void register(final NioChannel socket) {
        socket.setPoller(this);
    
        // 获取一个KeyAttachment对象,将当前socket的相关信息设置进去
        KeyAttachment key = keyCache.poll();
        final KeyAttachment ka = key!=null?key:new KeyAttachment(socket);
        ka.reset(this,socket,getSocketProperties().getSoTimeout());
        ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
        ka.setSecure(isSSLEnabled());
    
        // 获取一个PollerEvent对象,本事件为一个注册事件,对读事件感兴趣(这里暂时还没有真正的向select去注册事件)
        PollerEvent r = eventCache.poll();
        ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
        if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
        else r.reset(socket,ka,OP_REGISTER);
    
        // 把PollerEvent添加到事件列表中去
        addEvent(r);
    }
    
    public void addEvent(Runnable event) {
        events.offer(event);
        if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
    }
    

    上述代码就是将有事件的NioChannel构建成PollerEvent加入到events中。

    1. 接下来看Poller的实现
    public class Poller implements Runnable {
    
        @Override
        public void run() {
    
            while (true) {
                try {
              
                   	... 
                    } else {
                        // 执行PollerEvent事件,向Selector注册读写事件
                        hasEvents = events(); // 真正的向selector注册
                    }
                    try {
                        if ( !close ) {
                            if (wakeupCounter.getAndSet(-1) > 0) {
                                //if we are here, means we have other stuff to do
                                //do a non blocking select
                                // 上面的events()会去注册事件,而这里是去查询是否有事件就绪
                                // 不阻塞
                                keyCount = selector.selectNow();
                            } else {
                                // 阻塞,超时会继续执行下面的代码,不会报错
                                keyCount = selector.select(selectorTimeout);
                            }
                            wakeupCounter.set(0);
                        }
                        
                    } 
                    //either we timed out or we woke up, process events first
                    if ( keyCount == 0 ) hasEvents = (hasEvents | events());
    
                    // 如果存在就绪事件,那么则遍历并处理事件
                    Iterator 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
    
                	... 
    
                } catch (OutOfMemoryError oom) {
               
                }
            }//while
        }
    
    1. 上述过程中有一个非常重要的方法就是events()事件。看这个方法做了哪些事情。
    public boolean events() {
        boolean result = false;
    
        Runnable r = null;
        // poll会把元素从队列中删除掉
        for (int i = 0, size = events.size(); i < size && (r = events.poll()) != null; i++ ) {
            result = true;
            try {
                // 如果是PollerEvent,会将读事件注册到当前poller中的selector对象上
                r.run();
                if ( r instanceof PollerEvent ) {
                	// 重置PollerEvent
                    ((PollerEvent)r).reset();
                    // PollerEvent的循环使用
                    eventCache.offer((PollerEvent)r);
                }
            } catch ( Throwable x ) {
                log.error("",x);
            }
        }
    
        return result;
    }
    
    1. 调用PollerEvent的run()方法,注意这里不是线程启动后,系统自动调用run()方法,而是我们手动调用run()方法,此时run()方法只是一个普通的方法 。
    public static class PollerEvent implements Runnable {
        // PollerEvent表示需要注册的事件,
    
        protected NioChannel socket;
        protected int interestOps;
        protected KeyAttachment key;
        public PollerEvent(NioChannel ch, KeyAttachment k, int intOps) {
            reset(ch, k, intOps);
        }
    
        public void reset(NioChannel ch, KeyAttachment k, int intOps) {
            socket = ch;
            interestOps = intOps;
            key = k;
        }
    
        public void reset() {
            reset(null, null, 0);
        }
    
        @Override
        public void run() {
            if ( interestOps == OP_REGISTER ) {
                // 真正将读事件注册到当前poller中的selector对象上
                try {
                	// socket.getIOChannel() 为SocketChannel对象
                	// 因此下面这句实际上就是
                	// SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT) 这行代码 
                    socket.getIOChannel().register(socket.getPoller().getSelector(), SelectionKey.OP_READ, key);
                } catch (Exception x) {
                    log.error("", x);
                }
            } else {
                
            }//end if
        }//run
    
        @Override
        public String toString() {
            return super.toString()+"[intOps="+this.interestOps+"]";
        }
    }
    
    1. 看数据的处理方法
    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
    
                // 当前就绪事件对应的channel
                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()) {
                                // 从channel中读取数据
                                if (!processSocket(channel, SocketStatus.OPEN_READ, true)) {
                                    closeSocket = true;
                                }
                            }
                            // 读完数据之后可能就要写数据
                            if (!closeSocket && sk.isWritable()) {
                                // 将数据写入到channel中
                                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;
    }
    
    1. 如果是可读事件,真正由processSocket()方法处理socket
    public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
        // 该方法是用来从socket中读数据或写数据的,dispatch表示是不是要把这个任务派发给线程池,也就是要不要异步 
        try {
            KeyAttachment attachment = (KeyAttachment)socket.getAttachment();
            if (attachment == null) {
                return false;
            }
            attachment.setCometNotify(false); //will get reset upon next reg
    
            // 获取一个SocketProcessor对象 
            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;
    }
    
    1. 接着看SocketProcessor的run方法
    protected class SocketProcessor implements Runnable {
        protected NioChannel socket = null;
        protected SocketStatus status = null;
    
        public SocketProcessor(NioChannel socket, SocketStatus status) {
            reset(socket,status);
        }
    
        public void reset(NioChannel socket, SocketStatus status) {
            this.socket = socket;
            this.status = status;
        }
    
        @Override
        // 用NIO 方式读取套接字并进行处理,输出响应报文
        // 连接数计数器减1,腾出通道
        // 关闭套接字
        public void run() {
            // 获取当前channel上就绪的事件
            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 {
                // 在nio中,每产生一个就绪的io事件,就会通过一个线程来处理该事件,需要进行同步
                // 意思是,多个线程只能并发处理不同socket,不能处理同一个socket
                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);
            }
        }
    }
    

      上述handler为Http11NioProcessor

    HTTP非阻塞处理器-Http11NioProcessor

      Http11NioProcessor组件提供了对HTTP协议非阻塞模式的处理, 包括对套接字读取和过滤,对HTTP协议的解析与封装成请求对象,HTTP协议响应对象的生成等操作,整体结构如图6.41所示 。
    在这里插入图片描述

    1. 非阻塞套接字输入缓冲装置 InternalNioInputBuffer

      在消息传递过程中, 为了提高消息从一端传到另一端的效率,一般会引入缓冲区, 对于写入缓冲区,只有在强制刷新或缓冲区被填满后才会真正原执行写操作,Tomcat在BIO模式中使用了套接字输入缓冲装置来接收客户端的数据,它会提供一种缓冲模式从套接字中读取字节流并解析HTTP协议的请求行和请求头部, 最后填充好请求对象Request。
      在NIO模式下,Tomcat同样存在一个类似的缓冲装置来处理HTTP协议报文,它就是非阻塞套接字输入缓冲装置(InternalNioInputBuffer)它与阻塞套接字输入缓冲装置之间不同就在于读取套接字数据的方式,阻塞方式是会一直阻塞, 直到数据返回,而非阻塞则是尝试读取,有没有数据返回,所以它们的基本原理及机制都是相同的, 而唯一的差异在于此, 如果对缓冲装置的工作原理不太清楚,可以参考6.1.2节。
      为了更好的理解如何使用NIO模式从底层读取守节并进行解析,下面给出了简化的处理过程,首先需要一个方法提供读取的字节,代码如下所示,NIO 模式下读取数据不再使用流的方式读取,而是通过通道的读取,所以这里使用了NioChannel对象读取,非阻塞并不能保证一定读取到数据,读取不到数据时会直接返回-1。

    public class InternalInputBuffer {
    
        byte [] buf = new byte[8 * 1024];
        int pos = 0 ;
        int lastValid = 0 ;
        ByteBuffer readbuf = ByteBuffer.allocate(8192);
        public boolean fill(){
            int nRead = nioChannel.read(readbuf);
            if (nRead > 0 ){
                readbuf.get(buf,pos,nRead);
                lastValid = pos + nRead;
            }
            return nRead > 0 ;
        }
    }
    

      有了填充方法,接下来,需要一个解析报文的操作过程, 如图6.42所示, 当Poller轮询检测到有可读取事件后,开始处理相应的NioChannel,非阻塞套接字输入缓冲装置InternalNioInputBuffer将开始读取NioChannel里面的数据,然后开始尝试解析HTTP请求行, 解析过程中, 如果数据不足,则用fill方法尝试读取数据,此时,如果读取不到数据,则直接返回,结束此次处理,当Poller再次检测到该通道的可读事件后,非阻塞套接字输入缓冲装置再次从NioChannel里面读取数据,并接着上一次结束的位置继续处理,但如果用fill方法尝试读取数据成功,则不必等到Poller第二次轮询,往下继续尝试解析HTTP请求头部,由于这个过程中同样可能数据不足,因此同样也会使用fill方法尝试读取数据,如果读取不到数据,也会直接结束 。 并需要等到Poller第二次检查到该通道时, 才能继续往下执行,最后完成整个HTTP请求报文的解析 。
      非阻塞套接字输入缓冲装置与阻塞套接字输入缓冲装置的结构大体相同,如图6.43所示 , NIOChannel组件用于读取底层Socket数据,SocketInputBuffer组件则提供读取请求体的功能,还有若干InputFilter组件属于过滤器。
    在这里插入图片描述

    2. 非阻塞套接字输出缓冲装置 InternalNioOutputBuffer

      非阻塞套接字输出缓冲装置是提供了NIO模式输出数据到客户端的组件,整个结构如图6.44所示,它包含了NioChannel组件,SocketOutputBuffer组件和OutputFilter组件,其中NioChannel组件是非阻塞的套接字输出通道,通过它以非阻塞模式将字节写入操作系统底层,SocketOutputBuffer组件提供字节流输出通道,与OutputFilter组件组合实现过滤效果。
    在这里插入图片描述
      接着继续看process()方法。

    public SocketState process(SocketWrapper socketWrapper)
        throws IOException {
        RequestInfo rp = request.getRequestProcessor();
        rp.setStage(org.apache.coyote.Constants.STAGE_PARSE);   // 设置请求状态为解析状态
    
        // Setting up the I/O
        setSocketWrapper(socketWrapper);
        getInputBuffer().init(socketWrapper, endpoint);     // 将socket的InputStream与InternalNioInputBuffer进行绑定
        getOutputBuffer().init(socketWrapper, endpoint);    // 将socket的OutputStream与InternalNioOutputBuffer进行绑定
    
        // Flags
        keepAlive = true;
        comet = false;
        openSocket = false;
        sendfileInProgress = false;
        readComplete = true;
        // NioEndpoint返回true, Bio返回false
        if (endpoint.getUsePolling()) {
            keptAlive = false;
        } else {
            keptAlive = socketWrapper.isKeptAlive();
        }
    
    	// 如果当前活跃的线程数占线程池最大线程数的比例大于75%,那么则关闭KeepAlive,不再支持长连接
        if (disableKeepAlive()) {
            socketWrapper.setKeepAliveLeft(0);
        }
        // keepAlive默认为true,它的值会从请求中读取
        while (!getErrorState().isError() && keepAlive && !comet && !isAsync() &&
                upgradeInbound == null &&
                httpUpgradeHandler == null && !endpoint.isPaused()) {
            // keepAlive如果为true,接下来需要从socket中不停的获取http请求
    
            // Parsing the request header
            try {
                // 第一次从socket中读取数据,并设置socket的读取数据的超时时间
                // 对于BIO,一个socket连接建立好后,不一定马上就被Tomcat处理了,其中需要线程池的调度,所以这段等待的时间要算在socket读取数据的时间内
                // 而对于NIO而言,没有阻塞
                setRequestLineReadTimeout();
    
                // 解析请求行
                if (!getInputBuffer().parseRequestLine(keptAlive)) {
                    // 下面这个方法在NIO时有用,比如在解析请求行时,如果没有从操作系统读到数据,则上面的方法会返回false
                    // 而下面这个方法会返回true,从而退出while,表示此处read事件处理结束
                    // 到下一次read事件发生了,就会从小进入到while中
                    if (handleIncompleteRequestLineRead()) {
                        break;
                    }
                }
    
                ...
            } catch (IOException e) {
      		...
    }
    

      初始化socket , socketReadBufferSize,buf等信息

    protected void init(SocketWrapper socketWrapper,
            AbstractEndpoint endpoint) throws IOException {
    
        socket = socketWrapper.getSocket();
        socketReadBufferSize =
            socket.getBufHandler().getReadBuffer().capacity();
    
        int bufLength = headerBufferSize + socketReadBufferSize;
        if (buf == null || buf.length < bufLength) {
            buf = new byte[bufLength];
        }
    
        pool = ((NioEndpoint)endpoint).getSelectorPool();
    }
    
    public boolean parseRequestLine(boolean useAvailableDataOnly)
        throws IOException {
    
        //check state
        if ( !parsingRequestLine ) return true;
        //
        // Skipping blank lines
        //
        if ( parsingRequestLinePhase == 0 ) {
            byte chr = 0;
            do {
    
                // Read new bytes if needed
                if (pos >= lastValid) {
                    // Nio默认这个值为false
                    if (useAvailableDataOnly) {
                        return false;
                    }
                    // Do a simple read with a short timeout
                    // 把channel中的数据读到buf中,如果没有读到则会返回false
                    if (!fill(true, false)) {
                        return false;
                    }
                }
                // Set the start time once we start reading data (even if it is
                // just skipping blank lines)
                if (request.getStartTime() < 0) {
                    request.setStartTime(System.currentTimeMillis());
                }
                chr = buf[pos++];
            } while ((chr == Constants.CR) || (chr == Constants.LF));
            pos--;
    
            parsingRequestLineStart = pos;
            parsingRequestLinePhase = 2;
            if (log.isDebugEnabled()) {
                log.debug("Received ["
                        + new String(buf, pos, lastValid - pos, DEFAULT_CHARSET)
                        + "]");
            }
        }
    }
    

      看看fill()方法的实现

    protected boolean fill(boolean block) throws IOException, EOFException {
        return fill(true,block);
    }
    
    protected boolean fill(boolean timeout, boolean block) throws IOException, EOFException {
      // 读请求体数据的时候需要阻塞读
        boolean read = false;
    
        if (parsingHeader) {
    
            if (lastValid > headerBufferSize) {
                throw new IllegalArgumentException
                    (sm.getString("iib.requestheadertoolarge.error"));
            }
    
            // Do a simple read with a short timeout
            read = readSocket(timeout,block)>0;
        } else {
            lastValid = pos = end;
            // Do a simple read with a short timeout
            read = readSocket(timeout, block)>0;
        }
        return read;
    }
    
    private int readSocket(boolean timeout, boolean block) throws IOException {
        // 读请求体数据的时候需要阻塞读
        int nRead = 0;
        socket.getBufHandler().getReadBuffer().clear();
        if ( block ) {
            Selector selector = null;
            try {
                selector = pool.get();
            } catch ( IOException x ) {
                // Ignore
            }
            try {
                NioEndpoint.KeyAttachment att =
                        (NioEndpoint.KeyAttachment) socket.getAttachment();
                if (att == null) {
                    throw new IOException("Key must be cancelled.");
                }
                // socket. selector
                nRead = pool.read(socket.getBufHandler().getReadBuffer(),
                        socket, selector, att.getTimeout());
            } catch ( EOFException eof ) {
                nRead = -1;
            } finally {
                if ( selector != null ) pool.put(selector);
            }
        } else {
            // 非阻塞读,没有读到不会阻塞,立即返回0,如果在处理这次NIo事件中没有读到数据,那么此事件其实就是处理结束了,等待下一次事件
            nRead = socket.read(socket.getBufHandler().getReadBuffer());
        }
        if (nRead > 0) {
            socket.getBufHandler().getReadBuffer().flip();
            socket.getBufHandler().getReadBuffer().limit(nRead);
            expand(nRead + pos);
            // 把readBuffer中的数据转移到buf中
            socket.getBufHandler().getReadBuffer().get(buf, pos, nRead);
            lastValid = pos + nRead;
            return nRead;
        } else if (nRead == -1) {
            //return false;
            throw new EOFException(sm.getString("iib.eof.error"));
        } else {
            return 0;
        }
    }
    
    
    public int read(ByteBuffer dst) throws IOException {
        return sc.read(dst);
    }
    

    在这里插入图片描述
      sc 刚好是SocketChannelImpl,同时是read数据,这个和我们例子中的socketChannel.read(byteBuffer);这一行是一样的。
    在这里插入图片描述
      因此从Tomcat中找到了NIO Server代码实现,仔细查看,发现原来Tomcat NIO实现也只不过如此 。

      接下来看,MapperListener的初始化代码。
    在这里插入图片描述
      MapperListener本身并没有实现initInternal()方法,而是父类的在这里插入图片描述
      只是将MapperListener注册到jmx中。 接下来看mapperListener的start()方法 。
    在这里插入图片描述
      接下来看MapperListener的start()方法

    public void startInternal() throws LifecycleException {
        setState(LifecycleState.STARTING);
    
        // Find any components that have already been initialized since the
        // MBean listener won't be notified as those components will have
        // already registered their MBeans
        findDefaultHost();
    
        Engine engine = (Engine) connector.getService().getContainer();
        // 为StandardEngine,StandardHost,StandardContext及StandardWrapper
        // 添加容器事件监听器 和 生命周期事件监听器
        addListeners(engine);
    
        Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
                // Registering the host will register the context and wrappers
                registerHost(host); // 将每个host注册到Mapper.hosts中
            }
        }
    }
    

      先看findDefaultHost()方法实现。

    private void findDefaultHost() {
        Engine engine = (Engine) connector.getService().getContainer();
        String defaultHost = engine.getDefaultHost(); // engine默认的host
        boolean found = false;
        if (defaultHost != null && defaultHost.length() >0) {
            Container[] containers = engine.findChildren();
    
            for (Container container : containers) {
                Host host = (Host) container;
                if (defaultHost.equalsIgnoreCase(host.getName())) {
                    found = true;
                    break;
                }
    
                String[] aliases = host.findAliases();
                for (String alias : aliases) {
                    if (defaultHost.equalsIgnoreCase(alias)) {
                        found = true;
                        break;
                    }
                }
            }
        }
    	// 查找engine下所有的child,如果child的hostName与Engine默认的hostName一样,则
    	// 设置Mapper默认的hostName
        if(found) {
            mapper.setDefaultHostName(defaultHost);
        } else {
            log.warn(sm.getString("mapperListener.unknownDefaultHost",
                    defaultHost, connector));
        }
    }
    
    
    private void addListeners(Container container) {
        container.addContainerListener(this); // 容器事件监听器
        container.addLifecycleListener(this); // 生命周期事件监听器
        for (Container child : container.findChildren()) {
        	// 递归为孩子添加容器事件和生命周期事件
            addListeners(child);
        }
    }
    
    private void registerHost(Host host) {
        String[] aliases = host.findAliases();
        mapper.addHost(host.getName(), aliases, host);  //将host加入到mapper.hosts中
    
        for (Container container : host.findChildren()) {
        	// 如果StandardContext为启动状态
            if (container.getState().isAvailable()) {
                registerContext((Context) container); // 将每个Context注册到Mapper.Host.ContextList中
            }
        }
        if(log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.registerHost",
                    host.getName(), domain, connector));
        }
    }
    

      注册StandardContext

    private void registerContext(Context context) {
        String contextPath = context.getPath();
        if ("/".equals(contextPath)) {
            contextPath = "";
        }
        Container host = context.getParent();
    
        javax.naming.Context resources = context.getResources();
        String[] welcomeFiles = context.findWelcomeFiles();
        // 该List里面存储的就是Servlet的URL映射关系
        List wrappers = new ArrayList()
    
        // 循环Wrapper节点,将mapping关系解析到wrappers中
        for (Container container : context.findChildren()) {
            prepareWrapperMappingInfo(context, (Wrapper) container, wrappers);
    
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("mapperListener.registerWrapper",
                        container.getName(), contextPath, connector));
            }
        }
        
        mapper.addContextVersion(host.getName(), host, contextPath,
                context.getWebappVersion(), context, welcomeFiles, resources,
                wrappers, context.getMapperContextRootRedirectEnabled(),
                context.getMapperDirectoryRedirectEnabled());
    
        if(log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.registerContext",
                    contextPath, connector));
        }
    }
    

    在这里插入图片描述
      接下来mapper中看添加StandardContext的版本实现。

    public void addContextVersion(String hostName, Object host, String path,
                String version, Object context, String[] welcomeResources,
                javax.naming.Context resources, Collection wrappers,
                boolean mapperContextRootRedirectEnabled, boolean mapperDirectoryRedirectEnabled) {
    
        Host mappedHost = exactFind(hosts, hostName);
        if (mappedHost == null) {
            addHost(hostName, new String[0], host);
            mappedHost = exactFind(hosts, hostName);
            if (mappedHost == null) {
                log.error("No host found: " + hostName);
                return;
            }
        }
        if (mappedHost.isAlias()) {
            log.error("No host found: " + hostName);
            return;
        }
        int slashCount = slashCount(path);
        synchronized (mappedHost) {
            // 整个每个Context的不同版本生成不同的ContextVersion对象,其实就是不同的应用,应为就算Context的Path相同,version不相同,那么还是表示不同的应用
    
            ContextVersion newContextVersion = new ContextVersion(version, context);
            newContextVersion.path = path;
            newContextVersion.slashCount = slashCount;
            newContextVersion.welcomeResources = welcomeResources;
            newContextVersion.resources = resources;
            newContextVersion.mapperContextRootRedirectEnabled = mapperContextRootRedirectEnabled;
            newContextVersion.mapperDirectoryRedirectEnabled = mapperDirectoryRedirectEnabled;
    
            if (wrappers != null) {
                // 针对每个应用将下层包括的Wrapper映射关系进行分类,并且添加到contextVersion中不同的Wrapperlist中去
                addWrappers(newContextVersion, wrappers);
            }
    
            ContextList contextList = mappedHost.contextList;
            // 通过path查找Context
            Context mappedContext = exactFind(contextList.contexts, path);
            if (mappedContext == null) {
                mappedContext = new Context(path, newContextVersion);
                ContextList newContextList = contextList.addContext(
                        mappedContext, slashCount);
                if (newContextList != null) {
                    // 如果ContextList发生了改变,回头更新Host中的contextList
                    updateContextList(mappedHost, newContextList);
                }
            } else {
            	// 如果相同的path有不同的版本号
                ContextVersion[] contextVersions = mappedContext.versions;
                ContextVersion[] newContextVersions =
                    new ContextVersion[contextVersions.length + 1];
                if (insertMap(contextVersions, newContextVersions, newContextVersion)) {
                	// mappedContext会有多个版本存在
                    mappedContext.versions = newContextVersions;
                } else {
                    // Re-registration after Context.reload()
                    // Replace ContextVersion with the new one
                    int pos = find(contextVersions, version);
                    if (pos >= 0 && contextVersions[pos].name.equals(version)) {
                        contextVersions[pos] = newContextVersion;
                    }
                }
            }
        }
    }
    

      接下来看将wrappers添加到newContextVersion实现。

    private void addWrappers(ContextVersion contextVersion,
            Collection wrappers) {
        // 遍历所有的wrappers
        for (WrapperMappingInfo wrapper : wrappers) {
            addWrapper(contextVersion, wrapper.getMapping(),
                    wrapper.getWrapper(), wrapper.isJspWildCard(),
                    wrapper.isResourceOnly());
        }
    }
    
    protected void addWrapper(ContextVersion context, String path,
            Object wrapper, boolean jspWildCard, boolean resourceOnly) {
    
        synchronized (context) {
            if (path.endsWith("/*")) {  // 比如/test/* 
                // Wildcard wrapper 通配符Wrapper
                String name = path.substring(0, path.length() - 2);
                // 这里的两个Wrapper都表示Servlet包装器,不同的是,一个是只用来记录映射关系,一个是真正的StandardWrapper 
                Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                        resourceOnly);
    
                // 如果在老的wildcardWrappers中不存在相同name的,则把新的通配符wrapper插入到数组中 
                Wrapper[] oldWrappers = context.wildcardWrappers;
                Wrapper[] newWrappers =
                    new Wrapper[oldWrappers.length + 1];
                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                    context.wildcardWrappers = newWrappers;
                    int slashCount = slashCount(newWrapper.name);   // 一个url被"/"切分为几个部分
                    // 记录当前Context中url按"/"切分后长度最大的长度 
                    if (slashCount > context.nesting) {
                        context.nesting = slashCount;
                    }
                }
            } else if (path.startsWith("*.")) { // 比如*.jsp 
                // Extension wrapper  扩展匹配 
                String name = path.substring(2);
                Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                        resourceOnly);
                Wrapper[] oldWrappers = context.extensionWrappers;
                Wrapper[] newWrappers =
                    new Wrapper[oldWrappers.length + 1];
                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                    context.extensionWrappers = newWrappers;
                }
            } else if (path.equals("/")) {
                // Default wrapper 
                Wrapper newWrapper = new Wrapper("", wrapper, jspWildCard,
                        resourceOnly);
                context.defaultWrapper = newWrapper;
            } else {
                // Exact wrapper   精确匹配 
                final String name;
                if (path.length() == 0) {
                    // Special case for the Context Root mapping which is
                    // treated as an exact match
                    // 我们可以在web.xml中配置一个mapping关系是,url-pattern设置为空,那么就表示可以通过应用跟路径来访问 
                    name = "/";
                } else {
                    name = path;
                }
                Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                        resourceOnly);
                Wrapper[] oldWrappers = context.exactWrappers;
                Wrapper[] newWrappers =
                    new Wrapper[oldWrappers.length + 1];
                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                    context.exactWrappers = newWrappers;
                }
            }
        }
    }
    

      创建ContextList ,最终保存到host中

    public ContextList addContext(Context mappedContext, int slashCount) {
        Context[] newContexts = new Context[contexts.length + 1];
        // 根据name来进行比较,如果mappedContext已经在contexts中存在,那么则不会进行插入,返回空
        // 如果不存在,则将mappedContext插入到newContexts中,并返回一个新的ContextList对象
        if (insertMap(contexts, newContexts, mappedContext)) {
            return new ContextList(newContexts, Math.max(nesting,
                    slashCount));
        }
        return null;
    }
    

      这一块代码,其实很费解,到底有什么用呢?为什么StandardContext中还有版本一说呢?来看个例子。

    1. 在/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost目录创建两个xml文件,servelet-test-1.0##1.xml,和servelet-test-1.0##2.xml
      在这里插入图片描述

      文件内容分别如下

    
    
    
    

      在/servelet-test-1.1的index.jsp页面中打印 小明, 在servelet-test-1.2项目的index.jsp中打印 小林。

    在这里插入图片描述
      为什么会出现这样的结果呢?
    在这里插入图片描述

      这两个版本号又是从哪里取的呢?先来看描述符布署项目 。
    在这里插入图片描述

    在这里插入图片描述

      再来看如何使用这个版本号。
    在这里插入图片描述
      这里涉及到tomcat的请求处理过程,因此只分析一个大概,一下篇博客中,我们再来具体分析tomcat的整个请求。

    总结

      tomcat启动就分析到这里了,已经涉及到了从StandardEngine,StandardHost,StandardContext,以及StandardWrapper,过滤器,事件,等都作了一个分析,尤其是StandardContext的启动特别复杂,整个启动过程异常的繁锁,但是又特别重要,不然你分析Tomcat请求处理过程时,陌生的变量,会让你简直不知所云。经过一个月的努力,终于写成了 Tomcat 源码解析一容器加载-大寂灭指(上)Tomcat 源码解析一容器加载-大寂灭指(中)Tomcat 源码解析一容器加载-大寂灭指(下) 三篇博客 ,总共有50多万字了,当然包括tomcat本身的代码,加上我的文笔不一定写得好,因此,如果感兴趣的小伙伴可以自己去研究一下Tomcat的整个启动过程。 相信你有不小的收获,同时我看网上也有一些速成班,一个星期学懂Tomcat,这些视频用来开阔一下视野是可以的,但想真正的掌握一个框架的话,还是要自己脚踏实地的,一行一行代码,一个个方法,一个个类去分析,了解他们的属性,这个过程虽然痛苦,茫然,无助,但只要你坚持,努力思考,找到类与类之间,方法与方法之间,方法与类属性之间的关系,当你再回头来看一个框架时,你会发现,原来如此。 话不多说,关于Tomcat 加载启动过程就告一段落,期待下一篇博客相见 。

    本文相关的源码
    https://github.com/quyixiao/tomcat.git
    https://github.com/quyixiao/servelet-test.git
    https://github.com/quyixiao/web-fragment-test.git

  • 相关阅读:
    上电浪涌电流
    Transformer英语-法语机器翻译实例
    Java获取随机数
    人工智能之深度学习
    JaxWsProxyFactoryBean
    【Unity Shader】屏幕后处理1.0:调整亮度/饱和度/对比度
    Nginx反向代理
    乙方策划人员的内心独白:写不完的案子,是工作的常态吗?
    chapter14-集合——(List)——day17
    ASPX与ASP URL传递值问题
  • 原文地址:https://blog.csdn.net/quyixiao/article/details/127533144