在上节中,大致过了一遍Engine到Host的启动过程,重点我认为有以下几点
1、Engine容器启动了一个线程间隔10秒执行各子容器的backgroundProcess()方法,并发布periodic周期事件。默认配置下只有Engine启动后台任务线程,子容器Host/Context不启动这个线程,可以修改server.xml配置文件为容器增加backgroundProcessorDelay=20 的参数配置,则该容器会创建一个新的周期任务线程,之后该容器及子容器的后台backgroundProcess()任务则由该线程执行,20是周期(秒)
2、Host启动,将server.xml中配置的Context信息,还有webapp/文件夹下的war包,应用文件夹都解析为一个StandardContext,进行部署和Context的启动。同时也监听context的配置文件,监听到文件被修改,则在backgroundProcess()周期任务中重新部署
一个Context代表一个应用,下边逐行代码分析启动一个web应用时tomcat都做了什么工作
目录
ContextConfig
StandardContext是Context容器的默认实现类,启动时会先发布init_before /init_after/start_before/start_after事件,而ContextConfig就是StandardContext的生命周期监听器,先来看监听器在StandardContext.startInternal()之前进行的处理。
@Override
public void lifecycleEvent(LifecycleEvent event) {
// Identify the context we are associated with
try {
context = (Context) event.getLifecycle();
} catch (ClassCastException e) {
log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();// 3333 startinternal 执行期间发布config_start事件触发
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();// 2222 初始化后,startinternal 之前触发
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}// 4444
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();// 1111 初始化结束事件
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}
}
init
从各个地方加载Context的配置,设置Context对象的属性
1、\tomcat8\conf\context.xml 默认情况下如下配置,设置了该Context对象监听的资源路径,如果下边两个路径的xml被修改,则reload该应用Context对象
2、\tomcat8\conf\Catalina\localhost\context.xml.default 这默认没有,如果需要,则配置的context.xml是当前Host容器(localhost)下应用的通用属性
3、应用路径\META-INF\context.xml 各应用如有需要自行配置
protected synchronized void init() {
// xml文件解析器
Digester contextDigester = createContextDigester();
contextDigester.getParser();
context.setConfigured(false);
ok = true;
contextConfig(contextDigester);
}
protected void contextConfig(Digester digester) {
String defaultContextXml = null;
// ========================1========================
// Open the default context.xml file, if it exists
if (context instanceof StandardContext) {
defaultContextXml = ((StandardContext)context).getDefaultContextXml();
}
// set the default if we don't have any overrides
if (defaultContextXml == null) {
defaultContextXml = Constants.DefaultContextXml;
}
if (!context.getOverride()) {
File defaultContextFile = new File(defaultContextXml);
if (!defaultContextFile.isAbsolute()) {
defaultContextFile =
new File(context.getCatalinaBase(), defaultContextXml);
}
if (defaultContextFile.exists()) {
try {
URL defaultContextUrl = defaultContextFile.toURI().toURL();
processContextConfig(digester, defaultContextUrl);
} catch (MalformedURLException e) {
log.error(sm.getString(
"contextConfig.badUrl", defaultContextFile), e);
}
}
// ===========================2========================
File hostContextFile = new File(getHostConfigBase(), Constants.HostContextXml);
if (hostContextFile.exists()) {
try {
URL hostContextUrl = hostContextFile.toURI().toURL();
processContextConfig(digester, hostContextUrl);
} catch (MalformedURLException e) {
log.error(sm.getString(
"contextConfig.badUrl", hostContextFile), e);
}
}
}
// =============================3=======================
if (context.getConfigFile() != null) {
processContextConfig(digester, context.getConfigFile());
}
}
beforeStart
执行StandardContext.startInternal之前进行处理
1、调整docBase,根据配置重新设置docBse属性,在webapp下的应用设置为相对路径,不在的设置为绝对路径
2、Context参数antiResourceLocking,默认false,如果配置为true,则会建立临时文件夹(路径由系统参数java.io.tmpdir设置),复制该应用的所有文件到临时文件夹,启动时加载临时文件夹内容。
因为tomcat支持热部署,例如war包重新部署时需删除旧的文件夹,但可能某旧文件被锁定无法删除,所以此参数通过建立临时文件防止资源锁导致部署失败
protected synchronized void beforeStart() {
// 重新设置docBase
try {
fixDocBase();
} catch (IOException e) {
log.error(sm.getString(
"contextConfig.fixDocBase", context.getName()), e);
}
// 防止资源锁
antiLocking();
}
protected void antiLocking() {
if ((context instanceof StandardContext)
&& ((StandardContext) context).getAntiResourceLocking()) {
// 如果设置参数为true ,获取临时文件目录
if (originalDocBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
antiLockingDocBase = new File(
System.getProperty("java.io.tmpdir"),
deploymentCount++ + "-" + docBase + ".war");
} else {
antiLockingDocBase = new File(
System.getProperty("java.io.tmpdir"),
deploymentCount++ + "-" + docBase);
}
antiLockingDocBase = antiLockingDocBase.getAbsoluteFile();
// 删除原临时文件
ExpandWar.delete(antiLockingDocBase);
// 复制所有文件到临时目录
if (ExpandWar.copy(docBaseFile, antiLockingDocBase)) {
context.setDocBase(antiLockingDocBase.getPath());
}
}
}
StandardContext
startInternal
启动大致的操作如下
1、设置/创建jsp临时work目录
2、封装和加载当前应用的各种资源路径,包括docBase、WEB-INF/lib下jar包路径等
3、创建当前应用的WebAppClassLoader并绑定到当前线程,用于加载/WEB-INF/classes和/WEB-INF/lib目录下的class
4、发布configure_start事件(这里由ContextConfig接入进行加载web.xml等配置文件获取Servlet/Filter等定义)
5、第四步加载完所有类型的配置,这一步是启动所有配置,调用SCI初始化、实例化Listener、Filter、Servlet等。普通web.xml配置Spring容器的启动就在Listener的启动这步,没什么太需要说的,底下没有详细列代码
6、容器通用操作,启动后台任务线程、发布已启动事件start
@Override
protected synchronized void startInternal() throws LifecycleException {
// 。。。。
setConfigured(false);
boolean ok = true;
// 创建work目录,示例:apache-tomcat-6.0.53\work\Catalina\localhost
// jsp转为class的临时目录
postWorkDirectory();
// Add missing components as necessary
if (getResources() == null) { // (1) Required by Loader
try {
setResources(new StandardRoot(this));
} catch (IllegalArgumentException e) {
ok = false;
}
}
if (ok) {
// 启动StandardRoot对象
// StandardRoot对象中封装当前web应用资源
// 包括当前应用的项目class类路径、WEB-INF/lib下的jar包路径等
resourcesStart();
}
// 创建Context的加载器,其中包含web应用的WebappClassLoader
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// 。。。。。
// 将当前的webappClassLoader设置到当前线程,返回原线程中的classLoader
// 我debug执行,这里会返回null,因为此时webappClassLoader还未实例化
ClassLoader oldCCL = bindThread();
try {
if (ok) {
// 启动当前webAppLoader,主要是实例化当前应用的类加载器WebappClassLoader/ParallelWebappClassLoader
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
// 将原线程绑定的classLoader再恢复回来
// 如果oldCCL为null,空执行
unbindThread(oldCCL);
// 启动了webAppLoader之后,将实例化后的WebappClassLoader绑定到当前线程,返回原线程的classLoader
oldCCL = bindThread();
// 。。。。
}
// 发布configure_start事件,触发ContextConfig中的方法执行加载web.xml等配置
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
// configure_start事件执行完后,此时已经加载了子容器Warpper,启动之
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}
// 。。。。
}
// 。。。
// 调用ServletContainerInitializers,例如SpringBoot war包定义的启动类
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(), getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
// 开始实例化Listener/Filter/Servlet
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
// Reinitializing if something went wrong
if (!ok) {
setState(LifecycleState.FAILED);
} else {
setState(LifecycleState.STARTING);
}
}
ContextConfig
configureStart
在StandardContext启动中途会发布configure_start事件触发ContextConfig监听器的该方法执行,来加载web应用具体配置,加载web.xml、初始化容器等
/**
* Process a "contextConfig" event for this Context.
*/
protected synchronized void configureStart() {
// 解析/WEB-INF/web.xml
webConfig();
if (!context.getIgnoreAnnotations()) {
applicationAnnotationsConfig();
}
if (ok) {
validateSecurityRoles();
}
// Configure an authenticator if we need one
// Tomcat对Servlet的访问提供的安全机制,和Realm配合使用
// 简单的看就是web.xml中给Servlet配置能访问的角色role,在Realm组件中配置角色的用户名密码等信息(支持xml配置、jdbc等)
// 在请求访问Servlet时会被tomcat拦截要求输入用户名密码,调用Realm组件核验无误后才允许访问
// 没用过,好像不重要??
if (ok) {
authenticatorConfig();
}
// Make our application available if no problems were encountered
if (ok) {
context.setConfigured(true);
} else {
context.setConfigured(false);
}
}
webConfig
web应用配置的解析过程如下:
1、获取并加载路径 Tomcat目录/conf/web.xml 中通用的web.xml配置。其中配置了两个默认的Servlet,DefaultServlet和JspServlet,前者处理静态资源,后者处理后缀为 .jsp的请求(获取jsp文件,将其编译为Servlet,并执行其service方法返回静态页面)
2、获取应用目录/WEB-INF/web.xml 中的配置,解析为一个WebXml对象
3、解析web-fragment.xml,扫描ClassPath下(WebAppClassLoader及其父类能加载到的)所有的jar包的META-INF/web-fragment.xml文件。是Servlet3.0模块化新特性,xml配置与web.xml相同,即可以将通用配置配置在web-fragment.xml封入jar包,避免重复配置,具体可以百度用法
4、多个web-fragment.xml间的执行顺序在xml中可配置,这里按照顺序将多个xml对象排序。具体如何配置可以参考解析规则配置类WebRuleSet#addRuleInstances,博客参考Servlet3.0新特性之web-fragment.xml模块化配置文件 - 图图小淘气_real - 博客园
5、扫描ServletContainerInitializer实现类,这一步很重要,是不通过web.xml配置方式的入口,SpringBoot启动的时机就在这一步。ServletContainerInitializer 用于取代web.xml配置,使用编程风格注册Filter/Servlet等组件,使得编程框架(eg.spring)高度内聚,和容器Tomcat解耦。
原理:ServletContainerInitializer和HandlesTypes组合使用,ServletContainerInitializer是接口,定义如下,HandlesTypes是注解,表示ServletContainerInitializer实现类的处理对象类型,在Tomcat启动时由tomcat扫描所有class,将符合HandlesTypes配置的对象class作为参数c,然后调用该实现类进行初始化动作
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
接口实现类通过java SPI机制将实现类声明,例如spring-web.jar声明如下
SpringServletContainerInitializer 声明如下,即表示SpringServletContainerInitializer类中只处理实现了WebApplicationInitializer接口的类。在Tomcat启动该Context时会查找应用下所有实现了WebApplicationInitializer的类,将类列表作为参数传入调用SpringServletContainerInitializer.onStartup方法配置容器。
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
// 实例化所有WebApplicationInitializer的实现类,挨个调用onStartup方法
}
}
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
我们熟悉的SpringBoot项目war包部署初始化类如下,父类SpringBootServletInitializer 就是实现了WebApplicationInitializer
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
}
}
6、扫描当前应用和WEB-INF/lib下所有类,查找符合ServletContainerInitializer实现类配置的HandlesTypes的所有实现类。此外还有查找所有被WebServlet/WebFilter/WebListener注解的类,顾名思义也是个组件配置的路子,将这些类也作为配置添加到WebXml对象配置中
7、合并当前应用的WebXml对象和WEB-INF/lib下jar包的WebXml对象,把合并后的总配置信息挨个设置到StandardContext对象上
8、扫描WEB-INF/lib下jar包的META-INF/resources/路径,将静态资源也添加到当前Context
9、将扫描到的ServletContainerInitializer和HandlesTypes实现类配置到Context中
protected void webConfig() {
// web.xml的解析器,里边配置xml中各种元素的父子关系、解析规则等,和解析server.xml模式一致
WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
context.getXmlValidation(), context.getXmlBlockExternal());
// ============================1================================
Set<WebXml> defaults = new HashSet<>();
defaults.add(getDefaultWebXmlFragment(webXmlParser));
// ===========================2=================================
WebXml webXml = createWebXml();
// 开始解析,把WEB-INF/web.xml中的信息解析到WebXml对象中
InputSource contextWebXml = getContextWebXmlSource();
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}
ServletContext sContext = context.getServletContext();
// =============================3===================================
Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);
// =============================4====================================
Set<WebXml> orderedFragments = null;
orderedFragments =
WebXml.orderWebFragments(webXml, fragments, sContext);
// =============================5====================================
if (ok) {
// 查当前应用和WEB-INF/lib下 SPI配置的SCI接口实现类
processServletContainerInitializers();
}
// =============================6================================
if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
// 1、查当前应用和WEB-INF/lib下 SCI实现类配置的处理类型实现类
// 2、查当前应用和WEB-INF/lib下 被WebServlet/WebFilter/WebListener注解的类,将其也当作配置添加到当前Context
processClasses(webXml, orderedFragments);
}
// ================================7==============================
if (!webXml.isMetadataComplete()) {
// Step 6. Merge web-fragment.xml files into the main web.xml
// file.
if (ok) {
ok = webXml.merge(orderedFragments);
}
// Step 7. Apply global defaults
// Have to merge defaults before JSP conversion since defaults
// provide JSP servlet definition.
webXml.merge(defaults);
// Step 8. Convert explicitly mentioned jsps to servlets
if (ok) {
convertJsps(webXml);
}
// Step 9. Apply merged web.xml to Context
if (ok) {
configureContext(webXml);
}
} else {
webXml.merge(defaults);
convertJsps(webXml);
configureContext(webXml);
}
// ===============================8==========================
// Always need to look for static resources
// Step 10. Look for static resources packaged in JARs
if (ok) {
// Spec does not define an order.
// Use ordered JARs followed by remaining JARs
Set<WebXml> resourceJars = new LinkedHashSet<>();
for (WebXml fragment : orderedFragments) {
resourceJars.add(fragment);
}
for (WebXml fragment : fragments.values()) {
if (!resourceJars.contains(fragment)) {
resourceJars.add(fragment);
}
}
processResourceJARs(resourceJars);
// See also StandardContext.resourcesStart() for
// WEB-INF/classes/META-INF/resources configuration
}
// ===============================9========================
if (ok) {
for (Map.Entry<ServletContainerInitializer,
Set<Class<?>>> entry :
initializerClassMap.entrySet()) {
if (entry.getValue().isEmpty()) {
context.addServletContainerInitializer(
entry.getKey(), null);
} else {
context.addServletContainerInitializer(
entry.getKey(), entry.getValue());
}
}
}
}