在Tomcat架构解析设计思路参考中,我们学习了Tomcat的整体架构,如何从宏观角度设计复杂的系统,如何设计顶层-层次模块,以及模块之间的关系关系;Tomcat实现的两个核心功能:处理Socket连接,负责网络字节流和Request、Response对象的转换。加载和管理Servlets,处理特定的Request请求。所以Tomcat设计了两个核心组件,连接器(Connector)和容器(Container),连接器负责对外通信,容器负责内部处理。Tomcat整体架构本文是Tomcat系列文章的第三篇。它将向您展示Tomcat帝国是如何建立起来的?高层组件如何管理组件?连接器和容器是如何启动和管理的?Tomcat启动流程:startup.sh->catalina.shstart->java-jarorg.apache.catalina.startup.Bootstrap.main()Tomcat启动流程Bootstrap、Catalina、Server、Service、Engine分别负责什么?因你而单独对它们写一个介绍可以看出,这些启动类或组件并不处理具体的请求。它们的任务主要是管理,管理下层组件的生命周期,给下层组件分配任务,也就是将请求路由到负责工作的组件。他们就像一个公司的高层管理人员,管理着整个公司的运作,并把任务分配给专业的人。我们在设计软件系统的时候,难免会遇到需要一些管理功能的组件,所以我们可以借鉴和学习Tomcat是如何对这些组件进行抽象和管理的。所以我把他们比作Tomcat的高层,同时我也愿意不再996工作。顺便说一句,因为微信改变了推送规则,推文不再按照时间线显示。如果不想错过我的文章,请把公众号设为“星”,常点赞,评论也可以防止失联,支持鼓励我继续更新。Bootstrap在执行startup.sh脚本时,会启动一个JVM来运行Bootstrap的main方法,即Tomcat的启动类。我们先看看它的成员变量窥探核心功能:publicfinalclassBootstrap{ClassLoadercommonLoader=null;ClassLoadercatalinaLoader=null;ClassLoadersharedLoader=null;}它的主要任务是初始化Tomcat定义的类加载器,同时创建Catalina对象。Bootstrap就像一个大神,它初始化类加载器并加载所有内容。为什么要自定义各种类加载器,详见码哥的Tomcat架构设计分析类加载器部分。初始化类加载器WebAppClassLoader如果我们在Tomcat中运行两个Web应用,两个Web应用中存在同名但功能不同的Servlet,Tomcat需要同时加载和管理这两个同名的Servlet类时间要保证它们不会冲突,所以web应用程序之间的类需要隔离。Tomcat的解决方案是自定义一个类加载器WebAppClassLoader,为每个Web应用创建一个类加载器实例。我们知道一个Context容器组件对应一个Web应用,所以每个Context容器负责创建和维护一个WebAppClassLoader加载器实例。这背后的基本原理是,由不同加载器实例加载的类被认为是不同的类,即使它们具有相同的类名。Tomcat的自定义类加载器WebAppClassLoader打破了双亲委派机制。它首先尝试自己加载一个类。如果找不到,则通过ExtClassLoader加载JRE核心类,防止黑客攻击。如果无法加载,则委托给AppClassLoader加载器。Web应用程序定义的类首先加载。具体实现是重写ClassLoader的两个方法:findClass和loadClass。SharedClassLoader如果两个Web应用程序依赖同一个第三方JAR包,比如Spring,当SpringJAR包加载到内存后,Tomcat必须保证两个Web应用程序可以共享,即SpringJAR包仅由Loaditonce使用。SharedClassLoader是Web应用共享类库的加载器,它专门加载Web应用共享的类。如果WebAppClassLoader本身不加载某个类,它会委托父加载器SharedClassLoader来加载这个类。SharedClassLoader会加载指定目录下的共享类,然后返回给WebAppClassLoader,这样共享问题就解决了。CatalinaClassloader是如何隔离Tomcat本身的类和Web应用的类的呢?共享,可以用亲子关系,隔离,则需要兄弟关系。兄弟关系意味着两个类加载器是并行的,它们可能有相同的父加载器。Tomcat基于此设计了一个类加载器CatalinaClassloader,用来加载Tomcat自己的类。这个设计有问题。Tomcat和各种Web应用程序之间需要共享一些类怎么办?老办法是再增加一个CommonClassLoader作为CatalinaClassloader和SharedClassLoader的父加载器。CommonClassLoader可以加载的类可以被CatalinaClassLoader和SharedClassLoader使用。CatalinaTomcat是一家公司,而Catalina就像一个创始人。因为它负责组队,创建Server和所有子组件。Catalina的主要任务是创建服务器,解析server.xml创建其中配置的各个组件,调用各个组件的init和start方法启动整个Tomcat,从而使整个公司正常运行。我们可以根据Tomcat配置文件直观感受://顶层组件,可以包含多个Service,代表一个Tomcat实例//顶级组件,包含一个引擎,多个连接器//Connector//Container组件:一个Engine处理Service的所有请求,包括多个Host//Containercomponent:处理指定Host下的client请求,可以包含多个Context//Containercomponent:处理特定ContextWeb的所有client请求application作为创始人,Catalina还需要处理各种公司异常情况,如有人抢公章(执行Ctrl+C关闭Tomcat)。Tomcat是如何清理资源的?通过向JVM注册一个“shutdownhook”,具体关键逻辑参见org.apache.catalina.startup.Catalina#start源码:如果Server不存在,则解析server.xml创建;创建失败会报错;启动服务器;创建并注册一个“关闭挂钩”;await方法监听停止请求。/***Startanewserverinstance.*/publicvoidstart(){//如果Catalina持有的Server为空,解析server.xml创建if(getServer()==null){load();}if(getServer()==null){log.fatal("Cannotstartserver.Serverinstanceisnotconfigured.");return;}//Startthenewservertry{getServer().start();}catch(LifecycleExceptione){//省略部分代码}//创建一个hook并注册if(useShutdownHook){if(shutdownHook==null){shutdownHook=newCatalinaShutdownHook();}Runtime.getRuntime().addShutdownHook(shutdownHook);}//监听停止请求,内部调用Server的stopif(await){await();stop();}}当我们需要在JVM关闭时做一些清理工作,比如将缓存的数据刷新到磁盘或者清理一些文件,我们可以向JVM注册一个“shutdownhook”。它实际上是一个线程试图在JVM停止之前执行这个线程的run方法。org.apache.catalina.startup.Catalina.CatalinaShutdownHookprotectedclassCatalinaShutdownHookextendsThread{@Overridepublicvoidrun(){try{if(getServer()!=null){Catalina.this.stop();}}catch(Throwableex){//省略部分代码。...}}}其实是执行了Catalina的stop方法,通过这个方法停止了整个Tomcat。ServerServer组件的职责是管理Service组件,调用持有的Service的start方法。他就像公司的CEO,负责管理多个业务单元,每个业务单元就是一个Service。它管理两个部门:ConnectorConnector:外部营销部,推广吹牛和写PPT。货柜货柜:研发部,996无性。实现类是org.apache.catalina.core.StandardServer。Server继承了org.apache.catalina.util.LifecycleMBeanBase,所以它的生命周期也是统一管理的。Server的子组件是Service,所以也需要对Service的生命周期进行管理。也就是说,在Server启动和关闭的时候,Service的start和stop方法会先被调用。这就是设计思路,将Lifecycle接口抽象出来,体现接口隔离的原则,将生命周期的相关功能凝聚起来。下面看看Server是如何管理Service的。核心源码如下org.apache.catalina.core.StandardServer#addService:publicvoidaddService(Serviceservice){service.setServer(this);synchronized(servicesLock){//创建一个数组Serviceresults,长度+1[]=newService[services.length+1];//复制旧数据到新数组System.arraycopy(services,0,results,0,services.length);results[services.length]=service;services=results;//启动服务组件if(getState().isAvailable()){try{service.start();}catch(LifecycleExceptione){//Ignore}}//发送事件support.firePropertyChange("service",null,service);}}在添加Service的过程中动态扩充数组长度,以节省内存。另外,Server组件的一个重要任务就是启动一个Socket来监听stop端口,这也是为什么可以通过shutdown命令来关闭Tomcat的原因。不知道大家有没有注意到,上面Caralina的启动方法最后一行代码调用了Server的await方法。在await方法中,会创建一个Socket监听8005端口,无限循环地接收Socket上的连接请求。如果有新的连接到来,则建立连接,然后从Socket中读取数据;如果读取数据是停止命令“SHUTDOWN”退出循环,进入停止过程。Service的职责是管理Connector连接器和顶层容器Engine,分别调用它们的启动方法。至此,整个Tomcat启动完毕。Service是业务部门的代言人,管理两个职能部门:外部推广部门(connector)和内部研发部门(container)。Service组件的实现类是org.apache.catalina.core.StandardService,直接看关键成员变量。publicclassStandardServiceextendsLifecycleMBeanBaseimplementsService{//NameprivateStringname=null;//Server实例privateServerserver=null;//Connector数组protectedConnectorconnectors[]=newConnector[0];privatefinalObjectconnectorsLock=newObject();//对应Engine容器privateEngineengine=null;//Mapper及其对应侦听器protectedfinalMappermapper=newMapper();protectedfinalMapperListenermapperListener=newMapperListener(this);}继承LifecycleMBeanBase,LifecycleMBeanBase继承LifecycleBase,这里其实就是模板方法模式的使用,org.apache.catalina.util.LifecycleBase#init,org.apache.catalina.util.LifecycleBase#start,org.apache.catalina.util.LifecycleBase#stop是对应的模板方法,内部定义了整个算法流程,子类实现自己内部具体变化会变化和不变的抽象,实现开闭的设计思想原则。那为什么会有MapperListener呢?这是因为Tomcat支持热部署。当Web应用的部署发生变化时,Mapper中的映射信息也随之发生变化。MapperListener是一个监听器,监听容器中的变化并更新信息。对于Mapper来说,这是一个典型的观察者模式。作为“管理”角色的组件,最重要的是维护其他组件的生命周期。另外,在启动各个组件的时候,要注意它们的依赖关系,也就是要注意启动的顺序。我们看一下Service的启动方法:protectedvoidstartInternal()throwsLifecycleException{//1。触发启动监听器setState(LifecycleState.STARTING);//2.先启动Engine,Engine会启动它的子容器if(engine!=null){synchronized(engine){engine.start();}}//3.启动Mapper监听器mapperListener.start();//4.最后启动connector,connector会启动它的子组件,比如Endpointsynchronized(connectorsLock){for(Connectorconnector:connectors){if(connector.getState()!=LifecycleState.FAILED){connector.start();}}}}这里的启动顺序也很有讲究。Service先启动Engine组件,再启动Mapper监听连接器,最后才是启动连接器。这个很好理解,因为内层组件只有在激活后才能对外提供服务,产品做完之前市场部是不能瞎搞的。Mapper也依赖于容器组件,只有当容器组件启动时,才能监听到它们的变化。所以Mapper和MapperListener是在容器组件之后启动的。组件停止的顺序与它们启动的顺序相反,同样基于它们的依赖关系。Engine是研发部门的负责人,是顶层容器组件。继承自Container,所有的容器组件都继承自Container,这里其实就是采用组合方式统一管理。它的实现类是org.apache.catalina.core.StandardEngine,继承自ContainerBase。publicclassStandardEngineextendsContainerBaseimplementsEngine{}它的子容器是Host,所以它持有一个Host容器数组,这个属性会存在于每一个容器中,所以放在抽象类protectedfinalHashMapchildren=newHashMap<>();ContainerBase是用HashMap保存了它的子容器,而ContainerBase也实现了子容器的“增删改查”,甚至还提供了子组件启停的默认实现。例如,ContainerBase会使用专用的线程池来启动子容器。org.apache.catalina.core.ContainerBase#startInternal//Startourchildcontainers,ifanyContainerchildren[]=findChildren();List>results=newArrayList<>();for(Containerchild:children){results.add(startStopExecutor.submit(newStartChild(child)));}Engine在启动Host子容器时直接复用这个方法。容器组件最重要的功能就是处理请求,而Engine容器对请求的“处理”实际上就是将请求转发给某个Host子容器进行处理,具体是通过Valve。每个容器组件都有一个Pipeline,Pipeline中有一个BasicValve,通过构造方法创建Pipeline。publicStandardEngine(){super();pipeline.setBasic(newStandardEngineValve());//省略部分代码}Engine容器的基本valve定义如下:/获取请求Host容器Hosthost=request.getHost();if(host==null){return;}//在Pipeline中调用第一个Valvehost.getPipeline().getFirst().invoke(request,response)Host容器;}}这个基本valve的实现很简单,就是把请求转发给Host容器。从代码中可以看出,处理请求的Host容器对象是从请求中获取的。请求对象中怎么会有Host容器呢?这是因为Mapper组件在请求到达Engine容器之前已经路由了请求,Mapper组件通过请求的URL定位到对应的容器,并将容器对象保存到请求对象中。本文转载自微信公众号“码哥字节”,可通过以下二维码关注。转载本文请联系字节码哥公众号。