嵌入式tomcat的使用

1,418 阅读4分钟

前言

       笔者在做项目的时候遇见一个比较过时的Spring封装框架,然后咔嚓咔嚓简化后,发现只能tomcat运行,但是BOSS却要求main方法启动,笔者受到spring boot的启发,想到了嵌入式tomcat。当然笔者的业务不适应转spring boot框架,不然优先使用spring boot了。

1. pom依赖

pom如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>boot-parent</artifactId>
        <groupId>com.feng.boot</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>embe-tomcat-demo</artifactId>

    <properties>
        <spring.version>4.3.24.RELEASE</spring.version>
        <embed.tomcat.version>8.0.28</embed.tomcat.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>


        <!--<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>-->

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${embed.tomcat.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>${embed.tomcat.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-logging-juli</artifactId>
            <version>${embed.tomcat.version}</version>
        </dependency>


    </dependencies>
</project>

2. main方法编写

这里使用classpath作为webapp目录,实际可用根据情况设定,这个目录下放置WEB-INF/web.xml文件,tomcat启动会加载这个文件。

package com.feng.tomcat;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;

import java.io.IOException;

public class TomcatStarter {

    private static int port = 8080;
    private static String contextPath = "/";

    public static void start() throws LifecycleException, IOException {
        Tomcat tomcat = new Tomcat();
        String baseDir = Thread.currentThread().getContextClassLoader().getResource("").getPath();
        tomcat.setBaseDir(baseDir);
        tomcat.setPort(port);
        //tomcat.setConnector(new Connector());

        tomcat.addWebapp(contextPath, baseDir);
        tomcat.enableNaming();
        tomcat.start();
        tomcat.getServer().await();
    }

    public static void main(String[] args) throws IOException, LifecycleException {
        start();
    }
}

 3. web.xml

这里使用servlet3.0,方便去除web.xml,使用javaBean实现web.xml能力(下一篇章说明)

<?xml version="1.0" encoding="UTF-8"?>
<web-app
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                            http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        id="WebApp_ID"
        version="3.0">


    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>com.feng.tomcat.servlet.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

web.xml

写一个servlet

package com.feng.tomcat.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        String res ="{\"hello\":\"world\",\"hi\":\"I`m a embd tomcat\"}";
        out.println(res);
        out.flush();
        out.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }

}

 4. 验证

启动main方法

"C:\Program Files\intellijIdea\jdk-12.0.1\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50624,suspend=y,server=n -javaagent:C:\Users\huahua\.IntelliJIdea2019.1\system\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "D:\intellijProject\boot-parent\embe-tomcat-demo\target\classes;D:\soft\apache-maven-3.6.1\rep\org\apache\tomcat\embed\tomcat-embed-core\8.0.28\tomcat-embed-core-8.0.28.jar;D:\soft\apache-maven-3.6.1\rep\org\apache\tomcat\embed\tomcat-embed-jasper\8.0.28\tomcat-embed-jasper-8.0.28.jar;D:\soft\apache-maven-3.6.1\rep\org\apache\tomcat\embed\tomcat-embed-el\8.0.28\tomcat-embed-el-8.0.28.jar;D:\soft\apache-maven-3.6.1\rep\org\eclipse\jdt\core\compiler\ecj\4.4.2\ecj-4.4.2.jar;D:\soft\apache-maven-3.6.1\rep\org\apache\tomcat\embed\tomcat-embed-logging-juli\8.0.28\tomcat-embed-logging-juli-8.0.28.jar;D:\soft\apache-maven-3.6.1\rep\org\apache\tomcat\embed\tomcat-embed-logging-log4j\8.0.28\tomcat-embed-logging-log4j-8.0.28.jar;C:\Program Files\intellijIdea\lib\idea_rt.jar" com.feng.tomcat.TomcatStarter
Connected to the target VM, address: '127.0.0.1:50624', transport: 'socket'
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
708, 2019 7:53:48 下午 org.apache.catalina.core.StandardContext setPath
警告: A context path must either be an empty string or start with a '/' and do not end with a '/'. The path [/] does not meet these criteria and has been changed to []
708, 2019 7:53:48 下午 org.apache.coyote.AbstractProtocol init
信息: Initializing ProtocolHandler ["http-nio-8080"]
708, 2019 7:53:48 下午 org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
信息: Using a shared selector for servlet write/read
708, 2019 7:53:48 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service Tomcat
708, 2019 7:53:48 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet Engine: Apache Tomcat/8.0.28
708, 2019 7:53:48 下午 org.apache.catalina.loader.WebappLoader buildClassPath
信息: Unknown loader jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d class jdk.internal.loader.ClassLoaders$AppClassLoader
7月 08, 2019 7:53:48 下午 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
信息: No global web.xml found
7月 08, 2019 7:53:49 下午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
信息: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [110] milliseconds.
7月 08, 2019 7:53:49 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-nio-8080"]

访问http://localhost:8080/hello,说明tomcat启动成功。

5.启动源码分析

首先看tomcat的架构图(经典图)

 

跟踪

tomcat.addWebapp(contextPath, baseDir); 
/**
     * @see #addWebapp(String, String)
     */
    public Context addWebapp(Host host, String contextPath, String docBase, ContextConfig config) {
        silence(host, contextPath);

        Context ctx = createContext(host, contextPath);
        ctx.setPath(contextPath);
        ctx.setDocBase(docBase);
        ctx.addLifecycleListener(new DefaultWebXmlListener());
        ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));

        ctx.addLifecycleListener(config);

        // prevent it from looking ( if it finds one - it'll have dup error )
        config.setDefaultWebXml(noDefaultWebXmlPath());

        if (host == null) {
            getHost().addChild(ctx);
        } else {
            host.addChild(ctx);
        }

        return ctx;
    }

创建容器,加载listener,加载content,吻合架构图设计

然后跟踪,创建Host

getHost()
public Host getHost() {
        if (host == null) {
            host = new StandardHost();
            host.setName(hostname);

            getEngine().addChild( host );
        }
        return host;
    }

创建引擎getEngine()

/**
     * Access to the engine, for further customization.
     */
    public Engine getEngine() {
        if(engine == null ) {
            getServer();
            engine = new StandardEngine();
            engine.setName( "Tomcat" );
            engine.setDefaultHost(hostname);
            engine.setRealm(createDefaultRealm());
            service.setContainer(engine);
        }
        return engine;
    }

创建server,getServer();

/**
     * Get the server object. You can add listeners and few more
     * customizations. JNDI is disabled by default.
     */
    public Server getServer() {

        if (server != null) {
            return server;
        }

        System.setProperty("catalina.useNaming", "false");

        server = new StandardServer();

        initBaseDir();

        server.setPort( -1 );

        service = new StandardService();
        service.setName("Tomcat");
        server.addService( service );
        return server;
    }

创建service,代码

service = new StandardService();

那么Connector什么时候创建呢?跟踪start方法

/**
     * Start the server.
     *
     * @throws LifecycleException
     */
    public void start() throws LifecycleException {
        getServer();
        getConnector();
        server.start();
    }

getConnector();

/**
     * Get the default http connector. You can set more
     * parameters - the port is already initialized.
     *
     * Alternatively, you can construct a Connector and set any params,
     * then call addConnector(Connector)
     *
     * @return A connector object that can be customized
     */
    public Connector getConnector() {
        getServer();
        if (connector != null) {
            return connector;
        }
        // This will load Apr connector if available,
        // default to nio. I'm having strange problems with apr
        // XXX: jfclere weird... Don't add the AprLifecycleListener then.
        // and for the use case the speed benefit wouldn't matter.

        connector = new Connector("HTTP/1.1");
        // connector = new Connector("org.apache.coyote.http11.Http11Protocol");
        connector.setPort(port);
        service.addConnector( connector );
        return connector;
    }

创建了connector,协议默认HTTP/1.1

/**
     * Set the Coyote protocol which will be used by the connector.
     *
     * @param protocol The Coyote protocol name
     */
    public void setProtocol(String protocol) {

        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.Http11NioProtocol");
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.ajp.AjpNioProtocol");
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
            }
        }

    }

默认NIO,条件支持APR,BIO已经退出历史舞台

6.启动过程与web.xml加载

启动过程省略,现在讲讲web.xml加载的过程

tomcat启动使用线程池异步启动,启动过程中加载web.xml文件

/**
     * Scan the web.xml files that apply to the web application and merge them
     * using the rules defined in the spec. For the global web.xml files,
     * where there is duplicate configuration, the most specific level wins. ie
     * an application's web.xml takes precedence over the host level or global
     * web.xml file.
     */
    protected void webConfig() {
        /*
         * Anything and everything can override the global and host defaults.
         * This is implemented in two parts
         * - Handle as a web fragment that gets added after everything else so
         *   everything else takes priority
         * - Mark Servlets as overridable so SCI configuration can replace
         *   configuration from the defaults
         */

        /*
         * The rules for annotation scanning are not as clear-cut as one might
         * think. Tomcat implements the following process:
         * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of
         *   which Servlet spec version is declared in web.xml. The EG has
         *   confirmed this is the expected behaviour.
         * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main
         *   web.xml is marked as metadata-complete, JARs are still processed
         *   for SCIs.
         * - If metadata-complete=true and an absolute ordering is specified,
         *   JARs excluded from the ordering are also excluded from the SCI
         *   processing.
         * - If an SCI has a @HandlesType annotation then all classes (except
         *   those in JARs excluded from an absolute ordering) need to be
         *   scanned to check if they match.
         */
        Set<WebXml> defaults = new HashSet<>();
        defaults.add(getDefaultWebXmlFragment());

        WebXml webXml = createWebXml();

        // Parse context level web.xml
        InputSource contextWebXml = getContextWebXmlSource();
        if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
            ok = false;
        }

getContextWebXmlSource()

stream = servletContext.getResourceAsStream
                    (Constants.ApplicationWebXml);
                try {
                    url = servletContext.getResource(
                            Constants.ApplicationWebXml);
                } catch (MalformedURLException e) {
                    log.error(sm.getString("contextConfig.applicationUrl"));
                }

这里就加载了web.xml文件,日志也可以看到

7.embed tomcat 9

笔者在升级tomcat 9的时候怎么也不能启动,究其根源是connector未创建,修改版本号

<properties>
        <spring.version>4.3.24.RELEASE</spring.version>
        <embed.tomcat.version>9.0.21</embed.tomcat.version>
    </properties>

<dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${embed.tomcat.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>${embed.tomcat.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>
        <!--<dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-logging-juli</artifactId>
            <version>${embed.tomcat.version}</version>
        </dependency>-->

日志

Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
708, 2019 10:26:34 下午 org.apache.catalina.core.StandardContext setPath
警告: A context path must either be an empty string or start with a '/' and do not end with a '/'. The path [/] does not meet these criteria and has been changed to []
708, 2019 10:26:51 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
708, 2019 10:26:51 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet engine: [Apache Tomcat/9.0.21]
708, 2019 10:26:51 下午 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
信息: No global web.xml found
708, 2019 10:26:51 下午 org.apache.jasper.servlet.TldScanner scanJars
信息: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
708, 2019 10:26:51 下午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
警告: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [125] milliseconds.

根源是start的时候没有创建connector,源码分析

/**
     * Start the server.
     *
     * @throws LifecycleException Start error
     */
    public void start() throws LifecycleException {
        getServer();
        server.start();
    }

getConnector();没有了,坑啊,耽误笔者几个小时

手动调用或者手动设置即可启动成功,修改启动类

public class TomcatStarter {

    private static int port = 8080;
    private static String contextPath = "/";

    public static void start() throws LifecycleException, IOException, ServletException {
        Tomcat tomcat = new Tomcat();
        String baseDir = Thread.currentThread().getContextClassLoader().getResource("").getPath();
        tomcat.setBaseDir(baseDir);
        tomcat.setPort(port);
        //tomcat.setConnector(new Connector());

        tomcat.addWebapp(contextPath, baseDir);
        tomcat.enableNaming();
        //手动创建
        tomcat.getConnector();
        tomcat.start();
        tomcat.getServer().await();
    }

    public static void main(String[] args) throws IOException, LifecycleException, ServletException {
        start();
    }
}

或者手动设置

public class TomcatStarter {

    private static int port = 8080;
    private static String contextPath = "/";

    public static void start() throws LifecycleException, IOException, ServletException {
        Tomcat tomcat = new Tomcat();
        String baseDir = Thread.currentThread().getContextClassLoader().getResource("").getPath();
        tomcat.setBaseDir(baseDir);
        tomcat.setPort(port);
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setPort(port);
        tomcat.setConnector(connector);

        tomcat.addWebapp(contextPath, baseDir);
        tomcat.enableNaming();
        //手动创建
        //tomcat.getConnector();
        tomcat.start();
        tomcat.getServer().await();
    }

    public static void main(String[] args) throws IOException, LifecycleException, ServletException {
        start();
    }
}

启动成功

Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
708, 2019 10:31:53 下午 org.apache.catalina.core.StandardContext setPath
警告: A context path must either be an empty string or start with a '/' and do not end with a '/'. The path [/] does not meet these criteria and has been changed to []
708, 2019 10:31:53 下午 org.apache.coyote.AbstractProtocol init
信息: Initializing ProtocolHandler ["http-nio-8080"]
708, 2019 10:31:54 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
708, 2019 10:31:54 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet engine: [Apache Tomcat/9.0.21]
708, 2019 10:31:54 下午 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
信息: No global web.xml found
708, 2019 10:31:54 下午 org.apache.jasper.servlet.TldScanner scanJars
信息: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
708, 2019 10:31:54 下午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
警告: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [125] milliseconds.
708, 2019 10:31:54 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-nio-8080"]

总结

       嵌入式tomcat,更轻便,更容易与代码集成与调试,设置灵活,遵循tomcat架构设计,极度推荐使用,一个main方法即可启动,Spring Boot就是这样使用的。Spring Boot就是在refreshContext的时候启动tomcat的

此处就创建了tomcat

 注意getWebServerFactory(),工厂创建

private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			ServletWebServerFactory factory = getWebServerFactory();
			this.webServer = factory.getWebServer(getSelfInitializer());
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

跟踪

进一步跟踪,此处有多种实现,工厂模式,主流有netty tomcat jetty,现在分析tomcat

难怪spring boot集成tomcat 9没问题,自己设置了connector了,?。创建了TomcatWebServer,spring boot自己封装的。

/**
	 * Factory method called to create the {@link TomcatWebServer}. Subclasses can
	 * override this method to return a different {@link TomcatWebServer} or apply
	 * additional processing to the Tomcat server.
	 * @param tomcat the Tomcat server.
	 * @return a new {@link TomcatWebServer} instance
	 */
	protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
		return new TomcatWebServer(tomcat, getPort() >= 0);
	}

在initial的时候start

Spring Boot Starter Web没那么神奇的能力,只是在很多地方集成了第三方的能力,实现了自动配置能力。下一章将不用web.xml文件启动嵌入式tomcat。