前言
在上一篇文章Tomcat的生命周期(二)中我们分析了Container
所有子容器的初始化和启动过程、Connector
连接器的初始化,并介绍了Tomcat实现热加载的原理,本文同样基于之前所有Tomcat系列文章,主要对Connector
的启动过程进行讲解,同时介绍Mapper
和MapperListener
的运行机制,为下一篇讲解Tomcat对请求响应的处理做铺垫
Connector
的启动入口在StandardService
的startInternal()
中,最终调用Connector.startInternal()
启动方法可以分成两部分:1. 启动协议处理类,对于本文来说
Http11Protocol
是其具体实现类,初始化过程已在Tomcat架构中各个组件及组件间关系(二)中分析过;2. 容器组件映射关系监听器MapperListener
启动,该类非常重要,保存了Host
、Context
、Wrapper
之间的映射关系,试想一下,当一个请求过来时Tomcat是如何知道请求对应的是哪个war包,哪个Servlet
呢?MapperListener
和Mapper
类就做了请求“引路人”的作用。我们先看第一部分本文的分析可能设置各种内部类、方法之间的跳转,读者可以借助Tomcat的生命周期(二)中图6提供的类图帮助理解。
protocolHandler.start()
会调用协议处理父类AbstractProtocol.start()
方法内调用了端到端处理类
JIoEndpoint.start()
,实际会调用父类AbstractEndpoint.start()
模板方法进入具体实现
JIoEndpoint.startInternal()
,如果读者顺着Tomcat系列文章顺序看下来,应该对这个“套路”非常熟悉了,我们就不把时间浪费在重复了很多次的思路上面了在Tomcat架构中各个组件及组件间关系(二)中讲到解析
server.xml
中<Connector>
时曾经说过,默认情况下Connector
是没有线程池的,但是即使不在server.xml
中设置executor
在启动Connector
时Tomcat也会创建一个默认的线程池,对应的就是这里的createExecutor()
,从严谨的角度来说,这个线程池适用于处理端到端连接的线程池,即属于AbstractEndpoint
及其子类TaskQueue
继承自LinkedBlockingQueue
并重写了关键的take()
、offer(Runnable)
方法,通过创建的线程工厂TaskThreadFactory
设置了线程池的名称,开启守护线程并设置优先级为NORMAL
。线程池构造器中传递的参数分别设置corePoolSize = 10
,maxPoolSize = 200
,keepAliveTime = 60s
图4中的
InitializeConnectionLatch()
设置了端到端处理类最大连接数量为200,该数字在JIoEndpoint.bind()
中进行了设置,最后创建了一个异步请求超时线程,不是我们讲解的重点,我们来看下startAcceptorThreads()
第一句得到
Acceptor
线程的数量,该值同样在初始化时由JIoEndpoint.bind()
中进行了设置为1,调用createAcceptor()
创建对应端到端类型的Acceptor
线程,对应代码清单1
protected class Acceptor extends AbstractEndpoint.Acceptor {
@Override
public void run() {
int errorDelay = 0;
// Loop until we receive a shutdown command
while (running) {
// Loop if endpoint is paused
while (paused && running) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}
if (!running) {
break;
}
state = AcceptorState.RUNNING;
try {
//if we have reached max connections, wait
// (1)
countUpOrAwaitConnection();
Socket socket = null;
try {
// Accept the next incoming connection from the server
// socket
// (2)
socket = serverSocketFactory.acceptSocket(serverSocket);
} catch (IOException ioe) {
// (3)
countDownConnection();
// Introduce delay if necessary
errorDelay = handleExceptionWithDelay(errorDelay);
// re-throw
throw ioe;
}
// Successful accept, reset the error delay
errorDelay = 0;
// Configure the socket
// (4)
if (running && !paused && setSocketOptions(socket)) {
// Hand this socket off to an appropriate processor
// (5)
if (!processSocket(socket)) {
countDownConnection();
// Close socket right away
closeSocket(socket);
}
} else {
countDownConnection();
// Close socket right away
closeSocket(socket);
}
} catch (IOException x) {
if (running) {
log.error(sm.getString("endpoint.accept.fail"), x);
}
} catch (NullPointerException npe) {
if (running) {
log.error(sm.getString("endpoint.accept.fail"), npe);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
}
标注(1)对当前连接数进行判断,如果超过了阈值200则阻塞等待其他连接释放,底层用了AQS的无阻塞锁机制。之前分析过默认情况下或者说没有开启SSL的情况下产生Socket
的工厂为DefaultServerSocketFactory
,标注(2)底层其实就是服务端阻塞等待socket
连接的过程,当连接过程出现异常时由标注(3)的代码释放latch
门栓,防止资源被白白占用。标注(4)设置了一些socket
的连接参数,Tomcat中将所有socket
参数封装在SocketProperties
中,在使用过程中我们可以根据请求状况调整这些参数。比如,在Tomcat的生命周期(二)中初始化协议处理类Http11Protocol
时设置了socket
连接超时时间,是否支持延迟等参数。标注(5)是处理请求的入口
SocketProcessor
是在每一种类型的端到端处理类中的内部类,实现了Runnable
,总的来说一个Acceptor
线程监听端口得到socket
,一个socket
又对应一个SocketProcessor
线程,而所有的SocketProcessor
又会在一个corePoolSize = 10
,maxPoolSize = 200
的线程池中运行,进入SocketProcessor
意味着正式进入Tomcat处理请求响应的流程中,将在下一篇文章中详细分析接着我们分析一下图1中启动的第二部分,在正式开始之前,我们先找到
mapperListener
是何时创建的,因为之前的文章中并没有说到该类。实际上该类和另外一个有关系的类Mapper
是Connector
的两个成员变量,在创建Connector
时一同创建Mapper
中保存了所有Container
容器的对应关系,类中有几个内部类MapElement
、Host
、ContextList
、Context
、ContextVersion
和Wrapper
,其中Host
、Context
和Wrapper
继承了抽象类MapElement
,其中包含两个元素:1. name表示对应Container
容器的名称;2. object表示容器本身对象。Host
中持有ContextList
的引用,并维护了一个保存该Host
所有alias的集合;ContextList
持有Context[]
的引用;Context
中维护了一个ContextVersion[]
保存了一个war包的不同版本实例;ContextVersion
表示了某一特定版本的war包,其下必有代表多个Servlet
的Wrapper
数组MapperListener
实现了两个监听器接口,一个是经常出镜的LifecycleListener
,针对Tomcat整体生命周期进行监听;另一个是只用来监听Container
相关事件的ContainerListener
。前一个已经分析吐了,这里不再累述。所有Container
特有事件都保存在Container
接口中当有上述任意事件发生时,
Container
容器会首先调用ContainerBase.fireContainerEvent(String, Object)
,进而封装成ContainerEvent
,调用ContainerListener.conatinerEvent(ContainerEvent)
的具体实现,由感兴趣的监听器进行处理,至于MapperListener
的conatinerEvent(ContainerEvent)
的分析暂且放一放,先回到主题MapperListener
的startInternal()
上来findDefaultHost()
设置默认的Host
StandardService
作为Tomcat两大组件的“组合器”,因此Connector
需要通过上层容器StandardService
做一次中转找到对应的Container
容器StandardEngine
,然后得到<Engine>
中配置的defaultHost
属性的值,再与StandardEngine
下所有的StandardHost
一一比较,如果存在对应的实体(存在name
属性与<Engine> defaultHost
属性相同的<Host>
标签),就可以设置默认host
名称为<Engine>
中defaultHost
属性的值图10中
addListeners(engin)
如上图所示,采用了和ContainerBackgroundProcessor.processChildren(Container, ClassLoader)
同样的递归处理,让StandardEngine
下所有的children都添加了MapperListener
。最后看一下registerHost(Host)
代码中得到待注册
Host
所有的别名,将别名数组,Host
名称和对象本身塞入addHost(String, String[], Object)
中同
addService(Service)
等添加子容器的方法思路一样,这里添加一个Host
也首先创建一个比原数组大1的新数组,然后通过insertMap(Mapper.MapElement[], Mapper.MapElment[], Mapper.MapElement)
方法将老数组copy到新数组中,最后将老数组的引用指向新数组该方法是一个公共抽取方法,所有继承
MapElement
的映射组件都能通过该方法完成添加操作。其中find(MapElement[], String)
根据第二个参数(新元素的名称,不限于Host的名称)与第一个参数的数组中元素的名称进行比较(数组中元素根据名称有序排列),返回名称相同元素或者closest inferior元素(知道意思但不会用中文如何优雅的表达,抱歉,哈哈)的索引,该索引就是新元素要插入的索引减一,如果找到同名的元素,该方法会返回false,新重复元素在图14else中代码会覆盖老重复元素。最后会将新Host
元素的所有alias与该元素进行关联我们回到图13注册
Host
流程的后半部分,在Mapper
添加新Host
之后会遍历该Host
下所有的children并开始registerContext(Context)
从上图中可以发现在将
Context
真正放入Mapper
之前程序首先遍历了Context
下所有的StandardWrapper
,并调用prepareWrapperMappingInfo(Context, Wrapper, List<WrapperMappingInfo>)
在Tomcat架构中各个组件及组件间关系(二)中我们曾经分析过解析
web.xml
的规则,文件中就包含对于<servlet-mapping>
标签的处理,当解析到该标签时会调用WebXml
的addServletMapping(String, String)
,方法的两个参数对应了<servlet-mapping>
两个子标签<url-pattern>
和<servlet-name>
的值,所有的<servlet-mapping>
标签解析后都会放在Map<String,String> servletMappings
集合中。而在StandardContext
启动的流程中会发送CONFIGURE_START_EVENT
给ContextConfig
,进而产生configureStart()-->webConfig()-->configureContext(Context)-->StandardContext.addServletMapping(String, String)
最终将WebXml
中serlvetMappings
的values(所有<servlet-mapping>
下<url-pattern>
的集合)放入StandardWrapper
的成员变量ArrayList<String> mappings
中,该变量就对应上图中的mappings
数组。之后遍历所有的<servlet-mapping>
映射,如果servlet name
为jsp并且<url-pattern>
以通配符/*
结束,则认为该Servlet
是专门处理jsp的Servlet
,置标志位jspWildCard
为true。最后将封装好的WrapperMappingInfo
放入参数集合wrappers
中回到图16,最后将
Host
、Context
、WrapperMappingInfo
集合等信息传入addContextVersion
方法中,虽然我能理解这个方法为什么要传递这么多参数,可能是因为添加的ContextVersion
对象属于承上启下的中间对象,既作为Context
中的一个版本对象,也要处理下属的Wrapper
对象间关系,但是还是觉得传递这么多参数对于一个方法而言略多,我们在代码清单2中分析一下该方法
/**
* Add a new Context to an existing Host.
*
* @param hostName Virtual host name this context belongs to
* @param host Host object
* @param path Context path
* @param version Context version
* @param context Context object
* @param welcomeResources Welcome files defined for this context
* @param resources Static resources of the context
* @param wrappers Information on wrapper mappings
* @param mapperContextRootRedirectEnabled Mapper does context root redirects
* @param mapperDirectoryRedirectEnabled Mapper does directory redirects
*/
public void addContextVersion(String hostName, Object host, String path,
String version, Object context, String[] welcomeResources,
javax.naming.Context resources, Collection<WrapperMappingInfo> wrappers,
boolean mapperContextRootRedirectEnabled, boolean mapperDirectoryRedirectEnabled) {
// (1)
Host mappedHost = exactFind(hosts, hostName);
if (mappedHost == null) {
addHost(hostName, new String[0], host);
mappedHost = exactFind(hosts, hostName);
if (mappedHost == null) {
log.error("No host found: " + hostName);
return;
}
}
// (2)
if (mappedHost.isAlias()) {
log.error("No host found: " + hostName);
return;
}
int slashCount = slashCount(path);
synchronized (mappedHost) {
// (3)
ContextVersion newContextVersion = new ContextVersion(version, context);
newContextVersion.path = path;
newContextVersion.slashCount = slashCount;
newContextVersion.welcomeResources = welcomeResources;
newContextVersion.resources = resources;
newContextVersion.mapperContextRootRedirectEnabled = mapperContextRootRedirectEnabled;
newContextVersion.mapperDirectoryRedirectEnabled = mapperDirectoryRedirectEnabled;
if (wrappers != null) {
// (4)
addWrappers(newContextVersion, wrappers);
}
ContextList contextList = mappedHost.contextList;
// (5)
Context mappedContext = exactFind(contextList.contexts, path);
if (mappedContext == null) {
mappedContext = new Context(path, newContextVersion);
// (6)
ContextList newContextList = contextList.addContext(
mappedContext, slashCount);
if (newContextList != null) {
// (7)
updateContextList(mappedHost, newContextList);
}
} else {
ContextVersion[] contextVersions = mappedContext.versions;
ContextVersion[] newContextVersions =
new ContextVersion[contextVersions.length + 1];
if (insertMap(contextVersions, newContextVersions, newContextVersion)) {
// (8)
mappedContext.versions = newContextVersions;
} else {
// Re-registration after Context.reload()
// Replace ContextVersion with the new one
// (9)
int pos = find(contextVersions, version);
if (pos >= 0 && contextVersions[pos].name.equals(version)) {
contextVersions[pos] = newContextVersion;
}
}
}
}
}
方法每个参数的含义在注释中写的很清楚,我们主要看代码逻辑。标注(1)从Host[]
中查找匹配第二个参数hostName
的Host
,如果没有找到,说明该Host
还没有注册,调用addHost(String, String[], Object)
先添加到映射中,之后进行二次校验判断是否添加成功,如果还没有添加成功则结束流程。标注(2)说明了一点,Host
必须存在别名,否则无法执行操作。标注(3)根据参数构建出本次版本的ContextVersion
,如果参数wrappers
不为空,则先进行Wrapper
的映射添加,addWrappers(ContextVersion, Collection<WrapperMappingInfo>)
最终会调用代码清单3 中展示的方法
/**
* Adds a wrapper to the given context.
*
* @param context The context to which to add the wrapper
* @param path Wrapper mapping
* @param wrapper The Wrapper object
* @param jspWildCard true if the wrapper corresponds to the JspServlet
* and the mapping path contains a wildcard; false otherwise
* @param resourceOnly true if this wrapper always expects a physical
* resource to be present (such as a JSP)
*/
protected void addWrapper(ContextVersion context, String path,
Object wrapper, boolean jspWildCard, boolean resourceOnly) {
synchronized (context) {
if (path.endsWith("/*")) {
// Wildcard wrapper
String name = path.substring(0, path.length() - 2);
Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
resourceOnly);
Wrapper[] oldWrappers = context.wildcardWrappers;
Wrapper[] newWrappers =
new Wrapper[oldWrappers.length + 1];
if (insertMap(oldWrappers, newWrappers, newWrapper)) {
context.wildcardWrappers = newWrappers;
int slashCount = slashCount(newWrapper.name);
if (slashCount > context.nesting) {
context.nesting = slashCount;
}
}
} else if (path.startsWith("*.")) {
// Extension wrapper
String name = path.substring(2);
Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
resourceOnly);
Wrapper[] oldWrappers = context.extensionWrappers;
Wrapper[] newWrappers =
new Wrapper[oldWrappers.length + 1];
if (insertMap(oldWrappers, newWrappers, newWrapper)) {
context.extensionWrappers = newWrappers;
}
} else if (path.equals("/")) {
// Default wrapper
Wrapper newWrapper = new Wrapper("", wrapper, jspWildCard,
resourceOnly);
context.defaultWrapper = newWrapper;
} else {
// Exact wrapper
final String name;
if (path.length() == 0) {
// Special case for the Context Root mapping which is
// treated as an exact match
name = "/";
} else {
name = path;
}
Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
resourceOnly);
Wrapper[] oldWrappers = context.exactWrappers;
Wrapper[] newWrappers =
new Wrapper[oldWrappers.length + 1];
if (insertMap(oldWrappers, newWrappers, newWrapper)) {
context.exactWrappers = newWrappers;
}
}
}
}
从代码中可以和明显的看出,根据path
参数(对应<url-pattern>
)的不同,逻辑分为四个部分:1. 以/*
结尾的通配符匹配规则;2. 以*.
开始的扩展名匹配规则;3. 代表默认匹配规则的路径/
;4. 不满足上述三种的精确名匹配规则。如果大家对Servlet
有一定深度了解的话就会秒懂,这里的四种路径匹配分类正好对应了Servlet
的四种匹配规则,而这四种配置的Wrapper
会分别放置在ContextVersion
中对应的Wrapper[]
中
我们回到代码清单2,标注(5)根据
context path
在Context[]
中寻找匹配项,如果不存在匹配context path
的Context
,进入新增Context
流程,ContextList.addContext(Context, int)
将新增的Context
放入ContextList
中,而updateContextList(Host, ContextList)
更新改动后ContextList
所属Host
内的引用;如果存在同路径Context
则进入添加同路径Context
不同版本ContextVersion
流程,调用inserMap(MapElement[], MapElement[], MapElement)
进行顺位插入,如果发现存在一个同版本的ContextVersion
对象,则插入失败,进入最后的else流程,找到重复version的ContextVersion
并用新元素覆盖老元素至此所有元素地址对应元素实体的关系都存储在
Mapper
中,当请求到来时,可以根据StandardHost
中的成员变量mapper
定位到具体的Servlet
,最后我们再来看看上面提到的ContainerEvent
触发方法,代码清单4
@Override
public void containerEvent(ContainerEvent event) {
if (Container.ADD_CHILD_EVENT.equals(event.getType())) {
Container child = (Container) event.getData();
addListeners(child);
// If child is started then it is too late for life-cycle listener
// to register the child so register it here
if (child.getState().isAvailable()) {
if (child instanceof Host) {
registerHost((Host) child);
} else if (child instanceof Context) {
registerContext((Context) child);
} else if (child instanceof Wrapper) {
// Only if the Context has started. If it has not, then it
// will have its own "after_start" life-cycle event later.
if (child.getParent().getState().isAvailable()) {
registerWrapper((Wrapper) child);
}
}
}
} else if (Container.REMOVE_CHILD_EVENT.equals(event.getType())) {
Container child = (Container) event.getData();
removeListeners(child);
// No need to unregister - life-cycle listener will handle this when
// the child stops
} else if (Host.ADD_ALIAS_EVENT.equals(event.getType())) {
// Handle dynamically adding host aliases
mapper.addHostAlias(((Host) event.getSource()).getName(),
event.getData().toString());
} else if (Host.REMOVE_ALIAS_EVENT.equals(event.getType())) {
// Handle dynamically removing host aliases
mapper.removeHostAlias(event.getData().toString());
} else if (Wrapper.ADD_MAPPING_EVENT.equals(event.getType())) {
// Handle dynamically adding wrappers
Wrapper wrapper = (Wrapper) event.getSource();
Context context = (Context) wrapper.getParent();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String version = context.getWebappVersion();
String hostName = context.getParent().getName();
String wrapperName = wrapper.getName();
String mapping = (String) event.getData();
boolean jspWildCard = ("jsp".equals(wrapperName)
&& mapping.endsWith("/*"));
mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
jspWildCard, context.isResourceOnlyServlet(wrapperName));
} else if (Wrapper.REMOVE_MAPPING_EVENT.equals(event.getType())) {
// Handle dynamically removing wrappers
Wrapper wrapper = (Wrapper) event.getSource();
Context context = (Context) wrapper.getParent();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String version = context.getWebappVersion();
String hostName = context.getParent().getName();
String mapping = (String) event.getData();
mapper.removeWrapper(hostName, contextPath, version, mapping);
} else if (Context.ADD_WELCOME_FILE_EVENT.equals(event.getType())) {
// Handle dynamically adding welcome files
Context context = (Context) event.getSource();
String hostName = context.getParent().getName();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String welcomeFile = (String) event.getData();
mapper.addWelcomeFile(hostName, contextPath,
context.getWebappVersion(), welcomeFile);
} else if (Context.REMOVE_WELCOME_FILE_EVENT.equals(event.getType())) {
// Handle dynamically removing welcome files
Context context = (Context) event.getSource();
String hostName = context.getParent().getName();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String welcomeFile = (String) event.getData();
mapper.removeWelcomeFile(hostName, contextPath,
context.getWebappVersion(), welcomeFile);
} else if (Context.CLEAR_WELCOME_FILES_EVENT.equals(event.getType())) {
// Handle dynamically clearing welcome files
Context context = (Context) event.getSource();
String hostName = context.getParent().getName();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
mapper.clearWelcomeFiles(hostName, contextPath,
context.getWebappVersion());
}
}
事件处理中涉及的核心逻辑和方法本文中都细细分析过了,比如事件ADD_CHILD_EVENT
流程中首先会调用addListener(Container)
用递归方式将新添加的Container
下所有children都加上MapperListener
,再根据添加容器的不同类型调用不同的register
方法。另外ADD_MAPPING_EVENT
和REMOVE_MAPPING_EVENT
事件只是添加/删除Wrapper
的映射