嵌入式 Tomcat 调校
SpringBoot 嵌入了 Web 容器如 Tomcat/Jetty/Undertow,——这是怎么做到的?我们以 Tomcat 为例子,尝试调用嵌入式 Tomcat。
调用嵌入式 Tomcat,如果按照默认去启动,一个 main 函数就可以了。
简单的例子
下面是启动 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(); // 保持主线程不退出,让其阻塞,不让当前线程结束,等待处理请求
配置化你的 Tomcat
当然,我们不会满足于默认的 Tomcat 配置。Tomcat 本身提供开放的配置选项,一般是 server.xml 或 web.xml 的形式,而换到嵌入式 Tomcat 的话,那些 xml 配置则不可用了,于是我们得采取手动编码(Programmatically)在 Java 完成配置。
面对众多的 Tomcat 配置,我们选出下面若干最常见的。
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 : "";}
}
hostName 主机名称、port 端口这些大家应该都知道,就不多说了。其他有关配置说明如下:
- Web 上下文目录 contextPath。就是第一级的目录,你可以不设,但不要设为
/
,否则会有警告;设为空字符串""
就好。一般都加上。 - Web 目录的磁盘路径 docBase,就是 WebRoot 对应的磁盘目录,如
D:/1sync/static
,浏览器可以访问这里的静态文件和 JSP 文件等。 - Tomcat 临时文件的目录,tomcatBaseDir。可不设,默认
system properties - catalina.base, catalina.home
或$PWD/tomcat.$PORT
。如果不需要运行 JSP,或者可以禁止该目录 - enableSsi 是否激活 SSI(服务器端嵌入)
- 关闭的端口 shutdownPort。可以通过 Socket 关闭 tomcat:
telnet 127.0.0.1 8005
,输入SHUTDOWN
字符串(后面有介绍方法) - 是否激活 JSP enableJsp
- 是否激活 JMX 监控 enableJMX。用于 JMX 监控,关闭会提高启动速度
- 其他并发的性能调优 maxThreads、minSpareThreads、connectionTimeout、maxConnections、acceptCount
启动 Tomcat
有了配置,自然可以启动 Tomcat,我们把TomcatConfig
作为构造器参数传给TomcatStarter
解析各个参数去配置 Tomcat 最终启动。
如下是按照默认参数启动。
TomcatConfig cfg = new TomcatConfig();
TomcatStarter t = new TomcatStarter(cfg);
t.start();
另外补充一下两个配置的地方:
- 禁止 Tomcat 自动扫描 jar 包,会提高启动速度
- Tomcat 的 startStopThreads 属性用于配置 Tomcat 服务器启动和关闭时的线程池大小。它决定了 Tomcat 在启动和关闭过程中能够同时处理的任务数。但对于 Tomcat 8,没有直接的编程方式来设置 startStopThreads 属性
- 下面设置:设置核心线程数和最大线程数,又不会走到这里,这是悬而未决的问题
完整TomcatStarter源码如下。
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 workWebResourceRoot resources = new StandardRoot(context);// 创建 WebRootString 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); // 设置执行器} elseconnector = 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 workThreadPoolExecutor 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 porttomcat.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);// 去掉JSPcontext.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 方式连接 JMXConnectorServerJMXConnectorServer 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 mappingscontext.addServletMappingDecoded("*.html", "ssi");context.addServletMappingDecoded("*.shtml", "ssi");}
}
无非就是按部就班地执行如下
增强特性
下面特性好像用处不大,大家视情况加入。
EmbededContextConfig
扫描包含 web-fragment.xml 文件的 JAR 文件,以查看它们是否还包含静态资源,并将其添加到上下文中。 如果找到静态资源,则按照 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.*/@Overrideprotected 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("!/");}
}
使用方式
context = tomcat.addWebapp(host, cfg.getContextPath(), jspFolder, (LifecycleListener) new EmbededContextConfig());
EmbeddedStandardJarScanner
老实说,我也不太懂用来干嘛的。先记着,,
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();@Overridepublic JarScanFilter getJarScanFilter() {return jarScanFilter;}@Overridepublic 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*/@Overridepublic 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/libSet<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 JARif (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/classesif (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 classpathif (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 processedClassPathEntry 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 skippedif (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);} elsecallback.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 JARURL 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-throwIOException 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());}}@Overridepublic void close() {closeStream();if (warFile != null) {try {warFile.close();} catch (IOException ignored) {}}}@Overrideprotected 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();}}
}
使用方式
context.setJarScanner(new EmbeddedStandardJarScanner());
关闭 Tomcat
可以通过 Socket 关闭 tomcat: telnet 127.0.0.1 8005,输入 SHUTDOWN 字符串。
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);}}
}
整合 SpringMVC
本文只是讨论纯 Tomcat 的启动,关于整合 Spring 我在另外一篇文章中介绍《轻量级仿 SpringBoot=嵌入式 Tomcat+SpringMVC》。
参考
- 仿SpringBoot的启动方式
相关文章:

嵌入式 Tomcat 调校
SpringBoot 嵌入了 Web 容器如 Tomcat/Jetty/Undertow,——这是怎么做到的?我们以 Tomcat 为例子,尝试调用嵌入式 Tomcat。 调用嵌入式 Tomcat,如果按照默认去启动,一个 main 函数就可以了。 简单的例子 下面是启动…...

初始化固定长度的数组
完全解析Array.apply(null,「length: 1000」) 创建固定长度数组,并且初始化值。直接可以使用map、forEach、reduce等有遍历性质的方法。 如果直接使用Array(81),map里面的循环不会执行。 //方法一 Array.apply(null, { length: 20 })//方法二 Array(81)…...

实现基于 Jenkins 的多服务器打包方案
实现基于 Jenkins 的多服务器打包方案 在实际项目中,我们经常会遇到需要将一个应用程序或服务部署到不同的服务器上的需求。而使用 Jenkins 可以很方便地自动化这个过程。 设置参数 首先,我们需要设置一些参数,以便在构建过程中指定要部署…...

探索现代IT岗位:职业机遇的海洋
目录 1 引言2 传统软件开发3 数据分析与人工智能4 网络与系统管理5 信息安全6 新兴技术领域 1 引言 随着现代科技的迅猛发展,信息技术(IT)行业已经成为了全球经济的关键引擎,改变了我们的生活方式、商业模式和社会互动方式。IT行…...

np.linspace精确度
前言 今天发现一个大坑,如果是序列是小数的话,不要用np.linspace,而要用np.arrange指定等差序列。比如入下图中a和b是一样的意思,但是b是有较大误差的。 anp.arange(0,4,0.4) bnp.linspace(0,4,10) print("a",a) prin…...

GD32_定时器输入捕获波形频率
GD32_定时器输入捕获波形频率(多通道轮询) 之前项目上用到一个使用定时器捕获输入采集风扇波形频率得到风扇转速的模块,作为笔记简单记录以下当时的逻辑结构和遇到的问题,有需要参考源码、有疑问或需要提供帮助的可以留言告知 。…...

单窗口单IP适合炉石传说游戏么?
游戏道具制作在炉石传说中是一个很有挑战的任务,但与此同时,它也是一个充满机遇的领域。在这篇文章中,我们将向您展示如何在炉石传说游戏中使用动态包机、多窗口IP工具和动态IP进行游戏道具制作。 作者与主题的关系:作为一名热爱炉…...

win11安装docekr、docker-compose
1.docker安装 下载地址:Install Docker Desktop on Windows | Docker Docs 出问题别慌,看清楚提示信息,cmd更新wsl,什么是wsl,百度好好理解一下哦 2.docker-compose安装 还是去官方看看怎么说的,然后跟着处…...

Postman的简单使用
Postman简介 官网 Postman是Google公司开发的一款功能强大的网页调试与发送HTTP请求,并能运行测试用例的Chrome插件 使用Postman进行简单接口测试 新建测试 → 选择请求方式 → 请求URL,下面用百度作为例子: 参考文档 [1] Postman使用教程…...

信号继电器驱动芯片(led驱动芯片)
驱动继电器需要配合BAV99(防止反向脉冲)使用 具体应用参考开源项目 电阻箱 sbstnh/programmable_precision_resistor: A SCPI programmable precision resistor (github.com) 这个是芯片的输出电流设置 对应到上面的实际开源项目其设置电阻为1.5K&…...

IDEA配置HTML和Thymeleaf热部署开发
IDEA配置HTML和Thymeleaf热部署开发 1.项目配置2. IDEA配置3. 使用 需求:现在我们在开发不分离项目的时候(SpringBootThmeleaf)经常会改动了类或者静态html文件就需要重启一下服务器, 这样不仅时间开销很大,而且经常重…...

Nginx动静分离
为了加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度。降低原来单个服务器的压力。 在动静分离的tomcat的时候比较明显,因为tomcat解析静态很慢,其实这些原理的话都很好理解,简单来说&…...

Spring中AOP详解
目录 一、AOP的概念 二、AOP的底层实现原理 2.1 JDK的动态代理 2.1.1 invocationhandler接口 2.1.2 代理对象和原始类实现相同的接口 interfaces 2.1.3 类加载器ClassLoador 2.1.4 编码实现 2.2 Cglib动态代理 2.2.1 Cglib动态代理编码实现 三、AOP如何通过原始对象的id获取到代…...

Unity DOTS系列之Filter Baking Output与Prefab In Baking核心分析
最近DOTS发布了正式的版本, 我们来分享一下DOTS里面Baking核心机制,方便大家上手学习掌握Unity DOTS开发。今天给大家分享的Baking机制中的Filter Baking Output与Prefab In Baking。 对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础…...

Matlab读写操作
随机生成一个3*3矩阵,对矩阵进行按列升序排列 >> Arand(3,3); >> [B, ~] sort(A, 2); >> B B 0.4898 0.6797 0.70940.4456 0.6551 0.75470.1626 0.2760 0.6463在不同数值类型下显示π的值 1、默认数值类型 >> p_defa…...

Android 开发技巧:音乐播放器的后台处理【Service、Handler、MediaPlayer】
给定部分完成的MusicPlayer项目,实现其中未完成的service部分: 1、创建MusicService类,通过service组件实现后台播放音乐的功能; 2、在MainActivity中通过ServiceConnection连接MusicService,实现对音乐播放的控制&…...

使用Windows平台的Hyper-V虚拟机安装CentOS7的详细过程
Hyper-V虚拟机安装CentOS7 前言常见Linux系统CentOSUbuntuDebianKaliFedoraArch LinuxMintManjaroopenSUSE Hyper-V开启Hyper-V打开Hyper-V Hyper-V的使用新建虚拟机开始安装分区配置开始安装 修改yum源为阿里源 前言 作为一名开发者,就服务器而言,接触最…...

某马机房预约系统 C++项目(二) 完结
8.4、查看机房 8.4.1、添加机房信息 根据案例,我们还是先在computerRoom.txt中直接添加点数据 //几机房 机器数量 1 20 2 50 3 1008.4.2、机房类创建 同样我们在头文件下新建一个computerRoom.h文件 添加如下代码: #pragma once #include<i…...

npm 安装到指定文件夹
创建一个文件夹,用vscode或者cmd打开, 执行 npm install --prefix ./ 路径 包名, npm install --prefix ./ 包名 , 就会将包安装在当前文件夹, 例如: npm install --prefix ./ -g oppo-minigame…...

自建的离散傅里叶变换matlab程序实现及其与matlab自带函数比较举例
自建的离散傅里叶变换matlab程序实现及其与matlab自带函数比较举例 在matlab中有自带的离散傅里叶变换程序,即fft程序,但该程序是封装的,无法看到源码。为了比较清楚的了解matlab自带的实现过程,本文通过自建程序实现matlab程序&…...

Vue图片路径问题(动态引入)
vue项目中我们经常会遇到动态路径的图片无法显示的问题,以下是静态路径和动态路径的常见使用方法。 1.静态路径 在日常的开发中,图片的静态路径通过相对路径和绝对路径的方式引入。 相对路径:以.开头的,例如./、../之类的。就是…...

项目部署Linux步骤
1、最小化安装centos7-环境准备 安装epel-release 安装epel-release,因为有些rpm包在官方库中找不到。前提是保证可以联网 yum install -y epel-release 修改IP net-tools net-tool:工具包集合,包含ifconfig等命令 yum install -y net-…...

UG\NX二次开发 在资源栏(左侧面板)中添加按钮
文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 感谢粉丝订阅 感谢 apolloryd 订阅本专栏,非常感谢。 简介 UG\NX二次开发 在资源栏(左侧面板)中添加按钮,下面提供了帮助说明,在 UGOPEN 文件夹下有示例。 C++语言在UG二次…...

Proteus仿真--量程自动切换数字电压表(仿真+程序)
本文主要介绍基于51单片机的量程自动切换数字电压表Proteus仿真设计(完整仿真源文件及代码见文末链接) 简介 硬件电路主要分为单片机主控模块、AD转换模块、量程选择模块以及数码管显示模块 (1)单片机主控模块:单片…...

如何使用ArcGIS Pro制作一张地形图
01数据来源 本教程所使用的数据是从水经微图中下载的DEM数据,除了DEM数据,常见的GIS数据都可以从水经微图中下载,你可以通过关注“水经注GIS”,然后在后台回复“微图”即可获取软件下载地址,当然也可以直接在水经注…...

人工智能三要数之算法Transformer
1. 人工智能三要数之算法Transformer 人工智能的三个要素是算法、数据和计算资源。Transformer 模型作为一种机器学习算法,可以应用于人工智能系统中的数据处理和建模任务。 算法: Transformer 是一种基于自注意力机制的神经网络模型,用于处理序列数据的…...

Java ThreadPoolExecutor 线程池
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.ArrayBlockingQueue;public class ThreadPoolExample {public static void main(String[] args) {// 创建线程池对象ThreadPoolExecutor threadPool new…...

网络协议--IP选路
9.1 引言 选路是IP最重要的功能之一。图9-1是IP层处理过程的简单流程。需要进行选路的数据报可以由本地主机产生,也可以由其他主机产生。在后一种情况下,主机必须配置成一个路由器,否则通过网络接口接收到的数据报,如果目的地址不…...

使用udevil自动挂载U盘或者USB移动硬盘
最近在折腾用树莓派(实际上是平替香橙派orangepi zero3)搭建共享文件服务器,有一个问题很重要,如何在系统启动时自动挂载USB移动硬盘。 1 使用/etc/fstab 最开始尝试了用/etc/fstab文件下增加:"/dev/sda1 /home/orangepi/s…...

学习笔记二十二:K8s控制器Replicaset
K8s控制器Replicaset Replicaset控制器:概念、原理解读Replicaset概述Replicaset工作原理:如何管理PodReplicaset控制器三个组成部分 Replicaset资源清单文件编写技巧Replicaset使用案例:部署Guestbook留言板编写一个ReplicaSet资源清单资源清…...