服务器之家

服务器之家 > 正文

Spring Boot启动过程(四)之Spring Boot内嵌Tomcat启动

时间:2020-09-16 15:26     来源/作者:draculav

之前在spring boot启动过程(二)提到过createembeddedservletcontainer创建了内嵌的servlet容器,我用的是默认的tomcat。

private void createembeddedservletcontainer() {
 embeddedservletcontainer localcontainer = this.embeddedservletcontainer;
 servletcontext localservletcontext = getservletcontext();
 if (localcontainer == null && localservletcontext == null) {
  embeddedservletcontainerfactory containerfactory = getembeddedservletcontainerfactory();
  this.embeddedservletcontainer = containerfactory
   .getembeddedservletcontainer(getselfinitializer());
 }
 else if (localservletcontext != null) {
  try {
  getselfinitializer().onstartup(localservletcontext);
  }
  catch (servletexception ex) {
  throw new applicationcontextexception("cannot initialize servlet context",
   ex);
  }
 }
 initpropertysources();
 }

  getembeddedservletcontainerfactory方法中调用了serverproperties,从serverproperties的实例方法customize可以看出springboot支持三种内嵌容器的定制化配置:tomcat、jetty、undertow。

  这里直接说tomcatembeddedservletcontainerfactory的getembeddedservletcontainer方法了,原因在前面那篇里说过了。不过首先是getselfinitializer方法先执行的:

 private org.springframework.boot.web.servlet.servletcontextinitializer getselfinitializer() {
 return new servletcontextinitializer() {
  @override
  public void onstartup(servletcontext servletcontext) throws servletexception {
  selfinitialize(servletcontext);
  }
 };
 }

  将初始化的servletcontextinitializer传给了getembeddedservletcontainer方法。进入了getembeddedservletcontainer方法直接就是实例化了一个tomcat:

 tomcat tomcat = new tomcat();

   然后生成一个临时目录,并tomcat.setbasedir,setbasedir方法的注释说tomcat需要一个目录用于临时文件并且它应该是第一个被调用的方法;如果方法没有被调用会使用默认的几个位置system properties - catalina.base, catalina.home - $pwd/tomcat.$port,另外/tmp从安全角度来说不建议。

  接着:

 connector connector = new connector(this.protocol);

  创建Connector过程中,静态代码块:单独抽出来写了。RECYCLE_FACADES属性可以通过启动参数JAVA_OPTS来配置: -Dorg.apache.catalina.connector.RECYCLE_FACADES=,默认是false,配置成true可以提高安全性但同时性能会有些损耗,参考:http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html和http://bztax.gov.cn/docs/security-howto.html。其他属性不细说了,Connector构造的逻辑主要是在NIO和APR选择中选择一个协议,我的是org.apache.coyote.http11.Http11NioProtocol,然后反射创建实例并强转为ProtocolHandler。关于apr,似乎是更native,性能据说更好,但我没测,相关文档可参考:http://tomcat.apache.org/tomcat-8.5-doc/apr.html。这里简单提一下coyote,它的主要作用是将socket接受的信息封装为request和response并提供给上Servlet容器,进行上下层之间的沟通,文档我没找到比较新的:http://tomcat.apache.org/tomcat-4.1-doc/config/coyote.html。STRICT_SERVLET_COMPLIANCE也是启动参数控制,默认是false,配置项是org.apache.catalina.STRICT_SERVLET_COMPLIANCE,默认情况下会设置URIEncoding = "UTF-8"和URIEncodingLower = URIEncoding.toLowerCase(Locale.ENGLISH),相关详细介绍可参考:http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html。Connector的创建过程比较关键,容我单独写一篇吧。

  connector实例创建好了之后tomcat.getservice().addconnector(connector),getservice的getserver中new了一个standardserver,standardserver的初始化主要是创建了globalnamingresources(globalnamingresources主要用于管理明明上下文和jdni上下文),并根据catalina.usenaming判断是否注册namingcontextlistener监听器给lifecyclelisteners。创建server之后initbasedir,先读取catalina.home配置system.getproperty(globals.catalina_base_prop),如果没取到则使用之前生成的临时目录,这段直接看代码吧:

protected void initbasedir() {
 string catalinahome = system.getproperty(globals.catalina_home_prop);
 if (basedir == null) {
  basedir = system.getproperty(globals.catalina_base_prop);
 }
 if (basedir == null) {
  basedir = catalinahome;
 }
 if (basedir == null) {
  // create a temp dir.
  basedir = system.getproperty("user.dir") +
  "/tomcat." + port;
 }
 file basefile = new file(basedir);
 basefile.mkdirs();
 try {
  basefile = basefile.getcanonicalfile();
 } catch (ioexception e) {
  basefile = basefile.getabsolutefile();
 }
 server.setcatalinabase(basefile);
 system.setproperty(globals.catalina_base_prop, basefile.getpath());
 basedir = basefile.getpath();
 if (catalinahome == null) {
  server.setcatalinahome(basefile);
 } else {
  file homefile = new file(catalinahome);
  homefile.mkdirs();
  try {
  homefile = homefile.getcanonicalfile();
  } catch (ioexception e) {
  homefile = homefile.getabsolutefile();
  }
  server.setcatalinahome(homefile);
 }
 system.setproperty(globals.catalina_home_prop,
  server.getcatalinahome().getpath());
 }

   然后又实例化了个standardservice,代码并没有什么特别的:

 service = new standardservice();
 service.setname("tomcat");
 server.addservice( service )

  server.addservice( service )这里除了发布了一个propertychangeevent事件,也没做什么特别的,最后返回这个server。addconnector的逻辑和上面addservice没什么区别。然后是customizeconnector,这里设置了connector的端口、编码等信息,并将“bindoninit”和对应值false写入了最开头说的静态代码块中的replacements集合,introspectionutils.setproperty(protocolhandler, repl, value)通过反射的方法将protocolhandler实现对象的setbindoninit存在的情况下(拼字符串拼出来的)set为前面的false,这个方法里有大量的判断比如参数类型及setter的参数类型,比如返回值类型以及没找到还会try a setproperty("name", "value")等,setproperty可以处理比如abstractendpoint中有个hashmap<string, object> attributes的属性时会attributes.put(name, value)。如果是ssl还会执行customizessl方法,设置一些ssl用的属性比如协议比如秘钥还有可以用上秘钥仓库等。如果配置了压缩,这里还会给协议的相关setter设置值。tomcat.setconnector(connector)不解释。tomcat.gethost().setautodeploy(false),gethost方法中创建了standardhost并设置host名(例如localhost),并getengine().addchild( host );然后设置host的自动部署。configureengine(tomcat.getengine()),getengine中如果engine为null就初始化标准引擎,设置名字为tomcat,设置realm和service.setcontainer(engine),不过这里engine已经在gethost初始化过了所以直接返回;configureengine方法先设置引擎的后台进程延迟,并将引擎的value对象注册给引擎的pipeline,此时尚无value对象实例。这里简单说明一下:value对象在tomcat的各级容器中都有标准类型,并且各级容器都有一个pipeline,在请求处理过程中会从各级的第一个value对象开始依次执行一遍,value用于加入到对应的各级容器的逻辑,默认有一个标注value实现,名字类似standardhostvalue。

  preparecontext(tomcat.gethost(), initializers),initializers这里是annotationconfigembeddedwebapplicationcontext,context级的根;准备context的过程主要设置base目录,new一个tomcatembeddedcontext并在构造中判断了下loadonstartup方法是否被重写;注册一个fixcontextlistener监听,这个监听用于设置context的配置状态以及是否加入登录验证的逻辑;context.setparentclassloader;设置各种语言的编码映射,我这里是en和fr设置为utf-8,此处可以使用配置文件org/apache/catalina/util/charsetmapperdefault .properties;设置是否使用相对地址重定向userelativeredirects=false,此属性应该是tomcat 8.0.30版本加上的;接着就是初始化webapploader,这里和完整版的tomcat有点不一样,它用的是虚拟机的方式,会将加载类向上委托loader.setdelegate(true),context.setloader(loader);之后就开始创建wapper了,至此engine,host,context及wrapper四个层次的容器都创建完了:

 private void adddefaultservlet(context context) {
 wrapper defaultservlet = context.createwrapper();
 defaultservlet.setname("default");
 defaultservlet.setservletclass("org.apache.catalina.servlets.defaultservlet");
 defaultservlet.addinitparameter("debug", "0");
 defaultservlet.addinitparameter("listings", "false");
 defaultservlet.setloadonstartup(1);
 // otherwise the default location of a spring dispatcherservlet cannot be set
 defaultservlet.setoverridable(true);
 context.addchild(defaultservlet);
 addservletmapping(context, "/", "default");
 }

   connector从socket接收的数据,解析成httpservletrequest后就会经过这几层容器,有容器各自的value对象链依次处理。

  接着是是否注册jspservlet,jasperinitializer和storemergedwebxmllistener我这里是都没有的。接着的mergeinitializers方法:

 protected final servletcontextinitializer[] mergeinitializers(
  servletcontextinitializer... initializers) {
 list<servletcontextinitializer> mergedinitializers = new arraylist<servletcontextinitializer>();
 mergedinitializers.addall(arrays.aslist(initializers));
 mergedinitializers.addall(this.initializers);
 return mergedinitializers
  .toarray(new servletcontextinitializer[mergedinitializers.size()]);
 }

Spring Boot启动过程(四)之Spring Boot内嵌Tomcat启动

  configurecontext(context, initializerstouse)对context做了些设置工作,包括tomcatstarter(实例化并set给context),lifecyclelistener,contextvalue,errorpage,mime,session超时持久化等以及一些自定义工作:    

 tomcatstarter starter = new tomcatstarter(initializers);
 if (context instanceof tomcatembeddedcontext) {
  // should be true
  ((tomcatembeddedcontext) context).setstarter(starter);
 }
 context.addservletcontainerinitializer(starter, no_classes);
 for (lifecyclelistener lifecyclelistener : this.contextlifecyclelisteners) {
  context.addlifecyclelistener(lifecyclelistener);
 }
 for (valve valve : this.contextvalves) {
  context.getpipeline().addvalve(valve);
 }
 for (errorpage errorpage : geterrorpages()) {
  new tomcaterrorpage(errorpage).addtocontext(context);
 }
 for (mimemappings.mapping mapping : getmimemappings()) {
  context.addmimemapping(mapping.getextension(), mapping.getmimetype());
 }

Spring Boot启动过程(四)之Spring Boot内嵌Tomcat启动

  session如果不需要持久化会注册一个disablepersistsessionlistener。其他定制化操作是通过tomcatcontextcustomizer的实现类实现的:

Spring Boot启动过程(四)之Spring Boot内嵌Tomcat启动

  context配置完了作为child add给host,add时给context注册了个内存泄漏跟踪的监听memoryleaktrackinglistener。postprocesscontext(context)方法是空的,留给子类重写用的。

   getembeddedservletcontainer方法的最后一行:return gettomcatembeddedservletcontainer(tomcat)。

protected tomcatembeddedservletcontainer gettomcatembeddedservletcontainer(
  tomcat tomcat) {
 return new tomcatembeddedservletcontainer(tomcat, getport() >= 0);
 }

   tomcatembeddedservletcontainer的构造函数:

 public tomcatembeddedservletcontainer(tomcat tomcat, boolean autostart) {
 assert.notnull(tomcat, "tomcat server must not be null");
 this.tomcat = tomcat;
 this.autostart = autostart;
 initialize();
 }

  initialize的第一个方法addinstanceidtoenginename对全局原子变量containercounter+1,由于初始值是-1,所以addinstanceidtoenginename方法内后续的获取引擎并设置名字的逻辑没有执行:

 private void addinstanceidtoenginename() {
 int instanceid = containercounter.incrementandget();
 if (instanceid > 0) {
  engine engine = this.tomcat.getengine();
  engine.setname(engine.getname() + "-" + instanceid);
 }
 }

   initialize的第二个方法removeserviceconnectors,将上面new的connection以service(这里是standardservice[tomcat])做key保存到private final map<service, connector[]> serviceconnectors中,并将standardservice中的protected connector[] connectors与service解绑(connector.setservice((service)null);),解绑后下面利用lifecyclebase启动容器就不会启动到connector了。

  之后是this.tomcat.start(),这段比较复杂,我单独总结一篇吧。

  tomcatembeddedservletcontainer的初始化,接下来是rethrowdeferredstartupexceptions,这个方法检查初始化过程中的异常,如果有直接在主线程抛出,检查方法是tomcatstarter中的private volatile exception startupexception,这个值是在context启动过程中记录的:

Spring Boot启动过程(四)之Spring Boot内嵌Tomcat启动

 @override
 public void onstartup(set<class<?>> classes, servletcontext servletcontext)
  throws servletexception {
 try {
  for (servletcontextinitializer initializer : this.initializers) {
  initializer.onstartup(servletcontext);
  }
 }
 catch (exception ex) {
  this.startupexception = ex;
  // prevent tomcat from logging and re-throwing when we know we can
  // deal with it in the main thread, but log for information here.
  if (logger.iserrorenabled()) {
  logger.error("error starting tomcat context. exception: "
   + ex.getclass().getname() + ". message: " + ex.getmessage());
  }
 }
 }

  context context = findcontext():

 private context findcontext() {
 for (container child : this.tomcat.gethost().findchildren()) {
  if (child instanceof context) {
  return (context) child;
  }
 }
 throw new illegalstateexception("the host does not contain a context");
 }

  绑定命名的上下文和classloader,不成功也无所谓:          

 try {
   contextbindings.bindclassloader(context, getnamingtoken(context),
    getclass().getclassloader());
  }
  catch (namingexception ex) {
   // naming is not enabled. continue
  }

  startdaemonawaitthread方法的注释是:与jetty不同,tomcat所有的线程都是守护线程,所以创建一个非守护线程(例:thread[container-0,5,main])来避免服务到这就shutdown了:

 private void startdaemonawaitthread() {
 thread awaitthread = new thread("container-" + (containercounter.get())) {
  @override
  public void run() {
  tomcatembeddedservletcontainer.this.tomcat.getserver().await();
  }
 };
 awaitthread.setcontextclassloader(getclass().getclassloader());
 awaitthread.setdaemon(false);
 awaitthread.start();
 }

  这个await每10秒检查一次是否关闭了:        

 try {
  awaitthread = thread.currentthread();
  while(!stopawait) {
   try {
   thread.sleep( 10000 );
   } catch( interruptedexception ex ) {
   // continue and check the flag
   }
  }
  } finally {
  awaitthread = null;
  }
  return;

  回到embeddedwebapplicationcontext,initpropertysources方法,用初始化好的servletcontext完善环境变量:

 /**
 * {@inheritdoc}
 * <p>replace {@code servlet}-related property sources.
 */
 @override
 protected void initpropertysources() {
 configurableenvironment env = getenvironment();
 if (env instanceof configurablewebenvironment) {
  ((configurablewebenvironment) env).initpropertysources(this.servletcontext, null);
 }
 }

  createembeddedservletcontainer就结束了,内嵌容器的启动过程至此结束。

==========================================================

咱最近用的github:https://github.com/saaavsaaa

原文链接:http://www.cnblogs.com/saaav/p/6323350.html

相关文章

热门资讯

2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
Intellij idea2020永久破解,亲测可用!!!
Intellij idea2020永久破解,亲测可用!!! 2020-07-29
歪歪漫画vip账号共享2020_yy漫画免费账号密码共享
歪歪漫画vip账号共享2020_yy漫画免费账号密码共享 2020-04-07
电视剧《琉璃》全集在线观看 琉璃美人煞1-59集免费观看地址
电视剧《琉璃》全集在线观看 琉璃美人煞1-59集免费观看地址 2020-08-12
最新idea2020注册码永久激活(激活到2100年)
最新idea2020注册码永久激活(激活到2100年) 2020-07-29
返回顶部