嵌入式 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 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");
}
}
无非就是按部就班地执行如下
增强特性
下面特性好像用处不大,大家视情况加入。
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.
*/
@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("!/");
}
}
使用方式
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();
@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();
}
}
}
使用方式
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的启动方式