Embedded Tomcat tuning

SpringBoot embeds Web containers such as Tomcat/Jetty/Undertow - how is this done? Let's take Tomcat as an example and try to call embedded Tomcat.

To call embedded Tomcat, if you start it by default, a main function is enough.

simple example

Below is a simple example of starting Tomcat.

Tomcat tomcat = new Tomcat();
tomcat.enableNaming();
tomcat.getHost().setAutoDeploy(false);
tomcat.getHost().setAppBase("webapp");
// 在对应的 host 下面创建一个 context 并制定他的工作路径,会加载该目录下的所有 class 文件,或者静态文件
//        tomcat.setBaseDir(Thread.currentThread().getContextClassLoader().getResource("").getPath()); // 设置 tomcat 启动后的工作目录
//        System.out.println(Thread.currentThread().getContextClassLoader().getResource("").getPath());

// 读取项目路径
System.out.println(System.getProperty("user.dir"));
String jspDir = System.getProperty("user.dir");
StandardContext ctx = (StandardContext) tomcat.addWebapp("/", new File(jspDir).getAbsolutePath());
ctx.setReloadable(false);// 禁止重新载入
WebResourceRoot resources = new StandardRoot(ctx);// 创建WebRoot
resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));// tomcat 内部读取 Class 执行

// 创建连接器,并且添加对应的连接器,同时连接器指定端口 设置 IO 协议
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(port);
connector.setThrowOnFailure(true);

tomcat.getService().addConnector(connector);// 只能设置一个 service,直接拿默认的
tomcat.setConnector(connector); // 设置执行器

try {
    
    
	tomcat.start(); // tomcat 启动
} catch (LifecycleException e) {
    
    
	throw new RuntimeException(e);
}

tomcat.getServer().await(); // 保持主线程不退出,让其阻塞,不让当前线程结束,等待处理请求

Configure your Tomcat

Of course, we won't be satisfied with the default Tomcat configuration. Tomcat itself provides open configuration options, usually in the form of server.xml or web.xml. If you switch to embedded Tomcat, those xml configurations are no longer available, so we have to manually code (Programmatically) to complete the configuration in Java.

Faced with numerous Tomcat configurations, we have selected some of the most common ones below.

import lombok.Data;
import org.springframework.util.StringUtils;

/**
 * Tomcat 配置参数
 */
@Data
public class TomcatConfig {
    
    
    /**
     * 主机名称
     */
    private String hostName = "localhost";

    /**
     * 访问的端口
     */
    private Integer port = 8082;

    /**
     * Web 上下文目录
     */
    private String contextPath;

    /**
     * Web 目录的磁盘路径,如 D:/1sync/static
     */
    private String docBase;

    /**
     * Tomcat 临时文件的目录
     */
    private String tomcatBaseDir;

    /**
     * 关闭的端口
     */
    private Integer shutdownPort = 8005;

    /**
     * 是否激活 SSI(服务器端嵌入)
     */
    private Boolean enableSsi = false;

    /**
     * 是否激活 JSP
     */
    private Boolean enableJsp = true;

    /**
     * 是否激活 JMX 监控
     */
    private boolean enableJMX = false;

    /**
     * 自定义连接器
     */
    private boolean customerConnector = false;

    /**
     * 最大工作线程数 Maximum amount of worker threads.
     */
    private int maxThreads = 0;

    /**
     * 最小工作线程数,默认是 10。Minimum amount of worker threads. if not set, default value is 10
     */
    private int minSpareThreads = 0;

    /**
     * 当客户端从 Tomcat 获取数据时候,距离关闭连接的等待时间
     * When Tomcat expects data from the client, this is the time Tomcat will wait for that data to arrive before closing the connection.
     */
    private int connectionTimeout = 0;

    /**
     * 最大连接数
     * Maximum number of connections that the server will accept and process at any
     * given time. Once the limit has been reached, the operating system may still
     * accept connections based on the "acceptCount" property.
     */
    private int maxConnections = 0;

    /**
     * 当请求超过可用的线程试试,最大的请求排队数
     * Maximum queue length for incoming connection requests when all possible request processing threads are in use.
     */
    private int acceptCount = 0;

    /**
     * Tomcat 临时文件的目录。如果不需要(如不需要 jsp)禁止 work dir。
     * Tomcat needs a directory for temp files. This should be the first method called.
     *
     * <p>
     * By default, if this method is not called, we use:
     * <ul>
     *  <li>system properties - catalina.base, catalina.home</li>
     *  <li>$PWD/tomcat.$PORT</li>
     * </ul>
     * (/tmp doesn't seem a good choice for security).
     *
     * <p>
     * TODO: disable work dir if not needed ( no jsp, etc ).
     */
    public void setTomcatBaseDir(String tomcatBaseDir) {
    
    
        this.tomcatBaseDir = tomcatBaseDir;
    }

    public String getContextPath() {
    
    
        return StringUtils.hasText(contextPath) ? contextPath : "";
    }
}

Everyone should know the hostName host name and port port, so I won’t go into details. Other relevant configuration instructions are as follows:

  • Web context directory contextPath. It is the first-level directory. You don't need to set it, but don't set it /, otherwise there will be a warning; ""just set it to an empty string. Usually added.
  • The disk path docBase of the Web directory is the disk directory corresponding to the WebRoot. For example D:/1sync/static, the browser can access the static files and JSP files here.
  • The directory of Tomcat temporary files, tomcatBaseDir. Can not be set, the default is system properties - catalina.base, catalina.homeor $PWD/tomcat.$PORT. If there is no need to run JSP, or the directory can be disabled
  • enableSsi Whether to activate SSI (server-side embedding)
  • Shutdown port shutdownPort. You can close tomcat through Socket: telnet 127.0.0.1 8005, enter SHUTDOWNa string (methods will be introduced later)
  • Whether to activate JSP enableJsp
  • Whether to activate JMX monitor enableJMX. Used for JMX monitoring, turning off will increase startup speed
  • Other concurrent performance tuning maxThreads, minSpareThreads, connectionTimeout, maxConnections, acceptCount

Start Tomcat

With the configuration, Tomcat can naturally be started. We pass it TomcatConfigas a constructor parameter to TomcatStarterparse each parameter to configure Tomcat for final startup.

The following is started according to the default parameters.

TomcatConfig cfg = new TomcatConfig();
TomcatStarter t = new TomcatStarter(cfg);
t.start();

Insert image description here
In addition, I would like to add two configuration places:

  • Disabling Tomcat from automatically scanning jar packages will increase startup speed.
  • Tomcat's startStopThreads attribute is used to configure the thread pool size when the Tomcat server starts and shuts down. It determines the number of tasks that Tomcat can handle simultaneously during startup and shutdown. But for Tomcat 8, there is no direct programmatic way to set the startStopThreads attribute
  • The following settings: Set the number of core threads and the maximum number of threads without going so far as this is an open question

Insert image description here

The complete TomcatStarter source code is as follows.

import com.ajaxjs.Version;
import com.ajaxjs.framework.embeded_tomcat.jar_scan.EmbededContextConfig;
import com.ajaxjs.util.io.FileHelper;
import com.ajaxjs.util.io.Resources;
import com.ajaxjs.util.logger.LogHelper;
import org.apache.catalina.*;
import org.apache.catalina.WebResourceRoot.ResourceSetType;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.ProtocolHandler;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.scan.StandardJarScanFilter;

import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.servlet.Filter;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.registry.LocateRegistry;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Tomcat 的功能
 */
public class TomcatStarter {
    
    
    private static final LogHelper LOGGER = LogHelper.getLog(TomcatStarter.class);

    public TomcatStarter(TomcatConfig cfg) {
    
    
        this.cfg = cfg;
    }

    TomcatConfig cfg;

    Tomcat tomcat;

    /**
     * 获取监控信息用
     */
    public static Tomcat TOMCAT;

    Context context;

    public static long startedTime;

    public static long springTime;

    public void start() {
    
    
        startedTime = System.currentTimeMillis();
        initTomcat();
        initConnector();
        initContext();
        runTomcat();
    }

    private void initTomcat() {
    
    
        tomcat = new Tomcat();
        tomcat.setPort(cfg.getPort());
        tomcat.setHostname(cfg.getHostName());
        tomcat.enableNaming();

//        String tomcatBaseDir = cfg.getTomcatBaseDir();
//
//        if (tomcatBaseDir == null)
//            tomcatBaseDir = TomcatUtil.createTempDir("tomcat_embed_works_tmpdir").getAbsolutePath();
//
//        tomcat.setBaseDir(tomcatBaseDir);

        TOMCAT = tomcat;
    }

    private void runTomcat() {
    
    
        try {
    
    
            tomcat.start(); // tomcat 启动
        } catch (LifecycleException e) {
    
    
            LOGGER.warning(e);
            throw new RuntimeException(e);
        }

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    
    
            try {
    
    
                LOGGER.info("关闭 Tomcat");
                tomcat.destroy();
            } catch (LifecycleException e) {
    
    
                LOGGER.warning(e);
            }
        }));

//        ac.register(clz);
//        ac.refresh();
//        ac.registerShutdownHook();

        String tpl = "Web 服务启动完毕。Spring 耗时:%sms,总耗时:%sms 127.0.0.1:" + cfg.getPort() + cfg.getContextPath();
        tpl = String.format(tpl, springTime, System.currentTimeMillis() - startedTime);
        LOGGER.info(tpl);

        // 注册关闭端口以进行关闭
        // 可以通过Socket关闭tomcat: telnet 127.0.0.1 8005,输入SHUTDOWN字符串
        tomcat.getServer().setPort(cfg.getShutdownPort());
        tomcat.getServer().await(); // 保持主线程不退出,让其阻塞,不让当前线程结束,等待处理请求
        LOGGER.info("正在关闭 Tomcat,shutdown......");

        try {
    
    
            tomcat.stop();
        } catch (LifecycleException e) {
    
    
            LOGGER.warning(e);
        }

        // 删除 tomcat 临时路径
//        TomcatUtil.deleteAllFilesOfDir(tomcatBaseDirFile);
    }

    /**
     * 读取项目路径
     */
    private void initContext() {
    
    
        String jspFolder = getDevelopJspFolder();

        if (jspFolder == null) {
    
    
            jspFolder = Resources.getJarDir() + "/../webapp"; // 部署阶段。这个并不会实际保存 jsp。因为 jsp 都在 META-INF/resources 里面。但因为下面的 addWebapp() 又需要
            FileHelper.mkDir(jspFolder);
        }

//        System.out.println("jspFolder::::::" + Resources.getJarDir());
//        StandardContext ctx = (StandardContext) tomcat.addWebapp("/", new File("/mycar/mycar-service-4.0/security-oauth2-uam/sync/jsp").getAbsolutePath());
//        context = tomcat.addWebapp(contextPath, jspFolder);
        Host host = tomcat.getHost();
        host.setAutoDeploy(false);
        host.setAppBase("webapp");

        context = tomcat.addWebapp(host, cfg.getContextPath(), jspFolder, (LifecycleListener) new EmbededContextConfig());
        context.setReloadable(false);// 禁止重新载入
        context.addLifecycleListener(new Tomcat.FixContextListener());// required if you don't use web.xml

        // seems not work
        WebResourceRoot resources = new StandardRoot(context);// 创建 WebRoot
        String classDir = new File("target/classes").getAbsolutePath();
        resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", classDir, "/"));// tomcat 内部读取 Class 执行

        if (cfg.getEnableSsi())
            ssi();

        if (!cfg.getEnableJsp())
            disableJsp();

//        context.setJarScanner(new EmbeddedStandardJarScanner());
//        context.setParentClassLoader(TomcatStarter.class.getClassLoader());// needs?
        addWebXmlMountListener();
        setTomcatDisableScan();
//        initFilterByTomcat(UTF8CharsetFilter.class);
    }

    public static String getDevelopJspFolder() {
    
    
        return Resources.getResourcesFromClasspath("META-INF\\resources");// 开放调试阶段,直接读取源码的
    }

    /**
     * 禁止 Tomcat 自动扫描 jar 包,那样会很慢
     */
    private void setTomcatDisableScan() {
    
    
        StandardJarScanFilter filter = (StandardJarScanFilter) context.getJarScanner().getJarScanFilter();
        filter.setDefaultTldScan(false);

        /*
         * 这个对启动 tomcat 时间影响很大 又 很多 Servlet 3.0 新特性,不能禁掉,比如在 jar 里面放
         * jsp(部署时候就会这样,但开放阶段不用)。 故,用 isDebug 判断下
         */
        if (Version.isDebug)
            filter.setDefaultPluggabilityScan(false);
//      String oldTldSkip = filter.getTldSkip();
//      System.out.println("-------" + oldTldSkip);
//      String newTldSkip = oldTldSkip == null || oldTldSkip.trim().isEmpty() ? "pdq.jar" : oldTldSkip + ",pdq.jar";
//      filter.setTldSkip(newTldSkip);
    }

    /**
     * 设置 Connector
     */
    void initConnector() {
    
    
        Connector connector;

        if (cfg.isCustomerConnector()) {
    
    // 创建连接器,并且添加对应的连接器,同时连接器指定端口 设置 IO 协议
            connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
            connector.setPort(cfg.getPort());
            connector.setThrowOnFailure(true);

            tomcat.getService().addConnector(connector);// 只能设置一个 service,直接拿默认的
            tomcat.setConnector(connector); // 设置执行器
        } else
            connector = tomcat.getConnector();

        connector.setURIEncoding("UTF-8"); // 设置 URI 编码支持中文

        ProtocolHandler handler = connector.getProtocolHandler();

        // 设置 Tomcat 配置
        if (handler instanceof AbstractProtocol) {
    
    
            AbstractProtocol<?> protocol = (AbstractProtocol<?>) handler;

            if (cfg.getMinSpareThreads() > 0)
                protocol.setMinSpareThreads(cfg.getMinSpareThreads());

            if (cfg.getMaxThreads() > 0)
                protocol.setMaxThreads(cfg.getMaxThreads());

            if (cfg.getConnectionTimeout() > 0)
                protocol.setConnectionTimeout(cfg.getConnectionTimeout());

            if (cfg.getMaxConnections() > 0)
                protocol.setMaxConnections(cfg.getMaxConnections());

            if (cfg.getAcceptCount() > 0)
                protocol.setAcceptCount(cfg.getAcceptCount());
        }

        // Tomcat 的 startStopThreads 属性用于配置 Tomcat 服务器启动和关闭时的线程池大小。它决定了 Tomcat 在启动和关闭过程中能够同时处理的任务数。
        // 对于 Tomcat 8,没有直接的编程方式来设置 startStopThreads 属性
        Executor executor = handler.getExecutor();

        if (executor instanceof ThreadPoolExecutor) {
    
    // doesn't work
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
            threadPoolExecutor.setCorePoolSize(3);// 设置核心线程数和最大线程数
            threadPoolExecutor.setMaximumPoolSize(3);
        }

        if (cfg.isEnableJMX()) {
    
    
            Connector jmxConnector = new Connector("org.apache.coyote.jmx.JmxProtocol");
            jmxConnector.setPort(8999); // Set the desired JMX port
            tomcat.getService().addConnector(jmxConnector);
        }
    }

    /**
     * context load WEB-INF/web.xml from classpath
     */
    void addWebXmlMountListener() {
    
    
        context.addLifecycleListener(event -> {
    
    
            if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
    
    
                Context context = (Context) event.getLifecycle();
                WebResourceRoot resources = context.getResources();

                if (resources == null) {
    
    
                    resources = new StandardRoot(context);
                    context.setResources(resources);
                }

                /*
                 * When run as embedded tomcat, context.getParentClassLoader() is AppClassLoader,so it can load "WEB-INF/web.xml" from app classpath.
                 */
                URL resource = context.getParentClassLoader().getResource("WEB-INF/web.xml");

                if (resource != null) {
    
    
                    String webXmlUrlString = resource.toString();

                    try {
    
    
                        URL root = new URL(webXmlUrlString.substring(0, webXmlUrlString.length() - "WEB-INF/web.xml".length()));
                        resources.createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/WEB-INF", root, "/WEB-INF");
                    } catch (MalformedURLException e) {
    
    
                        LOGGER.warning(e);
                    }
                }
            }
        });
    }

    /**
     * 禁用 JSP
     */
    void disableJsp() {
    
    
        LifecycleListener tmplf = null;

        for (LifecycleListener lfl : context.findLifecycleListeners()) {
    
    
            if (lfl instanceof Tomcat.DefaultWebXmlListener) {
    
    
                tmplf = lfl;
                break;
            }
        }

        if (tmplf != null)
            context.removeLifecycleListener(tmplf);

        context.addLifecycleListener(event -> {
    
    
            if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) {
    
    
                Context context = (Context) event.getLifecycle();
                Tomcat.initWebappDefaults(context);
                // 去掉JSP
                context.removeServletMapping("*.jsp");
                context.removeServletMapping("*.jspx");
                context.removeChild(context.findChild("jsp"));
            }
        });
    }

    /**
     * 在 Tomcat 初始化阶段设置 Filter
     */
    @SuppressWarnings("unused")
    private void initFilterByTomcat(Class<? extends Filter> filterClz) {
    
    
        FilterDef filter1definition = new FilterDef();
        filter1definition.setFilterName(filterClz.getSimpleName());
        filter1definition.setFilterClass(filterClz.getName());
        context.addFilterDef(filter1definition);

        FilterMap filter1mapping = new FilterMap();
        filter1mapping.setFilterName(filterClz.getSimpleName());
        filter1mapping.addURLPattern("/*");
        context.addFilterMap(filter1mapping);
    }

    /**
     * 将定义好的 Tomcat MBean 注册到 MBeanServer
     * 参见 <a href="https://blog.csdn.net/zhangxin09/article/details/132136748">...</a>
     */
    private static void connectMBeanServer() {
    
    
        try {
    
    
            LocateRegistry.createRegistry(9011); //这个步骤很重要,注册一个端口,绑定url  后用于客户端通过 rmi 方式连接 JMXConnectorServer
            JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(new JMXServiceURL("service:jmx:rmi://localhost/jndi/rmi://localhost:9011/jmxrmi"), null, ManagementFactory.getPlatformMBeanServer() // 获取当前 JVM 的 MBeanServer,ObjectName 是 MBean 的唯一标示,一个 MBeanServer 不能有重复。
                    // 完整的格式「自定义命名空间:type=自定义类型,name=自定义名称」。当然你可以只声明 type ,不声明 name
            );

            cs.start();
            LOGGER.info("成功启动 JMXConnectorServer");
        } catch (IOException e) {
    
    
            LOGGER.warning(e);
        }
    }

    /**
     * SSI(服务器端嵌入)
     */
    void ssi() {
    
    
        context.setPrivileged(true);
        Wrapper servlet = Tomcat.addServlet(context, "ssi", "org.apache.catalina.ssi.SSIServlet");
        servlet.addInitParameter("buffered", "1");
        servlet.addInitParameter("inputEncoding", "UTF-8");
        servlet.addInitParameter("outputEncoding", "UTF-8");
        servlet.addInitParameter("debug", "0");
        servlet.addInitParameter("expires", "666");
        servlet.addInitParameter("isVirtualWebappRelative", "4");
        servlet.setLoadOnStartup(4);
        servlet.setOverridable(true);

        // Servlet mappings
        context.addServletMappingDecoded("*.html", "ssi");
        context.addServletMappingDecoded("*.shtml", "ssi");
    }
}

It’s nothing more than executing it step by step as follows:

Insert image description here

Enhanced features

The following features don’t seem to be of much use, so you can add them as appropriate.

EmbeddedContextConfig

Scan JAR files that contain web-fragment.xml files to see if they also contain static resources and add them to the context. If static resources are found, they are added in order of priority in web-fragment.xml.

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Set;

import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.startup.ContextConfig;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.descriptor.web.WebXml;
import org.apache.tomcat.Jar;
import org.apache.tomcat.util.scan.JarFactory;

/**
 * Support jar in jar. when boot by spring boot loader, jar url will be: fat.jar!/lib/!/test.jar!/ .
 */
public class EmbededContextConfig extends ContextConfig {
    
    
    private static final Log log = LogFactory.getLog(EmbededContextConfig.class);

    /**
     * 扫描包含 web-fragment.xml 文件的 JAR 文件,以查看它们是否还包含静态资源,并将其添加到上下文中。
     * 如果找到静态资源,则按照 web-fragment.xml 的优先级顺序添加。
     * Scan JARs that contain web-fragment.xml files that will be used to
     * configure this application to see if they also contain static resources. If static resources are found,
     * add them to the context. Resources are added in web-fragment.xml priority order.
     */
    @Override
    protected void processResourceJARs(Set<WebXml> fragments) {
    
    
        for (WebXml fragment : fragments) {
    
    
            URL url = fragment.getURL();
            String urlString = url.toString();

            // It's a nested jar, but we now don't want the suffix
            // because Tomcat is going to try and locate it as a root URL (not the resource inside it)
            if (isInsideNestedJar(urlString))
                urlString = urlString.substring(0, urlString.length() - 2);

            try {
    
    
                url = new URL(urlString);

                if ("jar".equals(url.getProtocol())) {
    
    
                    try (Jar jar = JarFactory.newInstance(url)) {
    
    
                        jar.nextEntry();
                        String entryName = jar.getEntryName();

                        while (entryName != null) {
    
    
                            if (entryName.startsWith("META-INF/resources/")) {
    
    
                                context.getResources().createWebResourceSet(
                                        WebResourceRoot.ResourceSetType.RESOURCE_JAR,
                                        "/", url, "/META-INF/resources");
                                break;
                            }

                            jar.nextEntry();
                            entryName = jar.getEntryName();
                        }
                    }
                } else if ("file".equals(url.getProtocol())) {
    
    
                    File file = new File(url.toURI());
                    File resources = new File(file, "META-INF/resources/");

                    if (resources.isDirectory())
                        context.getResources().createWebResourceSet(
                                WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/",
                                resources.getAbsolutePath(), null, "/");
                }
            } catch (IOException | URISyntaxException ioe) {
    
    
                log.error(sm.getString("contextConfig.resourceJarFail", url, context.getName()));
            }
        }
    }

    private static boolean isInsideNestedJar(String dir) {
    
    
        return dir.indexOf("!/") < dir.lastIndexOf("!/");
    }
}

Usage

 context = tomcat.addWebapp(host, cfg.getContextPath(), jspFolder, (LifecycleListener) new EmbededContextConfig());

EmbeddedStandardJarScanner

To be honest, I don't really know what it is used for. Remember first,

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.servlet.ServletContext;

import lombok.Data;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.JarScanFilter;
import org.apache.tomcat.JarScanType;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.JarScannerCallback;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.descriptor.web.FragmentJarScannerCallback;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.scan.Constants;
import org.apache.tomcat.util.scan.JarFileUrlJar;
import org.apache.tomcat.util.scan.StandardJarScanFilter;
import org.apache.tomcat.util.scan.UrlJar;

/**
 * When boot by SpringBoot loader, WebappClassLoader.getParent() is LaunchedURLClassLoader,
 * Just need to scan WebappClassLoader and LaunchedURLClassLoader.
 * When boot in IDE, WebappClassLoader.getParent() is AppClassLoader,
 * Just need to scan WebappClassLoader and AppClassLoader.
 */
@Data
public class EmbeddedStandardJarScanner implements JarScanner {
    
    
    private static final Log log = LogFactory.getLog(EmbeddedStandardJarScanner.class);

    /**
     * The string resources for this package.
     */
    private static final StringManager sm = StringManager.getManager(Constants.Package);

    /**
     * Controls the classpath scanning extension.
     */
    private boolean scanClassPath = true;

    /**
     * Controls the testing all files to see of they are JAR files extension.
     */
    private boolean scanAllFiles = false;

    /**
     * Controls the testing all directories to see of they are exploded JAR
     * files extension.
     */
    private boolean scanAllDirectories = false;

    /**
     * Controls the testing of the bootstrap classpath which consists of the
     * runtime classes provided by the JVM and any installed system extensions.
     */
    private boolean scanBootstrapClassPath = false;

    /**
     * Controls the filtering of the results from the scan for JARs
     */
    private JarScanFilter jarScanFilter = new StandardJarScanFilter();

    @Override
    public JarScanFilter getJarScanFilter() {
    
    
        return jarScanFilter;
    }

    @Override
    public void setJarScanFilter(JarScanFilter jarScanFilter) {
    
    
        this.jarScanFilter = jarScanFilter;
    }

    /**
     * Scan the provided ServletContext and class loader for JAR files. Each JAR
     * file found will be passed to the callback handler to be processed.
     *
     * @param scanType The type of JAR scan to perform. This is passed to the filter which uses it to determine how to filter the results
     * @param context  The ServletContext - used to locate and access WEB-INF/lib
     * @param callback The handler to process any JARs found
     */
    @Override
    public void scan(JarScanType scanType, ServletContext context, JarScannerCallback callback) {
    
    
        if (log.isTraceEnabled())
            log.trace(sm.getString("jarScan.webinflibStart"));

        Set<URL> processedURLs = new HashSet<>();

        // Scan WEB-INF/lib
        Set<String> dirList = context.getResourcePaths(Constants.WEB_INF_LIB);

        if (dirList != null) {
    
    
            Iterator<String> it = dirList.iterator();

            while (it.hasNext()) {
    
    
                String path = it.next();

                if (path.endsWith(Constants.JAR_EXT) && getJarScanFilter().check(scanType, path.substring(path.lastIndexOf('/') + 1))) {
    
    
                    // Need to scan this JAR
                    if (log.isDebugEnabled())
                        log.debug(sm.getString("jarScan.webinflibJarScan", path));

                    URL url = null;

                    try {
    
    
                        url = context.getResource(path);
                        processedURLs.add(url);
                        process(scanType, callback, url, path, true);
                    } catch (IOException e) {
    
    
                        log.warn(sm.getString("jarScan.webinflibFail", url), e);
                    }
                } else if (log.isTraceEnabled())
                    log.trace(sm.getString("jarScan.webinflibJarNoScan", path));
            }
        }

        // Scan WEB-INF/classes
        if (isScanAllDirectories()) {
    
    
            try {
    
    
                URL url = context.getResource("/WEB-INF/classes/META-INF");

                if (url != null) {
    
    
                    // Class path scanning will look at WEB-INF/classes since that is the URL that Tomcat's web application class
                    // loader returns. Therefore, it is this URL that needs to be added to the set of processed URLs.
                    URL webInfURL = context.getResource("/WEB-INF/classes");
                    if (webInfURL != null)
                        processedURLs.add(webInfURL);

                    try {
    
    
                        callback.scanWebInfClasses();
                    } catch (IOException e) {
    
    
                        log.warn(sm.getString("jarScan.webinfclassesFail"), e);
                    }
                }
            } catch (MalformedURLException e) {
    
    
                // Ignore
            }
        }

        // Scan the classpath
        if (isScanClassPath()) {
    
    
            if (log.isTraceEnabled())
                log.trace(sm.getString("jarScan.classloaderStart"));

            ClassLoader classLoader = context.getClassLoader();
            ClassLoader stopLoader = null;

            if (classLoader.getParent() != null) {
    
    
                // there are two cases:
                // 1. boot by SpringBoot loader
                // 2. boot in IDE
                // in two case, just need to scan WebappClassLoader and
                // WebappClassLoader.getParent()
                stopLoader = classLoader.getParent().getParent();
            }

            // JARs are treated as application provided until the common class
            // loader is reached.
            boolean isWebapp = true;

            while (classLoader != null && classLoader != stopLoader) {
    
    
                if (classLoader instanceof URLClassLoader) {
    
    
                    URL[] urls = ((URLClassLoader) classLoader).getURLs();

                    for (URL url : urls) {
    
    
                        if (processedURLs.contains(url))
                            continue;// Skip this URL it has already been processed

                        ClassPathEntry cpe = new ClassPathEntry(url);

                        // JARs are scanned unless the filter says not to.
                        // Directories are scanned for pluggability scans or if scanAllDirectories is enabled unless the filter says not to.
                        if ((cpe.isJar() || scanType == JarScanType.PLUGGABILITY || isScanAllDirectories()) && getJarScanFilter().check(scanType, cpe.getName())) {
    
    
                            if (log.isDebugEnabled())
                                log.debug(sm.getString("jarScan.classloaderJarScan", url));

                            try {
    
    
                                process(scanType, callback, url, null, isWebapp);
                            } catch (IOException ioe) {
    
    
                                log.warn(sm.getString("jarScan.classloaderFail", url), ioe);
                            }
                        } else {
    
    
                            // JAR / directory has been skipped
                            if (log.isTraceEnabled())
                                log.trace(sm.getString("jarScan.classloaderJarNoScan", url));
                        }
                    }
                }

                classLoader = classLoader.getParent();
            }
        }
    }

    private boolean nestedJar(String url) {
    
    
        int idx = url.indexOf(".jar!");
        int idx2 = url.lastIndexOf(".jar!");

        return idx != idx2;
    }

    /*
     * Scan a URL for JARs with the optional extensions to look at all files and all directories.
     */
    private void process(JarScanType scanType, JarScannerCallback callback, URL url, String webappPath, boolean isWebapp) throws IOException {
    
    
        if (log.isTraceEnabled())
            log.trace(sm.getString("jarScan.jarUrlStart", url));

        URLConnection conn = url.openConnection();
        String urlStr = url.toString();

        if (conn instanceof JarURLConnection) {
    
    
            System.out.println("-----scan UrlJar: " + urlStr);

            if (nestedJar(urlStr) && !(callback instanceof FragmentJarScannerCallback)) {
    
    
                //JarFileUrlNestedJar.scanTest(new UrlJar(conn.getURL()), webappPath, isWebapp);
                //callback.scan(new JarFileUrlNestedJar(conn.getURL()), webappPath, isWebapp);
            } else
                callback.scan(new UrlJar(conn.getURL()), webappPath, isWebapp);

//			callback.scan((JarURLConnection) conn, webappPath, isWebapp);
        } else {
    
    
            System.out.println("-----scan: " + urlStr);

            if (urlStr.startsWith("file:") || urlStr.startsWith("http:") || urlStr.startsWith("https:")) {
    
    
                if (urlStr.endsWith(Constants.JAR_EXT)) {
    
    
//					URL jarURL = new URL("jar:" + urlStr + "!/");
//					callback.scan((JarURLConnection) jarURL.openConnection(), webappPath, isWebapp);
//					System.out.println("-----" + jarURL);
//					callback.scan(new UrlJar(jarURL), webappPath, isWebapp);
                    callback.scan(new JarFileUrlJar(url, false), webappPath, isWebapp);
                } else {
    
    
                    File f;

                    try {
    
    
                        f = new File(url.toURI());

                        if (f.isFile() && isScanAllFiles()) {
    
    
                            // 把这个文件当作 JAR 包 Treat this file as a JAR
                            URL jarURL = new URL("jar:" + urlStr + "!/");
//							callback.scan((JarURLConnection) jarURL.openConnection(), webappPath, isWebapp);
                            callback.scan(new UrlJar(jarURL), webappPath, isWebapp);
                        } else if (f.isDirectory()) {
    
    
                            if (scanType == JarScanType.PLUGGABILITY)
                                callback.scan(f, webappPath, isWebapp);
                            else {
    
    
                                File metaInf = new File(f.getAbsoluteFile() + File.separator + "META-INF");

                                if (metaInf.isDirectory())
                                    callback.scan(f, webappPath, isWebapp);
                            }
                        }
                    } catch (Throwable t) {
    
    
                        ExceptionUtils.handleThrowable(t);
                        // Wrap the exception and re-throw
                        IOException ioe = new IOException();
                        ioe.initCause(t);
                        throw ioe;
                    }
                }
            }
        }

    }
}

ClassPathEntry

import java.net.URL;

import org.apache.tomcat.util.scan.Constants;

public class ClassPathEntry {
    
    
    private final boolean jar;
    private final String name;

    public ClassPathEntry(URL url) {
    
    
        String path = url.getPath();
        int end = path.indexOf(Constants.JAR_EXT);

        if (end != -1) {
    
    
            jar = true;
            int start = path.lastIndexOf('/', end);
            name = path.substring(start + 1, end + 4);
        } else {
    
    
            jar = false;
            if (path.endsWith("/"))
                path = path.substring(0, path.length() - 1);

            int start = path.lastIndexOf('/');
            name = path.substring(start + 1);
        }
    }

    public boolean isJar() {
    
    
        return jar;
    }

    public String getName() {
    
    
        return name;
    }
}

JarFileUrlNestedJar

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.tomcat.Jar;
import org.apache.tomcat.util.scan.AbstractInputStreamJar;
import org.apache.tomcat.util.scan.JarFactory;
import org.apache.tomcat.util.scan.NonClosingJarInputStream;

/**
 * 这是一个实现了 {@link org.apache.tomcat.Jar} 接口的类,针对基于文件的 JAR URL 进行了优化,
 * 这些 URL 引用了 WAR 内部嵌套的 JAR 文件(例如形如 jar:file: ... .war!/ ... .jar 的 URL)
 * Implementation of {@link org.apache.tomcat.Jar} that is optimised for file
 * based JAR URLs that refer to a JAR file nested inside a WAR (e.g. URLs of the form jar:file: ... .war!/ ... .jar).
 */
public class JarFileUrlNestedJar extends AbstractInputStreamJar {
    
    
    private final JarFile warFile;


    private final JarEntry jarEntry;

    public JarFileUrlNestedJar(URL url) throws IOException {
    
    
        super(url);

        JarURLConnection jarConn = (JarURLConnection) url.openConnection();
        jarConn.setUseCaches(false);
        warFile = jarConn.getJarFile();

        String urlAsString = url.toString();
        int pathStart = urlAsString.indexOf("!/") + 2;
        String jarPath = urlAsString.substring(pathStart);
        System.out.println("==== " + jarPath);
        jarEntry = warFile.getJarEntry(jarPath);
        Enumeration<JarEntry> ens = warFile.entries();

        while (ens.hasMoreElements()) {
    
    
            JarEntry e = ens.nextElement();
            System.out.println(e.getName());
        }
    }

    @Override
    public void close() {
    
    
        closeStream();

        if (warFile != null) {
    
    
            try {
    
    
                warFile.close();
            } catch (IOException ignored) {
    
    
            }
        }
    }

    @Override
    protected NonClosingJarInputStream createJarInputStream() throws IOException {
    
    
        return new NonClosingJarInputStream(warFile.getInputStream(jarEntry));
    }

    private static final String TLD_EXT = ".tld";

    public static void scanTest(Jar jar, String webappPath, boolean isWebapp) throws IOException {
    
    
        URL jarFileUrl = jar.getJarFileURL();
        System.out.println("xxxx------" + jarFileUrl.toString());
        jar.nextEntry();

        for (String entryName = jar.getEntryName(); entryName != null; jar.nextEntry(), entryName = jar.getEntryName()) {
    
    
            if (!(entryName.startsWith("META-INF/") && entryName.endsWith(TLD_EXT)))
                continue;

            URL entryUrl = JarFactory.getJarEntryURL(jarFileUrl, entryName);
            System.out.println(entryName + ": " + entryUrl);
            entryUrl.openStream();
        }
    }
}

Usage

context.setJarScanner(new EmbeddedStandardJarScanner());

Close Tomcat

Tomcat can be closed through Socket: telnet 127.0.0.1 8005, enter the SHUTDOWN string.

import java.io.*;
import java.net.Socket;

/**
 * 可以通过 Socket 关闭 tomcat: telnet 127.0.0.1 8005,输入 SHUTDOWN 字符串
 */
public class TomcatUtil {
    
    
    public static void shutdown() {
    
    
        shutdown("localhost", 8005);
    }

    public static void shutdown(String serverHost, Integer serverPort) {
    
    
        send("SHUTDOWN", serverHost, serverPort);
    }

    /**
     * 小型 Socket 客户端
     */
    public static String send(String msg, String host, int port) {
    
    
        try (Socket socket = new Socket(host, port);
             BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
    
    
            out.write(msg.getBytes());
            out.flush();

            socket.shutdownOutput();
            String ackMsg = socketRead(socket);
            socket.shutdownInput();
            System.out.println("[" + System.currentTimeMillis() + "] Reply from server " + host + ":" + port + ": ");
            System.out.println("\t" + ackMsg);

            return ackMsg;
        } catch (IOException e) {
    
    
            e.printStackTrace();
            return null;
        }
    }

    static String socketRead(Socket socket) throws IOException {
    
    
        socket.setSoTimeout(5000);

        int byteCount = 0;
        char[] buffer = new char[4096];
        int bytesRead;

        try (InputStreamReader in = new InputStreamReader(socket.getInputStream()); StringWriter out = new StringWriter()) {
    
    
            while ((bytesRead = in.read(buffer)) != -1) {
    
    
                out.write(buffer, 0, bytesRead);
                byteCount += bytesRead;
            }
//            out.flush();
            return out.toString();
        }
    }

    public static File createTempDir(String folderName) {
    
    
        File tmpdir = new File(System.getProperty("java.io.tmpdir"));
        tmpdir = new File(tmpdir, folderName);

        if (!tmpdir.exists())
            tmpdir.mkdir();

        return tmpdir;
    }


    public static File createTempDir(String prefix, int port) {
    
    
        File tempDir;

        try {
    
    
            tempDir = File.createTempFile(prefix + ".", "." + port);
        } catch (IOException e) {
    
    
            throw new RuntimeException(e);
        }

        tempDir.delete();
        tempDir.mkdir();
        tempDir.deleteOnExit();

        return tempDir;
    }

    public static void deleteAllFilesOfDir(File path) {
    
    
        if (!path.exists())
            return;

        try {
    
    
            if (path.isFile()) {
    
    
                java.nio.file.Files.delete(path.toPath());
                return;
            }

            File[] files = path.listFiles();
            assert files != null;

            for (File file : files) deleteAllFilesOfDir(file);
            java.nio.file.Files.delete(path.toPath());
        } catch (IOException e) {
    
    
            throw new RuntimeException(e);
        }
    }
}

Integrate SpringMVC

This article only discusses the startup of pure Tomcat. Regarding the integration of Spring, I introduced "Lightweight Imitation SpringBoot = Embedded Tomcat + SpringMVC" in another article .

reference

Guess you like

Origin blog.csdn.net/zhangxin09/article/details/134044518