Tomcat系列3-启动阶段源码分析_url_request_job_filtered_bytes_read堵塞请求-程序员宅基地

技术标签: tomcat  tomcat启动  

Tomcat的初始化流程结束后,就开始Tomcat各组件启动流程。初始化方法是Bootstrap的main方法中的daemon.load(args),启动就是它后续的daemon.start()。这个方法调用的是Catalina的start方法。

1.Catalina

  public void start() {
        log.info("Catalina--------start()");
        if (getServer() == null) {
        	//如果没有初始化tomcat,调用初始化方法
            load();
        }

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        long t1 = System.nanoTime();

        // Start the new server
        try {
        	//调用StandardServer的start方法
            getServer().start();
        } catch (LifecycleException e) {
            log.fatal(sm.getString("catalina.serverStartFail"), e);
            try {
                getServer().destroy();
            } catch (LifecycleException e1) {
                log.debug("destroy() failed for failed Server ", e1);
            }
            return;
        }
        long t2 = System.nanoTime();
        if(log.isInfoEnabled()) {
            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
        }
        // Register shutdown hook
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        false);
            }
        }

        if (await) {
            await();
            stop();
        }
    }

Catalina的启动:

  • 如果tomcat没有初始化,开始初始化
  • 调用StandardServer的start方法

2.StandardServer

(1)start方法

  public final synchronized void start() throws LifecycleException {
        if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
                LifecycleState.STARTED.equals(state)) {
            if (log.isDebugEnabled()) {
                Exception e = new LifecycleException();
                log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
            } else if (log.isInfoEnabled()) {
                log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
            }

            return;
        }
        //容器还没初始化,调用初始化方法
        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
        	//停止容器
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        try {
        	//设置容器状态为before_start
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            //调用容器实现的启动方法
            startInternal();
            if (state.equals(LifecycleState.FAILED)) {

                stop();
            } else if (!state.equals(LifecycleState.STARTING)) {
                invalidTransition(Lifecycle.AFTER_START_EVENT);
            } else {
            	//设置容器状态为after_start
                setStateInternal(LifecycleState.STARTED, null, false);
            }
        } catch (Throwable t) {
            // This is an 'uncontrolled' failure so put the component into the
            // FAILED state and throw an exception.
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t);
        }
    }

和初始化方法一样,容器真正的启动是在容器自身的startInternal方法。
(2)startInternal方法

   protected void startInternal() throws LifecycleException {
        log.info("StandardServer--------start()");
        fireLifecycleEvent(CONFIGURE_START_EVENT, null);
        setState(LifecycleState.STARTING);

        globalNamingResources.start();

        // Start our defined Services
        synchronized (servicesLock) {
            for (int i = 0; i < services.length; i++) {
            	//调用StandardService的start方法
                services[i].start();
            }
        }
    }

Server的启动:

  • 设置容器状态为before_start
  • 循环调用Service的启动方法
  • 设置容器状态为after_start

3.StandardService

同样StandardService真正启动的方法在startInternal方法。

 protected void startInternal() throws LifecycleException {
        log.info("StandardService--------start()");
        if(log.isInfoEnabled())
            log.info(sm.getString("standardService.start.name", this.name));
        //设置容器状态为start
        setState(LifecycleState.STARTING);

        if (container != null) {
            synchronized (container) {
            	//调用StandardEngine的start方法
                container.start();
            }
        }
        synchronized (executors) {
            for (Executor executor: executors) {
            	//调用Executor的start方法
                executor.start();
            }
        }

        mapperListener.start();
        synchronized (connectorsLock) {
            for (Connector connector: connectors) {
                try {
                    // If it has already failed, don't try and start it
                    if (connector.getState() != LifecycleState.FAILED) {
                    	//调用connector的start方法
                        connector.start();
                    }
                } catch (Exception e) {
                    log.error(sm.getString(
                            "standardService.connector.startFailed",
                            connector), e);
                }
            }
        }
    }

Service的启动:

  • 设置容器状态为before_start
  • 调用Container的启动方法
  • 调用Executor的start方法
  • 调用Connector的start方法
  • 设置容器状态为after_start

4.StandardEngine

同样StandardEngine真正启动的方法在startInternal方法。

4.1 StandardEngine

    protected synchronized void startInternal() throws LifecycleException {

        // Log our server identification information
        if(log.isInfoEnabled())
            log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());

        // 调用父类ContainerBase的startInternal方法
        super.startInternal();
    }

4.2 super.startInternal()

 protected synchronized void startInternal() throws LifecycleException {

        // Start our subordinate components, if any
        logger = null;
        getLogger();
        //我们这里没配集群,Cluster为空
        Cluster cluster = getClusterInternal();
        if ((cluster != null) && (cluster instanceof Lifecycle))
            ((Lifecycle) cluster).start();
        Realm realm = getRealmInternal();
        if ((realm != null) && (realm instanceof Lifecycle))
            ((Lifecycle) realm).start();

        // Start our child containers, if any
        //找到容器的子容器,这里的容器是StandardEngine,子容器就是StandardHost
        Container children[] = findChildren();
        List<Future<Void>> results = new ArrayList<>();
        for (int i = 0; i < children.length; i++) {
        	//开启后台线程启动子容器,这里就是启动StandardHost
            results.add(startStopExecutor.submit(new StartChild(children[i])));
        }

        boolean fail = false;
        for (Future<Void> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                log.error(sm.getString("containerBase.threadedStartFailed"), e);
                fail = true;
            }

        }
        if (fail) {
            throw new LifecycleException(
                    sm.getString("containerBase.threadedStartFailed"));
        }

        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle)
            ((Lifecycle) pipeline).start();


        setState(LifecycleState.STARTING);

        // Start our thread
		//启动后台处理线程
        threadStart();

    }

我们在前一篇初始化说到的Host、Context、Valve初始化就在这里。findChildren会找到当前容器的子容器,然后调用它的start方法。这里启动的是StandardHost。StandardHost启动放到下一节讲,这里继续分析后面,这里会将自己的Pipeline管道启动,然后启动一个后台处理线程,处理的事情和我们主流程没关系,不分析。接下来看下StandardHost的start方法。
Engine启动:

  • 设置容器状态为before_start
  • 调用子容器StandardHost的start方法
  • 设置容器状态为after_start

4.2 StandardHost

在分析StandardHost的start方法前,先说下几个监听器。
我们再前面讲过Digester对象是用来解析serve.xml对象的。看一下它的创建方法。
(1)createStartDigester

 protected Digester createStartDigester() {
        long t1=System.currentTimeMillis();
        // Initialize the digester
        Digester digester = new Digester();
        digester.setValidating(false);
        digester.setRulesValidation(true);
        HashMap<Class<?>, List<String>> fakeAttributes = new HashMap<>();
        ArrayList<String> attrs = new ArrayList<>();
        attrs.add("className");
        fakeAttributes.put(Object.class, attrs);
        digester.setFakeAttributes(fakeAttributes);
        digester.setUseContextClassLoader(true);

        // Configure the actions we will be using
        digester.addObjectCreate("Server",
                                 "org.apache.catalina.core.StandardServer",
                                 "className");
        digester.addSetProperties("Server");
        digester.addSetNext("Server",
                            "setServer",
                            "org.apache.catalina.Server");

        digester.addObjectCreate("Server/GlobalNamingResources",
                                 "org.apache.catalina.deploy.NamingResourcesImpl");
        digester.addSetProperties("Server/GlobalNamingResources");
        digester.addSetNext("Server/GlobalNamingResources",
                            "setGlobalNamingResources",
                            "org.apache.catalina.deploy.NamingResourcesImpl");

        digester.addObjectCreate("Server/Listener",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties("Server/Listener");
        digester.addSetNext("Server/Listener",
                            "addLifecycleListener",
                            "org.apache.catalina.LifecycleListener");

        digester.addObjectCreate("Server/Service",
                                 "org.apache.catalina.core.StandardService",
                                 "className");
        digester.addSetProperties("Server/Service");
        digester.addSetNext("Server/Service",
                            "addService",
                            "org.apache.catalina.Service");

        digester.addObjectCreate("Server/Service/Listener",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties("Server/Service/Listener");
        digester.addSetNext("Server/Service/Listener",
                            "addLifecycleListener",
                            "org.apache.catalina.LifecycleListener");

        //Executor
        digester.addObjectCreate("Server/Service/Executor",
                         "org.apache.catalina.core.StandardThreadExecutor",
                         "className");
        digester.addSetProperties("Server/Service/Executor");

        digester.addSetNext("Server/Service/Executor",
                            "addExecutor",
                            "org.apache.catalina.Executor");


        digester.addRule("Server/Service/Connector",
                         new ConnectorCreateRule());
        digester.addRule("Server/Service/Connector",
                         new SetAllPropertiesRule(new String[]{"executor"}));
        digester.addSetNext("Server/Service/Connector",
                            "addConnector",
                            "org.apache.catalina.connector.Connector");


        digester.addObjectCreate("Server/Service/Connector/Listener",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties("Server/Service/Connector/Listener");
        digester.addSetNext("Server/Service/Connector/Listener",
                            "addLifecycleListener",
                            "org.apache.catalina.LifecycleListener");

        // Add RuleSets for nested elements
        digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
        digester.addRuleSet(new EngineRuleSet("Server/Service/"));
        digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
        digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
        addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/");
        digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));

        // When the 'engine' is found, set the parentClassLoader.
        digester.addRule("Server/Service/Engine",
                         new SetParentClassLoaderRule(parentClassLoader));
        addClusterRuleSet(digester, "Server/Service/Engine/Cluster/");

        long t2=System.currentTimeMillis();
        if (log.isDebugEnabled()) {
            log.debug("Digester for server.xml created " + ( t2-t1 ));
        }
        return (digester);

    }

基本就是按照tomcat的元素结构构造对象,看下其中构造Host和Context两个方法。
(2)HostRuleSet

        digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));

看下HostRuleSet对象的addRuleInstances方法

public void addRuleInstances(Digester digester) {

        digester.addObjectCreate(prefix + "Host",
                                 "org.apache.catalina.core.StandardHost",
                                 "className");
        digester.addSetProperties(prefix + "Host");
        digester.addRule(prefix + "Host",
                         new CopyParentClassLoaderRule());
         //新增了一个HostConfig监听器                
        digester.addRule(prefix + "Host",
                         new LifecycleListenerRule
                         ("org.apache.catalina.startup.HostConfig",
                          "hostConfigClass"));
        digester.addSetNext(prefix + "Host",
                            "addChild",
                            "org.apache.catalina.Container");

        digester.addCallMethod(prefix + "Host/Alias",
                               "addAlias", 0);

        //Cluster configuration start
        digester.addObjectCreate(prefix + "Host/Cluster",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties(prefix + "Host/Cluster");
        digester.addSetNext(prefix + "Host/Cluster",
                            "setCluster",
                            "org.apache.catalina.Cluster");
        //Cluster configuration end

        digester.addObjectCreate(prefix + "Host/Listener",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties(prefix + "Host/Listener");
        digester.addSetNext(prefix + "Host/Listener",
                            "addLifecycleListener",
                            "org.apache.catalina.LifecycleListener");

        digester.addRuleSet(new RealmRuleSet(prefix + "Host/"));

        digester.addObjectCreate(prefix + "Host/Valve",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties(prefix + "Host/Valve");
        digester.addSetNext(prefix + "Host/Valve",
                            "addValve",
                            "org.apache.catalina.Valve");

    }

对于Context一样,也是增加了一个监听器,这里不贴代码了。回到StandardHost的start方法
(3)StandardHost的start
StandardHost和其他容器一样,start方法调用父类的start方法。

 public final synchronized void start() throws LifecycleException {
        if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
                LifecycleState.STARTED.equals(state)) {
            if (log.isDebugEnabled()) {
                Exception e = new LifecycleException();
                log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
            } else if (log.isInfoEnabled()) {
                log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
            }

            return;
        }
        //容器还没初始化,调用初始化方法
        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
        	//停止容器
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        try {
        	//设置容器状态为before_start
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            //调用容器实现的启动方法
            startInternal();
            if (state.equals(LifecycleState.FAILED)) {

                stop();
            } else if (!state.equals(LifecycleState.STARTING)) {
                invalidTransition(Lifecycle.AFTER_START_EVENT);
            } else {
            	//设置容器状态为after_start
                setStateInternal(LifecycleState.STARTED, null, false);
            }
        } catch (Throwable t) {
            // This is an 'uncontrolled' failure so put the component into the
            // FAILED state and throw an exception.
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t);
        }
    }

由于此时StandardHost还未初始化过,所以调用它的init方法。init方法也是父类的方法。
初始化过程

  public final synchronized void init() throws LifecycleException {
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }

        try {
        	//设置状态为before_init
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            initInternal();
        	//设置状态为after_init
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            throw new LifecycleException(
                    sm.getString("lifecycleBase.initFail",toString()), t);
        }
    }

在将StandardHost状态设值为before_init后,前面我们将的监听器就会触发事件,调用HostConfig的lifecycleEvent方法。

    public void lifecycleEvent(LifecycleEvent event) {

        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
                setContextClass(((StandardHost) host).getContextClass());
            }
        } catch (ClassCastException e) {
            log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
            check();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
		//状态为before_init时调用
            beforeStart();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
		//状态为start时调用
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }

此时还处于before_init阶段,所以调用beforeStart方法,这个方法只是创建所需的文件目录。
然后StandardHost继续执行自己初始化方法,调用自己的initInternal方法。StandardHost没实现这个方法,调用的是其父类ContainerBase的方法。前面说过,只是创建了线程池等工作,不是我们关注点。
接着StandardHost将自己状态设值为after_init。初始化方法结束,接着执行自己的start部分。
start部分
首先设值自己状态为before_start,然后调用自身的startInternal方法。

 protected synchronized void startInternal() throws LifecycleException {

        // Set error report valve
    	//给容器自身设置一系列组件,放入Pipeline管理中
        String errorValve = getErrorReportValveClass();
        if ((errorValve != null) && (!errorValve.equals(""))) {
            try {
                boolean found = false;
                Valve[] valves = getPipeline().getValves();
                for (Valve valve : valves) {
                    if (errorValve.equals(valve.getClass().getName())) {
                        found = true;
                        break;
                    }
                }
                if(!found) {
                    Valve valve =
                        (Valve) Class.forName(errorValve).newInstance();
                    getPipeline().addValve(valve);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString(
                        "standardHost.invalidErrorReportValveClass",
                        errorValve), t);
            }
        }
        super.startInternal();
    }

这里是定义本容器的Pipeline管道中有哪些组件,后续请求进入Host容器要经过这些组件。然后super.startInternal()和上面一样,又是开始找当前容器的子容器启动,这里子容器变成了StandardContext,这里同样启动StandardContext,这个放到下一节再分析。
StandardContext容器启动后,StandardHost会将自己的Pipeline管道启动。将自身状态设值为start。这里又将触发前面说的监听器,调用HostConfig的start方法。

  public void start() {

        if (log.isDebugEnabled())
            log.debug(sm.getString("hostConfig.start"));

        try {
            ObjectName hostON = host.getObjectName();
            oname = new ObjectName
                (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
            Registry.getRegistry(null, null).registerComponent
                (this, oname, this.getClass().getName());
        } catch (Exception e) {
            log.error(sm.getString("hostConfig.jmx.register", oname), e);
        }

        if (!host.getAppBaseFile().isDirectory()) {
            log.error(sm.getString("hostConfig.appBase", host.getName(),
                    host.getAppBaseFile().getPath()));
            host.setDeployOnStartup(false);
            host.setAutoDeploy(false);
        }

        if (host.getDeployOnStartup())
        	//如果开启了自动部署,进行自动部署
            deployApps();

    }

这里就是自动部署的核心所在。

   protected void deployApps() {

        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);

    }

这个方法看以看到tomcat的三种部署方式,具体的不再分析,不是我们的重点。
然后回到刚才地方执行threadStart方法

    protected void threadStart() {

        if (thread != null)
            return;
        if (backgroundProcessorDelay <= 0)
            return;

        threadDone = false;
        String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
        thread = new Thread(new ContainerBackgroundProcessor(), threadName);
        thread.setDaemon(true);
        thread.start();

    }

这里开启了一个后台线程。主要做日志打印,注册监听器,和主流程没多大关系,这里不分析。
最后StandardHost将自己状态设值为after_start。启动过程全部结束。
接下来分析StandardContext的启动。
Host启动:

  • 设置容器状态为before_init
  • 调用容器的init方法
  • 设置容器状态为after_init
  • 设置容器状态为before_start
  • 调用子容器StandardContext的start方法
  • 设值容器状态为start,这里会触发自动部署
  • 设置容器状态为after_start

4.3 StandardContext

流程和上述差不多,老规矩,StandardContext启动的时候由于前面未初始化,所以先调用初始化方法。和Host一样,容器的状态的变化会触发监听器执行相应事件,Context对应的事件执行方法为ContextConfig的lifecycleEvent方法。

 public void lifecycleEvent(LifecycleEvent event) {

        // Identify the context we are associated with
        try {
            context = (Context) event.getLifecycle();
        } catch (ClassCastException e) {
            log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
            configureStart();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            beforeStart();
        } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
            // Restore docBase for management tools
            if (originalDocBase != null) {
                context.setDocBase(originalDocBase);
            }
        } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
            configureStop();
        } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
            init();
        } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
            destroy();
        }

    }

(1)initInternal
和之前容器一样,初始化方法的核心在initInternal,其他都是设置容器状态。
initInternal方法

  protected void initInternal() throws LifecycleException {
        super.initInternal();

        // Register the naming resources
        if (namingResources != null) {
            namingResources.init();
        }

        // Send j2ee.object.created notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.object.created",
                    this.getObjectName(), sequenceNumber.getAndIncrement());
            broadcaster.sendNotification(notification);
        }
    }

初始化方法执行完后,将状态设置为after_init,这时候触发监听器执行ContextConfig的init方法。

protected void init() {
        // Called from StandardContext.init()

        Digester contextDigester = createContextDigester();
        contextDigester.getParser();

        if (log.isDebugEnabled()) {
            log.debug(sm.getString("contextConfig.init"));
        }
        context.setConfigured(false);
        ok = true;

        contextConfig(contextDigester);

        webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
                context.getXmlValidation(), context.getXmlBlockExternal());
    }

为解析web,xml做准备。这时候初始化部分执行完毕,开始执行start部分。
(2)startInternal
start部分首先将状态设置为before_start,又触发监听器的beforeStart方法,对主流程分析无影响,不分析,接着执行startInternal方法。
这个方法就是在加载我们程序目录的依赖的jar包,解析web.xml,创建servlet等等,方法很长,有兴趣的可以每行研究下,这里只贴出部分代码。

protected synchronized void startInternal() throws LifecycleException {

      ...//省略部分代码

        if (getLoader() == null) {
		    //这里是设置类加载器WebappClassLoader
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }
     ...//省略部分代码

        try {
            if (ok) {
                // Start our subordinate components, if any
                Loader loader = getLoader();
                if ((loader != null) && (loader instanceof Lifecycle))
                    ((Lifecycle) loader).start();

                // since the loader just started, the webapp classloader is now
                // created.
                setClassLoaderProperty("clearReferencesRmiTargets",
                        getClearReferencesRmiTargets());
                setClassLoaderProperty("clearReferencesStatic",
                        getClearReferencesStatic());
                setClassLoaderProperty("clearReferencesStopThreads",
                        getClearReferencesStopThreads());
                setClassLoaderProperty("clearReferencesStopTimerThreads",
                        getClearReferencesStopTimerThreads());
                setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread",
                        getClearReferencesHttpClientKeepAliveThread());

                ...//省略部分代码
                //设置容器状态为start
                // Notify our interested LifecycleListeners
                fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
     		   ...//省略部分代码
              

在中间有一步将容器状态设置成为了configure_start,这里又将触发监听器执行ContextConfig的configureStart方法。这个方法不贴了,其中有一步调用了webConfig方法,前面说的解析web.xml文件,解析jar包,创建servlet的地方。
(3)webConfig
这个方法本身的注释写的很详细,不解释了。

 protected void webConfig() {
        
        Set<WebXml> defaults = new HashSet<>();
        defaults.add(getDefaultWebXmlFragment());

        WebXml webXml = createWebXml();

        // Parse context level web.xml
		//解析web.xml文件
        InputSource contextWebXml = getContextWebXmlSource();
        if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
            ok = false;
        }
		//这里就包含了我们熟悉的ApplicationContext
        ServletContext sContext = context.getServletContext();

        // Ordering is important here

        // Step 1. Identify all the JARs packaged with the application and those
        // provided by the container. If any of the application JARs have a
        // web-fragment.xml it will be parsed at this point. web-fragment.xml
        // files are ignored for container provided JARs.
        Map<String,WebXml> fragments = processJarsForWebFragments(webXml);

        // Step 2. Order the fragments.
        Set<WebXml> orderedFragments = null;
        orderedFragments =
                WebXml.orderWebFragments(webXml, fragments, sContext);

        // Step 3. Look for ServletContainerInitializer implementations
        if (ok) {
            processServletContainerInitializers();
        }
		//下面就是解析jar包
        if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
            // Step 4. Process /WEB-INF/classes for annotations and
            // @HandlesTypes matches
            if (ok) {
                WebResource[] webResources =
                        context.getResources().listResources("/WEB-INF/classes");

                for (WebResource webResource : webResources) {
                    // Skip the META-INF directory from any JARs that have been
                    // expanded in to WEB-INF/classes (sometimes IDEs do this).
                    if ("META-INF".equals(webResource.getName())) {
                        continue;
                    }
                    processAnnotationsWebResource(webResource, webXml,
                            webXml.isMetadataComplete());
                }
            }

            // Step 5. Process JARs for annotations and
            // @HandlesTypes matches - only need to process those fragments we
            // are going to use (remember orderedFragments includes any
            // container fragments)
            if (ok) {
                processAnnotations(
                        orderedFragments, webXml.isMetadataComplete());
            }

            // Cache, if used, is no longer required so clear it
            javaClassCache.clear();
        }

        if (!webXml.isMetadataComplete()) {
            // Step 6. Merge web-fragment.xml files into the main web.xml
            // file.
            if (ok) {
                ok = webXml.merge(orderedFragments);
            }

            // Step 7. Apply global defaults
            // Have to merge defaults before JSP conversion since defaults
            // provide JSP servlet definition.
            webXml.merge(defaults);

            // Step 8. Convert explicitly mentioned jsps to servlets
            if (ok) {
                convertJsps(webXml);
            }

            // Step 9. Apply merged web.xml to Context
            if (ok) {
                configureContext(webXml);
            }
        } else {
            webXml.merge(defaults)
			//解析jsp
            convertJsps(webXml);
			//解析servlet和filter就在这个里面
            configureContext(webXml);
        }

        // Step 9a. Make the merged web.xml available to other
        // components.
        String mergedWebXml = webXml.toXml();
        @SuppressWarnings("deprecation")
        String attributeName = org.apache.tomcat.util.scan.Constants.MERGED_WEB_XML;
        sContext.setAttribute(attributeName, mergedWebXml);
        if (context.getLogEffectiveWebXml()) {
            log.info("web.xml:\n" + mergedWebXml);
        }

        // Always need to look for static resources
        // Step 10. Look for static resources packaged in JARs
        if (ok) {
            // Spec does not define an order.
            // Use ordered JARs followed by remaining JARs
            Set<WebXml> resourceJars = new LinkedHashSet<>();
            for (WebXml fragment : orderedFragments) {
                resourceJars.add(fragment);
            }
            for (WebXml fragment : fragments.values()) {
                if (!resourceJars.contains(fragment)) {
                    resourceJars.add(fragment);
                }
            }
            processResourceJARs(resourceJars);
            // See also StandardContext.resourcesStart() for
            // WEB-INF/classes/META-INF/resources configuration
        }

        // Step 11. Apply the ServletContainerInitializer config to the
        // context
        if (ok) {
            for (Map.Entry<ServletContainerInitializer,
                    Set<Class<?>>> entry :
                        initializerClassMap.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    context.addServletContainerInitializer(
                            entry.getKey(), null);
                } else {
                    context.addServletContainerInitializer(
                            entry.getKey(), entry.getValue());
                }
            }
        }
    }

再看下其中的configureContext方法

private void configureContext(WebXml webxml) {

        context.setPublicId(webxml.getPublicId());

    ...//省略部分代码

        for (Entry<String, String> entry : webxml.getContextParams().entrySet()) {
            context.addParameter(entry.getKey(), entry.getValue());
        }
        context.setDenyUncoveredHttpMethods(
                webxml.getDenyUncoveredHttpMethods());
        context.setDisplayName(webxml.getDisplayName());
        context.setDistributable(webxml.isDistributable());
        for (ContextLocalEjb ejbLocalRef : webxml.getEjbLocalRefs().values()) {
            context.getNamingResources().addLocalEjb(ejbLocalRef);
        }
        ...//省略部分代码
        for (ErrorPage errorPage : webxml.getErrorPages().values()) {
            context.addErrorPage(errorPage);
        }
        for (FilterDef filter : webxml.getFilters().values()) {
            if (filter.getAsyncSupported() == null) {
                filter.setAsyncSupported("false");
            }
            context.addFilterDef(filter);
        }
        for (FilterMap filterMap : webxml.getFilterMappings()) {
            context.addFilterMap(filterMap);
        }
        context.setJspConfigDescriptor(webxml.getJspConfigDescriptor());
        for (String listener : webxml.getListeners()) {
            context.addApplicationListener(listener);
        }
        for (Entry<String, String> entry :
                webxml.getLocaleEncodingMappings().entrySet()) {
            context.addLocaleEncodingMappingParameter(entry.getKey(),
                    entry.getValue());
        }
         ...//省略部分代码
        for (String role : webxml.getSecurityRoles()) {
            context.addSecurityRole(role);
        }
        for (ContextService service : webxml.getServiceRefs().values()) {
            context.getNamingResources().addService(service);
        }
        for (ServletDef servlet : webxml.getServlets().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<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
            wrapper.setServletClass(servlet.getServletClass());
             ...//省略部分代码
            context.addChild(wrapper);
        }
        for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        SessionConfig sessionConfig = webxml.getSessionConfig();
        if (sessionConfig != null) {
            if (sessionConfig.getSessionTimeout() != null) {
                context.setSessionTimeout(
                        sessionConfig.getSessionTimeout().intValue());
            }
            SessionCookieConfig scc =
                context.getServletContext().getSessionCookieConfig();
            scc.setName(sessionConfig.getCookieName());
            scc.setDomain(sessionConfig.getCookieDomain());
            scc.setPath(sessionConfig.getCookiePath());
            scc.setComment(sessionConfig.getCookieComment());
               ...//省略部分代码

        // Context doesn't use version directly

        for (String welcomeFile : webxml.getWelcomeFiles()) {
     
            if (welcomeFile != null && welcomeFile.length() > 0) {
                context.addWelcomeFile(welcomeFile);
            }
        }

        // Do this last as it depends on servlets
        for (JspPropertyGroup jspPropertyGroup :
                webxml.getJspPropertyGroups()) {
            String jspServletName = context.findServletMapping("*.jsp");
            if (jspServletName == null) {
                jspServletName = "jsp";
            }
            if (context.findChild(jspServletName) != null) {
                for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
                    context.addServletMappingDecoded(urlPattern, jspServletName, true);
                }
            } else {
                if(log.isDebugEnabled()) {
                    for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
                        log.debug("Skipping " + urlPattern + " , no servlet " +
                                jspServletName);
                    }
                }
            }
        }

         ...//省略部分代码

是不是都是一些熟悉的元素,这里也体现了servlet最后都包装成了wrapper。到这里整个容器已经启动完毕,下面看下连接器的启动。

Context启动:

  • 设置容器状态为before_init,调用容器的init方法,设置容器状态为after_init
  • 设置容器状态为before_start,调用容器的start方法,设值容器状态为start,这里会触发解析web.xml文件,最后设置容器状态为after_start

5.Connector

同样,Connector启动方法核心在startInternal方法。
(1)startInternal

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);
        }
    }

这里将Connector状态设置为start后,启动了ProtocolHandler。
(2)ProtocolHandler的start

   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;
        }
    }

ProtocolHandler又启动了Endpoint。这里调用的是AbstractEndpoint的start方法。
(3)AbstractEndpoint的start

public final void start() throws Exception {
        if (bindState == BindState.UNBOUND) {
            bind();
            bindState = BindState.BOUND_ON_START;
        }
        startInternal();
    }

初始化时候已经调用过bind方法,这里继续执行startInternal方法。在NIO中调用的是NioEndpoint的startInternal,BIO下调用的是JIoEndpoint。我们看下NioEndpoint,BIO性能太差,基本不用。

5.1 NioEndpoint

(1)startInternal

public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getProcessorCache());
            eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                            socketProperties.getEventCache());
            nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getBufferPool());

            // Create worker collection
            //如果没有线程池,创建线程池
            if ( getExecutor() == null ) {
                createExecutor();
            }

            initializeConnectionLatch();

            // Start poller threads
            //getPollerThreadCount是在2和电脑的CPU逻辑线程数之间取最小值,我的是双核4线程,所以这里为2
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; i<pollers.length; i++) {
                pollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true);
                //启动Poller线程
                pollerThread.start();
            }
			//启动Acceptor线程
            startAcceptorThreads();
        }

先看下Acceptor线程
(2)startAcceptorThreads

protected final void startAcceptorThreads() {
        //默认为1
        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());
            //启动Acceptor线程
            t.start();
        }
    }

看下Acceptor线程run方法。
(3)run

public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                	//超出最大连接数等待
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                    	//接受连接
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        //we didn't get a socket
                        countDownConnection();
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // setSocketOptions() will add channel to the poller
                    // if successful
                    if (running && !paused) {
                    //处理连接
                        if (!setSocketOptions(socket)) {
                        //减少已连接数量,唤醒等待线程
                            countDownConnection();
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        closeSocket(socket);
                    }
                } catch (SocketTimeoutException sx) {
                    // Ignore: Normal condition
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (OutOfMemoryError oom) {
                    try {
                        oomParachuteData = null;
                        releaseCaches();
                        log.error("", oom);
                    }catch ( Throwable oomt ) {
                        try {
                            try {
                                System.err.println(oomParachuteMsg);
                                oomt.printStackTrace();
                            }catch (Throwable letsHopeWeDontGetHere){
                                ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                            }
                        }catch (Throwable letsHopeWeDontGetHere){
                            ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                        }
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }
    }

先看下最大连接数怎么限制countUpOrAwaitConnection

  public void countUpOrAwait() throws InterruptedException {
        if (log.isDebugEnabled()) {
            log.debug("Counting up["+Thread.currentThread().getName()+"] latch="+getCount());
        }
        sync.acquireSharedInterruptibly(1);
    }

这里就是前面并发编程里面讲的AQS了,这里不细讲,这里有个AtomicLong变量表示最大连接,超出这个数,线程将挂起等待。
再看下setSocketOptions方法
(4)setSocketOptions

 protected boolean setSocketOptions(SocketChannel socket) {
        // Process the connection
        try {
            //disable blocking, APR style, we are gonna be polling it
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);

            NioChannel channel = nioChannels.pop();
            if ( channel == null ) {
                // SSL setup
                if (sslContext != null) {
                    SSLEngine engine = createSSLEngine();
                    int appbufsize = engine.getSession().getApplicationBufferSize();
                    NioBufferHandler bufhandler = new NioBufferHandler(Math.max(appbufsize,socketProperties.getAppReadBufSize()),
                                                                       Math.max(appbufsize,socketProperties.getAppWriteBufSize()),
                                                                       socketProperties.getDirectBuffer());
                    channel = new SecureNioChannel(socket, engine, bufhandler, selectorPool);
                } else {
                    // normal tcp setup
                    NioBufferHandler bufhandler = new NioBufferHandler(socketProperties.getAppReadBufSize(),
                                                                       socketProperties.getAppWriteBufSize(),
                                                                       socketProperties.getDirectBuffer());

                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                channel.setIOChannel(socket);
                if ( channel instanceof SecureNioChannel ) {
                    SSLEngine engine = createSSLEngine();
                    ((SecureNioChannel)channel).reset(engine);
                } else {
                    channel.reset();
                }
            }
            //连接封装成event放入队列
            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;
    }

(5)register
看下注册方法register

    public void register(final NioChannel socket) {
            socket.setPoller(this);
            KeyAttachment ka = new KeyAttachment(socket);
            ka.setPoller(this);
            ka.setTimeout(getSocketProperties().getSoTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());
            PollerEvent r = eventCache.pop();
            //socket关注的操作是OP_READ,读数据
            ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
            //封装成PollerEvent,并且表明将要添加的PollerEvent事件的执行会是将目标套接字执行操作OP_REGISTER
            if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
            else r.reset(socket,ka,OP_REGISTER);
            //放入队列
            addEvent(r);
        }
   private void addEvent(PollerEvent event) {
            events.offer(event);
            if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
        }

这里的Event也是一个线程,看下它的run方法

  public void run() {
            if ( interestOps == OP_REGISTER ) {
                try {
                	// 如果在待操作的socket上所关注的操作是OP_REGISTER,则将其注册到
                     // 待操作的socket的Poller的selector上关注其NIO事件OP_READ读数据
                    socket.getIOChannel().register(socket.getPoller().getSelector(), SelectionKey.OP_READ, key);
                } catch (Exception x) {
                    log.error("", x);
                }
            } else {
                final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
                try {
                    if (key == null) {
                        // The key was cancelled (e.g. due to socket closure)
                        // and removed from the selector while it was being
                        // processed. Count down the connections at this point
                        // since it won't have been counted down when the socket
                        // closed.
                        socket.getPoller().getEndpoint().countDownConnection();
                    } else {
                        final KeyAttachment att = (KeyAttachment) key.attachment();
                        if ( att!=null ) {
                            att.access();//to prevent timeout
                            //we are registering the key to start with, reset the fairness counter.
                            int ops = key.interestOps() | interestOps;
                            att.interestOps(ops);
                            key.interestOps(ops);
                        } else {
                            socket.getPoller().cancelledKey(key, SocketStatus.ERROR);
                        }
                    }
                } catch (CancelledKeyException ckx) {
                    try {
                        socket.getPoller().cancelledKey(key, SocketStatus.DISCONNECT);
                    } catch (Exception ignore) {}
                }
            }//end if
        }//run

所以Acceptor线程作用是将连接封装成PollerEvent放入队列,如果到达最大连接数,阻塞连接请求。
(6)Poller的run
再来看下Poller线程的run方法。

  public void run() {
            // Loop until destroy() is called
            while (true) {
                try {
                    // Loop if endpoint is paused
                    while (paused && (!close) ) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }

                    boolean hasEvents = false;

                    // Time to terminate?
                    if (close) {
                        events();
                        timeout(0, false);
                        try {
                            selector.close();
                        } catch (IOException ioe) {
                            log.error(sm.getString(
                                    "endpoint.nio.selectorCloseFail"), ioe);
                        }
                        break;
                    } else {
                    	//队列中是否有PollEnent,如果有,取出运行
                    	//这里实际上就是把连接请求从队列取出,然后将其注册到selector上,并标注关注读事件
                        hasEvents = events();
                    }
                    try {
                        if ( !close ) {
                            if (wakeupCounter.getAndSet(-1) > 0) {
                                //if we are here, means we have other stuff to do
                                //do a non blocking select
                                keyCount = selector.selectNow();
                            } else {
                                keyCount = selector.select(selectorTimeout);
                            }
                            wakeupCounter.set(0);
                        }
                        if (close) {
                            events();
                            timeout(0, false);
                            try {
                                selector.close();
                            } catch (IOException ioe) {
                                log.error(sm.getString(
                                        "endpoint.nio.selectorCloseFail"), ioe);
                            }
                            break;
                        }
                    } catch (Throwable x) {
                        ExceptionUtils.handleThrowable(x);
                        log.error("",x);
                        continue;
                    }
                    //either we timed out or we woke up, process events first
                    if ( keyCount == 0 ) hasEvents = (hasEvents | events());

                    Iterator<SelectionKey> iterator =
                        keyCount > 0 ? selector.selectedKeys().iterator() : null;
                    // Walk through the collection of ready keys and dispatch
                    // any active event.
                      //遍历处理所有待处理的NIO事件
                    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();
        }

(7)processKey
看下请求处理的方法processKey

 protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {
            boolean result = true;
            try {
                if ( close ) {
                    cancelledKey(sk, SocketStatus.STOP);
                } else if ( sk.isValid() && attachment != null ) {
                    // 如果参数SelectionKey有效并且带有附件
                    attachment.access();//make sure we don't time out valid sockets
                    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()) {
                                	 // 处理Socket NIO读操作,交给SocketProcessor和线程池完成
                                    if (!processSocket(attachment, SocketStatus.OPEN_READ, true)) {
                                        closeSocket = true;
                                    }
                                }
                                if (!closeSocket && sk.isWritable()) {
                               	 // 处理Socket NIO写操作,交给SocketProcessor和线程池完成
                                    if (!processSocket(attachment, SocketStatus.OPEN_WRITE, true)) {
                                        closeSocket = true;
                                    }
                                }
                                if (closeSocket) {
                                    cancelledKey(sk,SocketStatus.DISCONNECT);
                                }
                            } else {
                                result = false;
                            }
                        }
                    }
                } else {
                    //invalid key
                    cancelledKey(sk, SocketStatus.ERROR);
                }
            } catch ( CancelledKeyException ckx ) {
                cancelledKey(sk, SocketStatus.ERROR);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error("",t);
            }
            return result;
        }

(8)processSocket
processSocket方法:

 protected boolean processSocket(KeyAttachment attachment, SocketStatus status, boolean dispatch) {
        try {
            if (attachment == null) {
                return false;
            }
            SocketProcessor sc = processorCache.pop();
            if ( sc == null ) sc = new SocketProcessor(attachment, status);
            else sc.reset(attachment, status);
            //交给线程池去执行
            Executor executor = getExecutor();
            if (dispatch && executor != null) {
                executor.execute(sc);
            } else {
                sc.run();
            }
        } catch (RejectedExecutionException ree) {
            log.warn(sm.getString("endpoint.executor.fail", attachment.getSocket()), ree);
            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;
    }

(9)SocketProcessor
这里的SocketProcessor也是一个线程。

 public void run() {
            NioChannel socket = ka.getSocket();

            // 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.isUpgraded() && SocketStatus.OPEN_WRITE == status) {
                synchronized (ka.getWriteThreadLock()) {
                    doRun();
                }
            } else {
                synchronized (socket) {
                    doRun();
                }
            }
        }
 private void doRun() {
            NioChannel socket = ka.getSocket();
            SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());

            try {
                int handshake = -1;
                	// 这里的 handshake 是用来处理 https 的握手过程的,
            	// 如果是 http 不需要该握手阶段,下面会将该标志设置为 0, 表示握手已经完成
                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());
                            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
                    // 默认是读事件处理
                    // 这里的getHandler()返回AbstractProtocol.ConnectionHandler,
                    if (status == null) {
                        state = handler.process(ka, SocketStatus.OPEN_READ);
                    } else {
                        state = handler.process(ka, status);
                    }
                    if (state == SocketState.CLOSED) {
                        close(socket, key, SocketStatus.ERROR);
                    }
                } else if (handshake == -1 ) {
                    close(socket, key, SocketStatus.DISCONNECT);
                } else {
                    ka.getPoller().add(socket,handshake);
                }
            } catch (CancelledKeyException cx) {
                socket.getPoller().cancelledKey(key, null);
            } catch (OutOfMemoryError oom) {
                try {
                    oomParachuteData = null;
                    log.error("", oom);
                    socket.getPoller().cancelledKey(key,SocketStatus.ERROR);
                    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);
                socket.getPoller().cancelledKey(key,SocketStatus.ERROR);
            } finally {
                ka = null;
                status = null;
                //return to cache
                if (running && !paused) {
                    processorCache.push(this);
                }
            }
        }

(10)process
这里的handler是Http11ConnectionHandler。它的process方法是其父类的AbstractConnectionHandler的方法。

  public SocketState process(SocketWrapper<S> wrapper,
                SocketStatus status) {
            if (wrapper == null) {
                // Nothing to do. Socket has been closed.
                return SocketState.CLOSED;
            }

            S socket = wrapper.getSocket();
            if (socket == null) {
                // Nothing to do. Socket has been closed.
                return SocketState.CLOSED;
            }

            Processor<S> processor = connections.get(socket);
            if (status == SocketStatus.DISCONNECT && processor == null) {
                // Nothing to do. Endpoint requested a close and there is no
                // longer a processor associated with this socket.
                return SocketState.CLOSED;
            }

            wrapper.setAsync(false);
            ContainerThreadMarker.set();

            try {
                if (processor == null) {
                    processor = recycledProcessors.pop();
                }
                //创建processor
                if (processor == null) {
                    processor = createProcessor();
                }

                initSsl(wrapper, processor);

                SocketState state = SocketState.CLOSED;
                Iterator<DispatchType> dispatches = null;
                do {
                    if (dispatches != null) {
                        // Associate the processor with the connection as
                        // these calls may result in a nested call to process()
                        connections.put(socket, processor);
                        DispatchType nextDispatch = dispatches.next();
                        if (processor.isUpgrade()) {
                            state = processor.upgradeDispatch(
                                    nextDispatch.getSocketStatus());
                        } else {
                            state = processor.asyncDispatch(
                                    nextDispatch.getSocketStatus());
                        }
                    } else if (processor.isComet()) {
                        state = processor.event(status);
                    } else if (processor.isUpgrade()) {
                        state = processor.upgradeDispatch(status);
                    } else if (status == SocketStatus.DISCONNECT) {
                        // Comet and upgrade need to see DISCONNECT but the
                        // others don't. NO-OP and let socket close.
                    } else if (processor.isAsync() || state == SocketState.ASYNC_END) {
                        state = processor.asyncDispatch(status);
                        if (state == SocketState.OPEN) {
                           
                        }
                    } else if (status == SocketStatus.OPEN_WRITE) {
                        // Extra write event likely after async, ignore
                        state = SocketState.LONG;
                    } else {
                    	//处理请求
                        state = processor.process(wrapper);
                    }

                    if (state != SocketState.CLOSED && processor.isAsync()) {
                        state = processor.asyncPostProcess();
                    }

                    if (state == SocketState.UPGRADING) {
                      ...//省略部分代码
                    }
                  ...//省略部分代码
                } while (state == SocketState.ASYNC_END ||
                        state == SocketState.UPGRADING ||
                        dispatches != null && state != SocketState.CLOSED);

               ...//省略部分代码
        }

看下Processor对象的process方法。

 public SocketState process(SocketWrapper<S> socketWrapper)
        throws IOException {
        RequestInfo rp = request.getRequestProcessor();
        rp.setStage(org.apache.coyote.Constants.STAGE_PARSE);

        // Setting up the I/O
        setSocketWrapper(socketWrapper);
        //绑定了socket和InputBuffer
        getInputBuffer().init(socketWrapper, endpoint);
        //绑定了socket和OutputBuffer
        getOutputBuffer().init(socketWrapper, endpoint);
       ...//省略部分代码
        while (!getErrorState().isError() && keepAlive && !comet && !isAsync() &&
                upgradeToken == null && !endpoint.isPaused()) {

            // Parsing the request header
            try {
                setRequestLineReadTimeout();

                if (!getInputBuffer().parseRequestLine(keptAlive)) {
                    if (handleIncompleteRequestLineRead()) {
                        break;
                    }
                }

                if (endpoint.isPaused()) {
                    // 503 - Service unavailable
                    response.setStatus(503);
                    setErrorState(ErrorState.CLOSE_CLEAN, null);
                } else {
                   ...//省略部分代码
                }
            } catch (IOException e) {
               ...//省略部分代码
            } catch (Throwable t) {
                  ...//省略部分代码
            }
  ...//省略部分代码

            if (maxKeepAliveRequests == 1) {
                keepAlive = false;
            } else if (maxKeepAliveRequests > 0 &&
                    socketWrapper.decrementKeepAlive() <= 0) {
                keepAlive = false;
            }

            // Process the request in the adapter
            if (!getErrorState().isError()) {
                try {
                    rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
                    //处理请求
                    getAdapter().service(request, response);
                   ...//省略部分代码
                    setCometTimeouts(socketWrapper);
                } catch (InterruptedIOException e) {
                    ...//省略部分代码
                } catch (HeadersTooLargeException e) {
                    ...//省略部分代码
                } catch (Throwable t) {
                    ...//省略部分代码
                }
            }

            // Finish the handling of the request
            rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT);

            if (!isAsync() && !comet) {
                if (getErrorState().isError()) {
                    // If we know we are closing the connection, don't drain
                    // input. This way uploading a 100GB file doesn't tie up the
                    // thread if the servlet has rejected it.
                    getInputBuffer().setSwallowInput(false);
                } else {
                    // Need to check this again here in case the response was
                    // committed before the error that requires the connection
                    // to be closed occurred.
                    checkExpectationAndResponseStatus();
                }
                endRequest();
            }

         ...//省略部分代码
            if (!isAsync() && !comet || getErrorState().isError()) {
                request.updateCounters();
                if (getErrorState().isIoAllowed()) {
                    getInputBuffer().nextRequest();
                    getOutputBuffer().nextRequest();
                }
            }

            if (!disableUploadTimeout) {
                if(endpoint.getSoTimeout() > 0) {
                    setSocketTimeout(endpoint.getSoTimeout());
                } else {
                    setSocketTimeout(0);
                }
            }

            rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE);

            if (breakKeepAliveLoop(socketWrapper)) {
                break;
            }
        }

        rp.setStage(org.apache.coyote.Constants.STAGE_ENDED);

         ...//省略部分代码
    }

(11)service
看下getAdapter().service,这个的getAdapter就是我们前面的CoyoteAdapter。

 public void service(org.apache.coyote.Request req,
                        org.apache.coyote.Response res)
        throws Exception {
/下面在给Request和Response赋值,代码省略部分
       ...
        try {
            // Parse and set Catalina and configuration specific
            // request parameters
            req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
            //这个方法会找出当前请求对应的Host、Context
            postParseSuccess = postParseRequest(req, request, res, response);
            if (postParseSuccess) {
                //check valves if we support async
                request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
                // Calling the container
                //调用Container的Pipeline的invoke方法
                //这里会调用Container的Pipeline里面的所有vavle一个个处理,然后最后一个又调用了host的Pipeline里面的vavle,然后是Context,Servlet
                connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

                if (request.isComet()) {
                    if (!response.isClosed() && !response.isError()) {
                        comet = true;
                        res.action(ActionCode.COMET_BEGIN, null);
                        if (request.getAvailable() || (request.getContentLength() > 0 && (!request.isParametersParsed()))) {
                            // Invoke a read event right away if there are available bytes
                            event(req, res, SocketStatus.OPEN_READ);
                        }
                    } else {
                        // Clear the filter chain, as otherwise it will not be reset elsewhere
                        // since this is a Comet request
                        request.setFilterChain(null);
                    }
                }
            }

            if (request.isAsync()) {
                async = true;
                ReadListener readListener = req.getReadListener();
                if (readListener != null && request.isFinished()) {
                    // Possible the all data may have been read during service()
                    // method so this needs to be checked here
                    ClassLoader oldCL = null;
                    try {
                        oldCL = request.getContext().bind(false, null);
                        if (req.sendAllDataReadEvent()) {
                            req.getReadListener().onAllDataRead();
                        }
                    } finally {
                        request.getContext().unbind(false, oldCL);
                    }
                }
                Throwable throwable =
                        (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

                // If an async request was started, is not going to end once
                // this container thread finishes and an error occurred, trigger
                // the async error process
                if (!request.isAsyncCompleting() && throwable != null) {
                    request.getAsyncContextInternal().setErrorState(throwable, true);
                }
            } else if (!comet) {
                request.finishRequest();
                //把Response输出给页面
                response.finishResponse();
            }
        } catch (IOException e) {
            // Ignore
        } finally {
            AtomicBoolean error = new AtomicBoolean(false);
            res.action(ActionCode.IS_ERROR, error);

            if (request.isAsyncCompleting() && error.get()) {
                // Connection will be forcibly closed which will prevent
                // completion happening at the usual point. Need to trigger
                // call to onComplete() here.
                res.action(ActionCode.ASYNC_POST_PROCESS,  null);
                async = false;
            }

         ...//省略部分代码

    }

到此整连接器的启动结束了,过程是(我们这里分析的是NIO):

  • 调用ProtocolHandler的start方法,ProtocolHandler又会调用NioEndpoint的start方法
  • NioEndpoint启动Poller线程等待从队列获取连接,然后启动Acceptor线程接收连接方法队列,同时Acceptor线程会判断超时最大连接数时会阻塞
  • Poller线程从队列取出连接将其注册到selector,在真正有读请求时,会调用work线程处理(在线程池中处理的)
  • work线程中会调用Http11ConnectionHandler处理,Http11ConnectionHandler会调用processor处理
  • processor会将IO和socket绑定,然后调用CoyoteAdapter处理
  • CoyoteAdapter将会调用Host的Pipeline链,Host的最后一个valve调用Context的Pipeline链,Context最后将调用servlet处理请求。最后响应再一层层的返回,最终返回客户端。
    这里有个Poller线程,还记得在初始化的时候也有个Poller线程,在NioBlockingSelector中。这两个有什么区别:
    上面的Poller线程是获取连接,注册到selector,当请求处理完返回响应的时候,会调用NioBlockingSelector的write写数据,如果超时或者当前缓冲区已满,就把这个响应注册到NioBlockingSelector中的selector,由NioBlockingSelector的Poller线程后续将数据写回到客户端。
    这里的目的是将各个Channel的就绪事件分散注册到不同的Selector对象中,避免大量Channel集中注册就绪事件到一个Selector对象,影响性能。
    (12)postParseRequest
    这个是前面代码注释写的一个方法,tomcat可能配多个虚拟机、每个虚拟机下面也会有对个应用,当我们一个请求来时,怎么判定走哪个。这个方法就是用来做判断的。
    postParseRequest内部核心判断只有一个方法,其他的代码这里不贴了。
 connector.getService().getMapper().map(serverName, decodedURI,
                    version, request.getMappingData())

map方法如下:

    public void map(MessageBytes host, MessageBytes uri, String version,
                    MappingData mappingData) throws IOException {

        if (host.isNull()) {
            host.getCharChunk().append(defaultHostName);
        }
        host.toChars();
        uri.toChars();
        internalMap(host.getCharChunk(), uri.getCharChunk(), version,
                mappingData);

    }

internalMap方法会根据请求路径来选择host和context。

 private final void internalMap(CharChunk host, CharChunk uri,
            String version, MappingData mappingData) throws IOException {

        if (mappingData.host != null) {
            // The legacy code (dating down at least to Tomcat 4.1) just
            // skipped all mapping work in this case. That behaviour has a risk
            // of returning an inconsistent result.
            // I do not see a valid use case for it.
            throw new AssertionError();
        }

        uri.setLimit(-1);

        // Virtual host mapping
        MappedHost[] hosts = this.hosts;
        MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
        if (mappedHost == null) {
            if (defaultHostName == null) {
                return;
            }
            //根据路径选择host
            mappedHost = exactFind(hosts, defaultHostName);
            if (mappedHost == null) {
                return;
            }
        }
        mappingData.host = mappedHost.object;

        // Context mapping
        ContextList contextList = mappedHost.contextList;
        MappedContext[] contexts = contextList.contexts;
        //根据路径选择context
        int pos = find(contexts, uri);
        if (pos == -1) {
            return;
        }

        int lastSlash = -1;
        int uriEnd = uri.getEnd();
        int length = -1;
        boolean found = false;
        MappedContext context = null;
        while (pos >= 0) {
            context = contexts[pos];
            if (uri.startsWith(context.name)) {
                length = context.name.length();
                if (uri.getLength() == length) {
                    found = true;
                    break;
                } else if (uri.startsWithIgnoreCase("/", length)) {
                    found = true;
                    break;
                }
            }
            if (lastSlash == -1) {
                lastSlash = nthSlash(uri, contextList.nesting + 1);
            } else {
                lastSlash = lastSlash(uri);
            }
            uri.setEnd(lastSlash);
            pos = find(contexts, uri);
        }
        uri.setEnd(uriEnd);

        if (!found) {
            if (contexts[0].name.equals("")) {
                context = contexts[0];
            } else {
                context = null;
            }
        }
        if (context == null) {
            return;
        }

        mappingData.contextPath.setString(context.name);

        ContextVersion contextVersion = null;
        ContextVersion[] contextVersions = context.versions;
        final int versionCount = contextVersions.length;
        if (versionCount > 1) {
            Context[] contextObjects = new Context[contextVersions.length];
            for (int i = 0; i < contextObjects.length; i++) {
                contextObjects[i] = contextVersions[i].object;
            }
            mappingData.contexts = contextObjects;
            if (version != null) {
                contextVersion = exactFind(contextVersions, version);
            }
        }
        if (contextVersion == null) {
            // Return the latest version
            // The versions array is known to contain at least one element
            contextVersion = contextVersions[versionCount - 1];
        }
        mappingData.context = contextVersion.object;
        mappingData.contextSlashCount = contextVersion.slashCount;

        // Wrapper mapping
        if (!contextVersion.isPaused()) {
            internalMapWrapper(contextVersion, uri, mappingData);
        }

    }

6.总结

到这里tomcat的启动过程分析完了,基本和初始化一样,父容器会启动子容器。在其中加载应用的类、解析web.xml,创建servlet。启动Acceptor和Poller线程。
所以当一个请求来的时候,简单来说处理过程就是Acceptor接收线程放入队列,Poller线程消费队列,再调用容器一层层处理,最后将响应返回给客户端。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/huanshirenjian/article/details/90047048

智能推荐

关于OpenCore引导下没有install macOS……的解决方案_opencore boot menu下没有mac选项-程序员宅基地

文章浏览阅读2.7w次,点赞2次,收藏23次。前言*本方案同样也适用于SanPolicy配置问题导致的opencore找不到macOS/Windows启动项的问题昨天折腾黑苹果,想把原来的Catalina升级到Big Sur,使用的是黑果小兵的镜像,原来小兵的Catalina镜像我安装一切正常,用的clover引导。但是他的Big Sur镜像下了好几个版本,opencore引导始终都会卡在奇怪的代码上,clover引导能正常安装,但是重启两次过后就找不到macos的分区,为此头疼了很长时间。后来考虑到可能问题出在EFI上,原来的EFI是用小兵_opencore boot menu下没有mac选项

常用触摸屏485通讯引脚及下载口_触摸屏的常用通信接口-程序员宅基地

文章浏览阅读1.2w次,点赞2次,收藏14次。常用触摸屏485通讯引脚及下载口品牌型号通讯口通讯线引脚定义A+引脚定义B-引脚定义GND引脚定义其他短接线程序下载口MiniUSB-B程序下载口MicroUSB-B程序下载口USB-B程序下载口网口程序下载口U盘施耐德HMIGXU3500COM1DB9母头1,32,75无√威纶通TK6071IQCOM2DB9母头2..._触摸屏的常用通信接口

stm32f4xx.h文件的详解-程序员宅基地

文章浏览阅读6.6k次,点赞10次,收藏40次。stm32f4xx.h详解我的上一篇博客中详细讲解了system_stm32f4xx.c文件,在那个文件中,包含了唯一一个头文件,而这个头文件在开发过程中起到至关重要的作用,如果没有这个文件,就像大厦没有了根基,是肯定会倒的,所以,今天我们来深入了了解一下这么重要的文件它的内容到底是怎样的。受先我们先来看下这个文件的思维导图,以及该文件的介绍。这段文字已经大概地介绍了一下这个文件的内容,接下来,我们详细解释。首先,在这里大家可能有点懵逼,这是个什么东西,这就是混合编程的一种用法,在这里我们并没_stm32f4xx.h

xp linux双系统u盘,我装了XP在C盘,想在装个LINUX系统在F盘,实现双系统,我已经把Linux镜像制作成u盘系统了,...-程序员宅基地

文章浏览阅读49次。用U盘安装Linux系统的简单方法title veket Ckernel (hd0,0)/veket/vmlinuz root=/dev/ram0 PMEDIA=idehdinitrd (hd0,0)/veket/initrd.gztitle veket Dkernel (hd0,4)/veket/vmlinuz root=/dev/ram0 PMEDIA=idehdinitrd (hd0,4)/..._xp和veket安装双系统

编程世界的架构师之路:Assembly语言详解_assembly 语法-程序员宅基地

文章浏览阅读545次。Assembly语言,又称汇编语言,是计算机可识别的低级语言,与特定硬件体系结构密切相关。它使用助记符(Mnemonic)来代替二进制指令,使程序员更易于理解和编写底层指令。通过对Assembly语言的深入学习和实践,我们能更好地理解计算机底层运行机制,并具备针对性的优化能力。虽然现代编程语言的发展使得Assembly语言的应用范围相对较小,但它在特定领域的底层开发中依然发挥着重要作用。_assembly 语法

【经验】一劳永逸解决中文显示是大方块,不是乱码(最详细解释,有图有真相)_nx.draw汉字方块-程序员宅基地

文章浏览阅读1.4k次,点赞2次,收藏2次。问题描述大家好,我们在用 networkx 显示中文的时候,会发现不能显示中文。解决办法下载附件中的字体;在 jupyter notebook 中执行import matplotlibprint(matplotlib.__path__)找到 matplotlib 的路径,然后 cd 到这个路径。 cd 到这个路径之后,继续 cd,cd 到 mpl-data/fonts/ttf..._nx.draw汉字方块

随便推点

osg编译ffmpeg插件_osgmovie-程序员宅基地

文章浏览阅读2k次。osg版本:3.4.0ffmpeg版本:3.4.21 编译过程使用cmake配置osg的扩展插件,配置ffmpeg选项,如下图所示:配置完成生成并打开工程,编译ffmpeg插件,3.4版本中的ffmpeg代码错误,从git中下载高版本的插件源码并替换,重新编译,插件编译通过并生成osgdb_ffmpeg,dll。FFMPEG_STDINT_INCLUDE_DIR不能为空,否则不能生产插件工程; 可直接配置FFMPEG_ROOT路径,其它选择自动..._osgmovie

解决idea自动删除最后空格问题_idea eclips 不同工具提交代码时空格问题-程序员宅基地

文章浏览阅读2.2k次,点赞2次,收藏2次。解决idea自动删除空格问题,公司有个人一直用的eclipse,然后他那边更新的文件,我打开后再提交时,就会显示很多空格被删除了,如下图,就很烦实在忍不了百度Google一番后,发现这样操作可以解决这个问题,依次打开File-->Settings-->Editor-->Genera,拉到最下面然后把Strip trailing spaces on Save下拉框,原本是ALL,改为我图中的None就可以了..._idea eclips 不同工具提交代码时空格问题

thrift源码解析——深度学习模型的服务器端工程化落地方案_java项目深度学习算法工程化-程序员宅基地

文章浏览阅读405次。来源 | 极链AI云(性价比最高的共享GPU算力平台,双十活动进行中 10.9-10.11,充值就送!最多可送2500元现金券!免费试用地址:https://cloud.videojj.com/)文章来源:https://cloud.videojj.com/bbs/topic/28/thrift源码解析-深度学习模型的服务器端工程化落地方案/2有了训练好的模型,怎么用服务调用?很多人可能会想到用flask进行http调用。那如果是内网呢?如果希望去掉http封包解包一系列耗时操作呢?自然我们会._java项目深度学习算法工程化

Gantt - attachEvent事件监听 - 无参数事件_dhtmlxgantt onbeforecollapse-程序员宅基地

文章浏览阅读864次。Gantt - attachEvent事件监听 - 无参数事件_dhtmlxgantt onbeforecollapse

GraphPad Prism 如何更改数据表格式丨使用教程_graphpad怎么设置数字的格式-程序员宅基地

文章浏览阅读2.4k次。如果您的表格需要更多子列,可随时更改数据表中子列(副本)的数量。或者,如果您输入计算得到的错误值,但您的表格已标记为错误(假设您实际上输入的是SD,但该表的格式旨在用于SEM),则您可以进行切换。如需更改图表上显示的误差条类型,则无需更改数据表。取而代之的是,您可以双击图表上的任何数据点来打开图表类型对话框,并使用下拉菜单来更改误差条格式。如果您未选择合适的数据表类型,则将无法制作出您想要的图表类型或者执行您想要的分析。如需更改数据表的格式,点击数据表左上角的“表格格式”区域。_graphpad怎么设置数字的格式

Maven较完整教程_maevn详细文章教程-程序员宅基地

文章浏览阅读4.2w次,点赞13次,收藏47次。1. Maven介绍 1.1. 简介 java编写的用于构建系统的自动化工具。目前版本是2.0.9,注意maven2和maven1有很大区别,阅读第三方文档时需要区分版本。 1.2. Maven资源 见官方网站;The 5 minute test,官方简易入门文档;Getting Started Tutorial,官方入门文档;Build Cookbook,官方的cookbook;POM Ref..._maevn详细文章教程

推荐文章

热门文章

相关标签