文章目录
上下文
tomcat源码分析共2个阶段
1.tomcat如何接收处理请求源码分析(一)(Connector部分)
2.tomcat如何接收处理请求源码分析(二)(Container部分)本篇
目标:
本文主要分析tomcat
的Container
部分,已经封装好的HttpRequest,经过一层层Valve
一层层filter
最终是如何到达controller
。下图中的2号框和3号框即使本所涉及的范围。
正文
1container
结构分析
valve
(阀门)可以简单理解为过滤器(filter
),阀门和过滤器本来就相似,我们开发过web的同事应该都有使用filter
的经历,比如session
校验,非法请求拦截等一些常用的方案,都可以通过filter
解决。而我们tomcat
层面的valve
也类似于filter
,会对请求进行拦截。那么它和filter
的区别是什么?filter
拦截的目标是 tomcat
的 webapps
下的一个应用 ,而valve
则是属于容器层面的拦截。什么叫容器层面的拦截。我们前面分析过,一个请求最终进入到controller
,首先是经历了connector
对连接处理,然后进入container
容器,而容器内部先是进入最外层的Engine
,,然后进入第二层Host
,然后是Context
,最后是Wrapper
,这是一种嵌套的顺序,类似下图。
2对engine
、host
、context
、wrapper
的理解
容器是一个比较大的概念,以上四个都属于容器,大小不同,容器是针对tomcat内部的组件的抽象。
engine
理解为引擎、host
理解为虚拟主机、context
理解为webapps下的一个文件夹、wrapper
理解为一个controller。这些容器都可以在tomcat
的配置文件server.xml中配置,如下文所示。
<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWNING">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Service name="Catalina">
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
<Connector URIEncoding="UTF-8" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">//
<Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="XForwarded-Proto" protocolHeaderHttpsValue="https" httpsServerPort="443"/>
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false" deployOnStartup="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b" />
<context ...>
<Valve/>
<wrapper>
<valve>
</wrapper>
</context>
</Host>
</Engine>
</Service>
</Server>
现在我们现在大致理解了container
内部的一个调用关系。
3valve
机制理解
下图中红框所在位置,即我们本节需要分析的区域
controller
的方法打上断点之后我们看到的方法调用链上很多valve
后缀的类被调用了。我理解是,tomcat
的这些子容器都希望能在请求执行过程中调用一些和自己相关的操作。所以每个容器都会维护一个自己的Pipeline
对象(可以理解为一个队列),这个队列里面放入一些valve拦截类,这些拦截类可以拦截请求,并处理,处理完之后转给pipeline队列中的下一个valve类。当一个子容器的所有valve执行完后,请求进入下一个容器,并执行下个容器的相关valve
。
从上图我们看到Engine
有一个自己的pipeline
,里面只有一个valve
,Host
也有一个自己的pipeline
,里面有两个valve
,context
也有一个pipeline
,里面有两个valve
,最后一个wrapper
也有一个pipeline
,里面有一个valve
。
每个容器对应一个pipeline
,每个pipeline
至少会有一个默认valve,比如Engine
里面的StandardEngineValve
。除了这些StandardXXXValve
外,可以有其他valve
,也可以没有。在pipeLine 的valve队列里面,这些默认valve
的总是会放在最后。
现在我们大致了解了vavle
与pipleline
的数量情况,那么这些valve
和pipeline
什么时候初始化的,是通过什么样的方式,使得这些调用可以串联起来。可以肯定的是,这种牛逼的框架肯定不是硬编码的。那到底是怎样?
答案是在实例化这些子容器的时候,会同时实例化pipeline
,并往pipeline
中加入一个默认valve
例如
#StandardEngine 类对象的初始化过程如下
/**
* Create a new StandardEngine component with the default basic Valve.
*/
public StandardEngine() {
super();//父类中实例化了pipeline
//protected final Pipeline pipeline = new StandardPipeline(this);
//给pipeline设置默认的valve对象
pipeline.setBasic(new StandardEngineValve());
/* Set the jmvRoute using the system property jvmRoute */
try {
setJvmRoute(System.getProperty("jvmRoute"));
} catch(Exception ex) {
log.warn(sm.getString("standardEngine.jvmRouteFail"));
}
// By default, the engine will hold the reloading thread
backgroundProcessorDelay = 10;
}
那它的调用链又是如何串联起来的呢?
//CoyoteAdapter#service()
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
从connector
获取service
对象,再获取container
(这里是engine
),再获取engine
的pipeline
,取得pipeline
的第一个valve,调用invoke
方法。
** 如果进入的是pipeline
中最后一个标准valve
(有standard
的那种) **
那就拿出下一个子容器的pipeline
,取得第一个valve
去执行,例如:
# StandardEngineValve 作为最后一个valve 如何获得下一个子容器的pipeline
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//这个是StandardEngineValve,所以下个容器应该是host
Host host = request.getHost();
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getServerName()));
return;
}
// 调用host容器的pipeline的第一个valve对象
host.getPipeline().getFirst().invoke(request, response);
}
** 如果进入的是pipeline
中非最后一个valve
(也就是非标准valve
) **
#ErrorReportValve 属于host的pipeline中的第一个
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// 调用host的pipeline的下一个valve对象
getNext().invoke(request, response);
response.setSuspended(false);
try {
report(request, response, throwable);
} catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
}
至此我们已经初步讲完了 几个容器的valve
相关处理过程,与实现细节。接下来要研究的就是ApplicationFilterChain
4 拦截器 filter
(责任链模式调用)
下图即是我们本节需要搞懂的地方
使用责任链模式执行filter
拦截器。在程序员没有定义filter的情况下,系统会有5个默认的filter,一个filter执行完毕,共涉及到4个方法需要分析。
- 1
ApplicationFilterChain.doFilter(request, response)
- 2
ApplicationFilterChain.internalDoFilter(request, response)
; - 3
OncePerRequestFilter.doFilter(request, response)
;//防止重复调用 - 4
XXXFilter.doFilterInternal(request, response)
; // 真正的业务执行 - 1
ApplicationFilterChain.doFilter(request, response)
//回到起点
总结下上面的步骤
filterChain
会维护所有filter
,存在filters
数组中,通过调用filterChain
的doFilter
方法,转入内部的internalDoFilter
方法、依次调用所有filter
的doFilter
方法
这filter
的dofilter
方法并没有维护在filter
自己内部,而是调用的父类OncePerRequestFilter
的,
然后这个父类再统一调用实际子类的doFilterInternal方法执行真正的业务逻辑。
子类的internalDoFilter
方法中会继续调用filterChain
的doFilter
方法,至此一个完整的filter执行完毕。
为啥需要走一次OncePerRequestFilter
的doFilter
方法呢?因为父类的这个方法能保证无论是异步还是同步,对于一个请求,filters
里面的filter
都只会执行一次。
我们看下我们这个项目中调用了哪些filter
** 简单介绍下这几个filter **
CharacterEncodingFilter
主要解决body
内的字符编码问题
HiddenHttpMethodFilter
让浏览器form表单支持发出DELETE
、PUT
方式的请求
HttpPutFormContentFilter
put方法的content
处理,可以和post
一样拿到body
中的参数
RequestContextFilter
让你在一个请求的线程内,任意地方都可以获取到请求参数的相关信息,非常的方便。
filterChain
在执行完相关的filter
后就会进入servlet.service(request, response)
;方法,这才是真正的到Servlet
处理模块了。
5 Servlet处理
servlet
阶段我们有很多可以深入去了解的,但是我比较关心的是一个请求如何定位到一个controller
的方法。
经过debug调试我发现解析完请求头拿到uri&method
后,就可以在registry
里面直接拿到url
对应的方法。registry
是啥,下文有介绍。
其中MappingRegistry
是AbstractHandlerMethodMapping
的子类
registry
又是MappingRegistry
的一个hashmap
类型的属性,从这个hashmap
的key
可以看出,只要能拿到uri和method就可以直接拿到一个HandlerMethod
这个里面就有我们controller
的方法了,最后通过反射调用即可。
问题又来了,这些uri
的映射关系是什么时候初始化的呢?
我们发现AbstractHandlerMethodMapping
是一个InitializingBean
的实现类,而在spring
中凡是InitializingBean
的实现类,在bean
初始化后都会调用自身的一个afterPropertiesSet
方法。我们看看这个方法
#AbstractHandlerMethodMapping的afterPropertiesSet方法
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
//获取所有的beanName
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
//对每个bean检查下是否是一个hander,什么算是handler?凡是有Controller和RequestMapping注解的都算
for (String beanName : beanNames) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class<?> beanType = null;
beanType = obtainApplicationContext().getType(beanName);
//在这里检查hander
if (beanType != null && isHandler(beanType)) {
//对handler提取method
detectHandlerMethods(beanName);
}
}
}
handlerMethodsInitialized(getHandlerMethods());
}
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
至此如何由uri
映射到我们controller
里面的一个方法这个过程,基本算是分析完了。