tomcat如何处理请求 源码分析(二)(Container部分)

上下文

tomcat源码分析共2个阶段
1.tomcat如何接收处理请求源码分析(一)(Connector部分)
2.tomcat如何接收处理请求源码分析(二)(Container部分)本篇

目标:

本文主要分析tomcatContainer部分,已经封装好的HttpRequest,经过一层层Valve一层层filter最终是如何到达controller。下图中的2号框和3号框即使本所涉及的范围。
在这里插入图片描述
在这里插入图片描述

正文

1container结构分析

valve(阀门)可以简单理解为过滤器(filter),阀门和过滤器本来就相似,我们开发过web的同事应该都有使用filter的经历,比如session校验,非法请求拦截等一些常用的方案,都可以通过filter解决。而我们tomcat层面的valve也类似于filter,会对请求进行拦截。那么它和filter的区别是什么?filter拦截的目标是 tomcatwebapps下的一个应用 ,而valve则是属于容器层面的拦截。什么叫容器层面的拦截。我们前面分析过,一个请求最终进入到controller,首先是经历了connector对连接处理,然后进入container容器,而容器内部先是进入最外层的Engine,,然后进入第二层Host,然后是Context,最后是Wrapper,这是一种嵌套的顺序,类似下图。
在这里插入图片描述

2对enginehostcontextwrapper的理解

容器是一个比较大的概念,以上四个都属于容器,大小不同,容器是针对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 &quot;%r&quot; %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,里面只有一个valveHost也有一个自己的pipeline,里面有两个valvecontext也有一个pipeline,里面有两个valve,最后一个wrapper也有一个pipeline,里面有一个valve

每个容器对应一个pipeline,每个pipeline至少会有一个默认valve,比如Engine里面的StandardEngineValve。除了这些StandardXXXValve外,可以有其他valve,也可以没有。在pipeLine 的valve队列里面,这些默认valve的总是会放在最后。

现在我们大致了解了vavlepipleline的数量情况,那么这些valvepipeline什么时候初始化的,是通过什么样的方式,使得这些调用可以串联起来。可以肯定的是,这种牛逼的框架肯定不是硬编码的。那到底是怎样?

答案是在实例化这些子容器的时候,会同时实例化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),再获取enginepipeline,取得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数组中,通过调用filterChaindoFilter方法,转入内部的internalDoFilter方法、依次调用所有filterdoFilter方法
filterdofilter方法并没有维护在filter自己内部,而是调用的父类OncePerRequestFilter的,
然后这个父类再统一调用实际子类的doFilterInternal方法执行真正的业务逻辑。
子类的internalDoFilter方法中会继续调用filterChaindoFilter方法,至此一个完整的filter执行完毕。

为啥需要走一次OncePerRequestFilterdoFilter方法呢?因为父类的这个方法能保证无论是异步还是同步,对于一个请求,filters里面的filter都只会执行一次。

我们看下我们这个项目中调用了哪些filter在这里插入图片描述
** 简单介绍下这几个filter **
CharacterEncodingFilter主要解决body内的字符编码问题
HiddenHttpMethodFilter让浏览器form表单支持发出DELETEPUT方式的请求
HttpPutFormContentFilterput方法的content处理,可以和post一样拿到body中的参数
RequestContextFilter让你在一个请求的线程内,任意地方都可以获取到请求参数的相关信息,非常的方便。

filterChain在执行完相关的filter后就会进入servlet.service(request, response);方法,这才是真正的到Servlet处理模块了。

5 Servlet处理

servlet阶段我们有很多可以深入去了解的,但是我比较关心的是一个请求如何定位到一个controller的方法。
经过debug调试我发现解析完请求头拿到uri&method后,就可以在registry里面直接拿到url对应的方法。registry是啥,下文有介绍。
在这里插入图片描述
其中MappingRegistryAbstractHandlerMethodMapping的子类
registry又是MappingRegistry的一个hashmap类型的属性,从这个hashmapkey可以看出,只要能拿到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里面的一个方法这个过程,基本算是分析完了。

猜你喜欢

转载自blog.csdn.net/ygy982883422/article/details/105695537