嵌入式tomcat的使用

前言

       笔者在做项目的时候遇见一个比较过时的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
7月 08, 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 []
7月 08, 2019 7:53:48 下午 org.apache.coyote.AbstractProtocol init
信息: Initializing ProtocolHandler ["http-nio-8080"]
7月 08, 2019 7:53:48 下午 org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
信息: Using a shared selector for servlet write/read
7月 08, 2019 7:53:48 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service Tomcat
7月 08, 2019 7:53:48 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet Engine: Apache Tomcat/8.0.28
7月 08, 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
7月 08, 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 []
7月 08, 2019 10:26:51 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
7月 08, 2019 10:26:51 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet engine: [Apache Tomcat/9.0.21]
7月 08, 2019 10:26:51 下午 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
信息: No global web.xml found
7月 08, 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.
7月 08, 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
7月 08, 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 []
7月 08, 2019 10:31:53 下午 org.apache.coyote.AbstractProtocol init
信息: Initializing ProtocolHandler ["http-nio-8080"]
7月 08, 2019 10:31:54 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
7月 08, 2019 10:31:54 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet engine: [Apache Tomcat/9.0.21]
7月 08, 2019 10:31:54 下午 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
信息: No global web.xml found
7月 08, 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.
7月 08, 2019 10:31:54 下午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
警告: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [125] milliseconds.
7月 08, 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。 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值