首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐
2025年7月23日 星期三 2:10am

Tomcat源码解析(五):StandardEngine、StandardHost、StandardContext、StandardWrapper

  • 25-03-03 07:42
  • 3829
  • 13276
blog.csdn.net

Tomcat源码系列文章

Tomcat源码解析(一):Tomcat整体架构

Tomcat源码解析(二):Bootstrap和Catalina

Tomcat源码解析(三):LifeCycle生命周期管理

Tomcat源码解析(四):StandardServer和StandardService

Tomcat源码解析(五):StandardEngine、StandardHost、StandardContext、StandardWrapper


文章目录

  • 前言
  • 一、Container容器
    • 1、容器基本组成及关系
    • 2、Container接口
  • 二、StandardEngine
    • 1、解析server.xml
    • 2、解析\标签
    • 3、initInternal初始化
    • 4、startInternal启动
    • 5、stopInternal停止
    • 6、destroyInternal销毁
  • 三、StandardHost
    • 1、解析server.xml
    • 2、解析\标签
    • 3、initInternal初始化
    • 4、startInternal启动
    • 5、停止和销毁方法
    • 6、监听器HostConfig
      • 6.1、监听器触发时机
      • 6.2、Web应用部署
      • 6.2、Context的实例化
  • 四、StandardContext
    • 1、initInternal初始化
    • 2、startInternal启动
      • 2.1、监听器ContextConfig
      • 2.2、ServletContext的实现类
      • 2.3、实例化ServletContextListener监听器并执行
      • 2.4、实例化Filter并初始化
      • 2.5、实例化loadOnStartup>0的Servlet并初始化
    • 3、stopInternal停止
    • 4、destroyInternal销毁
  • 五、StandardWrapper
    • 1、创建Servlet
    • 2、ServletConfig
  • 六、Mapper组件
  • 总结


前言

  前文中我们介绍了StandServer与StandService的init与start方法,而Service的init方法和start方法则是调用顶级容器Engine、请求url映射Mapper、连接器Connector的init和start方法。本文就介绍下容器的init和start及Mapper的组成。

容器接口与实现类的类图如下:

在这里插入图片描述


一、Container容器

1、容器基本组成及关系

  • Container的子容器Engine、Host、Context、Wrapper 是逐层包含的关系
  • Wrapper:表示一个Servlet,它负责管理Servlet的生命周期
    • Wrapper作为容器中的最底层,不能包含子容器
  • Context:表示一个Web应用程序,是Servlet、Filter的父容器
    • 一个Web应用可包含多个Wrapper
  • Host:表示一个虚拟主机,包含主机名称和IP地址,默认是localhost
    • Tomcat可以配置多个虚拟主机地址
    • 一个虚拟主机下可包含多个Context
  • Engine:表示顶级容器,接受连接器的所有请求,并将响应返回给连接器
    • 一个Service最多只能有一个Engine,但是一个Engine可包含多个Host

在这里插入图片描述

Context和Host的区别

  Context表示一个应用,比如,默认配置下webapps下的每个目录都是一个应用,其中ROOT目录中存放着主应用,其他目录存放着别的子应用。
  而整个webapps是一个Host(站点,有名虚拟主机)。假如www.excelib.com域名对应着webapps目录所代表的站点,其中的ROOT目录里的应用就是主应用,访问时直接使用域名就可以,而webapps/test目录存放的是test子应用,访问时需要www.excelib.com/test,每一个应用对应一个Context ,所有webapps下的应用都属于www.excelib.com站点,而blog.excelib.com则是另外一个站点,属于另外一个Host。

2、Container接口

  • Container的实现类Engine、Host、Context、Wrapper都具有父子关系(注意不是继承)
  • Container提供了父节点的设置、查找以及子节点的添加、删除、查找
  • 由于一个父节点可以有多个子节点因此返回的是数组
public interface Container extends Lifecycle {
	public Container getParent();
	public void setParent(Container container);
	public void addChild(Container child);
	public Container[] findChildren();
	public void removeChild(Container child);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

二、StandardEngine

  Engine的实现类为StandardEngine,在前文中我们了解到StandardService在init时调用了engine.init()来初始化engine,StandardEngine没有重写Init方法,但由于StandardEngine继承了ContainerBase从而间接继承了LifecycleBase抽象类,得以复用LifecycleBase中的init方法,start方法也是如此,因此以engine为代表的这些子容器实际上只需要重写initInternal、startInternal即可。

1、解析server.xml

  • Engine组件是Service组件中的请求处理组件
  • 在Service组件中有且只有一个
  • Engine组件从一个或多个Connector中接受请求并处理,并将完成的响应返回给Connector,最终传递给客户端

<Engine name="Catalina" defaultHost="localhost">
...
Engine>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2、解析标签

# Catalina#createStartDigester方法

digester.addRuleSet(new EngineRuleSet("Server/Service/"));
  • 1
  • 2
  • 3
  • 标签内容用来实例化StandardEngine组件
  • 通过Service的setContainer方法将StandardEngine对象设置到Service的engine属性中
# EngineRuleSet#addRuleInstances方法

// prefix = "Server/Service/"
/** 解析标签实例化StandardEngine对象 **/
digester.addObjectCreate(prefix + "Engine",
                         "org.apache.catalina.core.StandardEngine",
                         "className");
                         
/** 解析标签将标签中属性值映射到StandardEngine对象中 **/          
digester.addSetProperties(prefix + "Engine");

/** 将StandardEngine对象通过Service的setContainer设置到Service的engine属性中 **/   
digester.addSetNext(prefix + "Engine",
                    "setContainer",
                    "org.apache.catalina.Engine");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3、initInternal初始化

  • 调用super.initInternal(),也就是ContainerBase的initInternal方法
@Override
protected void initInternal() throws LifecycleException {
    // Realm:安全认证的,不用管
    getRealm();
    super.initInternal();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • ContainerBase的initInternal方法如下
  • 主要创建了一个线程池,将会在下文用于启动子容器,这样可以用多个线程来同时启动,效率更高
protected ThreadPoolExecutor startStopExecutor;

@Override
protected void initInternal() throws LifecycleException {
	// 创建线程安全的队列
    BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
    // 创建线程池
    startStopExecutor = new ThreadPoolExecutor(
            getStartStopThreadsInternal(),
            getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
            startStopQueue,
            new StartStopThreadFactory(getName() + "-startStop-"));
    startStopExecutor.allowCoreThreadTimeOut(true);
    // 调用父类LifecycleMBeanBase的初始化方法
    super.initInternal();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4、startInternal启动

  • 和initInternal一样,直接调用了父类ContainerBase中的该方法
protected synchronized void startInternal() throws LifecycleException {
    if (log.isInfoEnabled()) {
        log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
    }
    super.startInternal();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • ContainerBase的startInternal方法如下
  • 核心内容使用线程池异步调用子容器的start方法
  • 每一个容器都有一个Pipeline对象,处理请求时候,依次经过每个容器的Pipeline,做一些处理,后面章节讲
protected synchronized void startInternal() throws LifecycleException {
     // Cluster 用于配置集群,在server.xml 中有注释的参考配置,它的作用就是同步Session
     Cluster cluster = getClusterInternal();
     if (cluster instanceof Lifecycle) {
         ((Lifecycle) cluster).start();
     }

     // Realm 是Tomcat 的安全域,可以用来管理资源的访问权限
     Realm realm = getRealmInternal();
     if (realm instanceof Lifecycle) {
         ((Lifecycle) realm).start();
     }
	
    // 将要启动的子容器加入到线程池中异步启动
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (Container child : children) {
        results.add(startStopExecutor.submit(new StartChild(child)));
    }
	
	// 省略获取子容器启动结果results,正常启动返回null,如果返回结果则证明异常,抛出错误
	...	

	// 子容器启动完成后接着启动容器的管道,管道下篇文章中详细讲解
    if (pipeline instanceof Lifecycle) {
        ((Lifecycle) pipeline).start();
    }
    
    // 设置状态为启动中,这里修改状态是为了触发监听器
    setState(LifecycleState.STARTING);
    
    // 启动后台线程,该线程将定期检查会话超时
    threadStart();
}

// 启动子容器的线程类型StartChild 是一个实现了Callable 的内部类,主要作用就是调用子容器的start 方法
private static class StartChild implements Callable<Void> {

    private Container child;

    public StartChild(Container child) {
        this.child = child;
    }

    @Override
    public Void call() throws LifecycleException {
        child.start();
        return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

5、stopInternal停止

  • StandardEngine没有重写stopInternal 方法,停止默认调用ContainerBase的stopInternal方法
// ContainerBase的stopInternal方法
@Override
protected synchronized void stopInternal() throws LifecycleException {

    // 停止定期检查会话超时的后台线程
    threadStop();

	// 设置状态为停止中
    setState(LifecycleState.STOPPING);

    // 调用容器管道的关闭方法
    if (pipeline instanceof Lifecycle &&
            ((Lifecycle) pipeline).getState().isAvailable()) {
        ((Lifecycle) pipeline).stop();
    }

    // 调用子容器的stop方法
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (int i = 0; i < children.length; i++) {
        results.add(startStopExecutor.submit(new StopChild(children[i])));
    }

	// 省略容器关闭结果处理,如果关闭中出现错误结果results,抛出异常
	...

    // 停止Realm和Cluster
    Realm realm = getRealmInternal();
    if (realm instanceof Lifecycle) {
        ((Lifecycle) realm).stop();
    }
    Cluster cluster = getClusterInternal();
    if (cluster instanceof Lifecycle) {
        ((Lifecycle) cluster).stop();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

6、destroyInternal销毁

  • StandardEngine没有重写destroyInternal方法,销毁默认调用ContainerBase的destroyInternal方法
// ContainerBase的destroyInternal方法
@Override
protected void destroyInternal() throws LifecycleException {

    Realm realm = getRealmInternal();
    if (realm instanceof Lifecycle) {
        ((Lifecycle) realm).destroy();
    }
    Cluster cluster = getClusterInternal();
    if (cluster instanceof Lifecycle) {
        ((Lifecycle) cluster).destroy();
    }

    // 调用容器管道的销毁方法
    if (pipeline instanceof Lifecycle) {
        ((Lifecycle) pipeline).destroy();
    }

    // 获取所有的子容器,调用子容器的销毁方法
    for (Container child : findChildren()) {
        removeChild(child);
    }

    // 从父容器中移除子容器,并调用当前容器的销毁方法
    if (parent != null) {
        parent.removeChild(this);
    }

    // 中断所有工作线程,并清空工作队列,拒绝新提交的任务,
    if (startStopExecutor != null) {
        startStopExecutor.shutdownNow();
    }
	
	// 父类LifecycleMBeanBase的方法,jmx内容
    super.destroyInternal();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

三、StandardHost

  Host是Engine的子容器。Engine组件中可以内嵌一个或多个Host组件,每个Host组件代表Engine中的一个虚拟主机。Host组件至少有一个,且其中一个的name必须与Engine组件的defaultHost属性相匹配。

1、解析server.xml

  • Host虚拟主机的作用,运行多个Web应用(一个Context代表一个Web应用 ),并负责安装、展开、启动和结束每个Web应用
  • Host组件代表的虚拟主机,对应了服务器中一个网络名实体(如"www.test.com"或IP地址"116.25.25.25"),为了使用户可以通过网络名连接Tomcat服务器,这个名字应该在DNS服务器上注册
  • Tomcat从HTTP头中提取主机名,寻找名称匹配的主机。如果没有匹配,请求将转发至默认主机(不需要再DNS服务器中注册)

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
...
Host>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2、解析标签

# Catalina#createStartDigester方法

digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
  • 1
  • 2
  • 3
  • 标签内容用来实例化StandardHost组件
  • 通过Engine的addChild方法将StandardHost对象设置到StandardEngine的父类ContainerBase的children的map集合属性中
  • 将HostConfig监听器添加到Host的父类LifecycleBase的监听器集合lifecycleListeners中,很重要下面讲
# HostRuleSet#addRuleInstances方法

// prefix = "Server/Service/Engine/"
/** 解析标签实例化StandardHost对象 **/
digester.addObjectCreate(prefix + "Host",
                         "org.apache.catalina.core.StandardHost",
                         "className");

/** 将HostConfig添加到Host的父类LifecycleBase的监听器集合lifecycleListeners中 **/ 
digester.addRule(prefix + "Host",
                 new LifecycleListenerRule
                 ("org.apache.catalina.startup.HostConfig",
                  "hostConfigClass"));

/** 解析标签将标签中属性值映射到StandardHost对象中 **/ 
digester.addSetProperties(prefix + "Host");

/** 通过StandardEngine的addChild方法设置到Engine的children属性中 **/   
digester.addSetNext(prefix + "Host",
                    "addChild",
                    "org.apache.catalina.Container");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3、initInternal初始化

  StandardHost没有重写initlntemal 方法,初始化默认调用ContainerBase的initlntemal方法,ContainerBase的initlntemal方法上面说过了,就是创建了一个线程池,用于启动子容器,StandardEngine创建线程池用于启动子容器StandardHost,StandardHost也创建线程池用于启动子容器StandardContext(下面会讲)。

4、startInternal启动

  核心内容调用父类ContainerBase的startInternal方法上面说过了,就是使用线程池异步调用子容器的start方法,子容器启动完成后接着启动容器的管道。

@Override
protected synchronized void startInternal() throws LifecycleException {
	
	// 省略检查错误报告Class
	...
	
    super.startInternal();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

5、停止和销毁方法

  StandardHost与StandardEngine一样,都没有重新stopInternal和destroyInternal方法,默认调用ContainerBase的stopInternal和destroyInternal方法。

6、监听器HostConfig

6.1、监听器触发时机

  在前面章节LifeCycle生命周期管理中讲过,生命周期状态转化成功之后会触发事件,如下setStateInternal转化状态的公共方法,初始化、启动前中后都会切换状态,调用如下公共方法,触发事件实际就是调用监听器的lifecycleEvent方法。

// LifecycleBase类属性和方法

private volatile LifecycleState state = LifecycleState.NEW;。
 
private synchronized void setStateInternal(LifecycleState state,
        Object data, boolean check) throws LifecycleException {
    // 是否校验状态,一般初始化启动流程这里都是false
    if (check) {
        // state不允许为null
        if (state == null) {
        	// 抛异常
            invalidTransition("null");
            return;
        }
        if (!(state == LifecycleState.FAILED ||
                (this.state == LifecycleState.STARTING_PREP &&
                        state == LifecycleState.STARTING) ||
                (this.state == LifecycleState.STOPPING_PREP &&
                        state == LifecycleState.STOPPING) ||
                (this.state == LifecycleState.FAILED &&
                        state == LifecycleState.STOPPING))) {
            invalidTransition(state.name());
        }
    }
    // 设置状态
    this.state = state;
    // 触发事件
    String lifecycleEvent = state.getLifecycleEvent();
    if (lifecycleEvent != null) {
        fireLifecycleEvent(lifecycleEvent, data);
    }
}

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 解析标签的时候,在Host对象里添加了监听器HostConfig,下面看下HostConfig的lifecycleEvent方法
// HostConfig类方法
@Override
public void lifecycleEvent(LifecycleEvent event) {

	...

    if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
    	// 定时检查(检查资源修改以触发重新部署、热部署等)
        check();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
    	// 启动前调用,创建webapps目录
        beforeStart();
    } else if (event.getType().equals(Lifecycle.START_EVENT)) {
    	// 启动中调用
        start();
    } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
    	// 停止中调用
        stop();
    }
}

public void start() {

	...
	// true表示应发现并自动部署此主机的子Web应用
    if (host.getDeployOnStartup()) {
        deployApps();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

6.2、Web应用部署

  • Web应用的三种部署类型
    • 解析指定的部署文件(.xml文件),加载Web应用,可以进行热部署
    • 读取webapps目录下的web应用的war包
    • 读取webapps目录下的以目录形式存在的web应用
// HostConfig类方法
protected void deployApps() {
    File appBase = host.getAppBaseFile();
    File configBase = host.getConfigBaseFile();
    // 如下图,获取webapps下的文件
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // 部署XML
    deployDescriptors(configBase, configBase.list());
    // 部署WAR
    deployWARs(appBase, filteredAppPaths);
    // 部署扩展的文件夹
    deployDirectories(appBase, filteredAppPaths);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里插入图片描述

  • 部署WAR(files即webapps目录下文件)
// HostConfig类方法
protected void deployWARs(File appBase, String[] files) {
    ExecutorService es = host.getStartStopExecutor();
    List<Future<?>> results = new ArrayList<>();

    for (int i = 0; i < files.length; i++) {
		// 文件夹为META-INF或WEB-INF跳过不处理
        if (files[i].equalsIgnoreCase("META-INF")) {
            continue;
        }
        if (files[i].equalsIgnoreCase("WEB-INF")) {
            continue;
        }
        File war = new File(appBase, files[i]);
        if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
                war.isFile() && !invalidWars.contains(files[i]) ) {
			// 这里以war包文件名+/命名,如果springmvc.war,Context path = /springmvc
            ContextName cn = new ContextName(files[i], true);
			
			// 服务已部署,文件路径文件名不能包含*和?两个字符,否则跳过不处理
			...

            results.add(es.submit(new DeployWar(this, cn, war)));
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

6.2、Context的实例化

  • 反射实例化Context的实现类StandardContext,添加监听器ContextConfig
  • 将context添加到父容器host的子容器集合中
    • host容器启动时候会启动所有的子容器,但那个时候Context还没有创建
    • 修改host容器状态为启动中时候,触发监听器,创建Conext,添加到host子容器集合中
    • 此时添加到子容器集合中的这个动作就包含了host子容器Context的启动
  • path属性,我这里为/springmvc
    • 指定了访问该web应用的上下文路径,当请求到来时,tomcat根据web应用的path属性与URI的匹配程度来选择web应用处理相应请求
    • 如果一个Context元素的path属性为"",则这个Context是虚拟主机的默认web应用
    • 当请求的URI与所有path都不匹配时,使用该默认web应用来处理
    • 可通过http://localhost:8080/springmvc访问我们的自己的项目
    • 也可以http://localhost:8080/,最后的“/”表示http的根-ROOT
// HostConfig内部类DeployWar,线程启动调用deployWAR方法

protected String contextClass = "org.apache.catalina.core.StandardContext";

protected void deployWAR(ContextName cn, File war) {
	
	// 省略,只留关键代码
	...	
	// 实例化Context
	Context context = (Context) Class.forName(contextClass).getConstructor().newInstance();
	
	// 添加监听器ContextConfig
    Class<?> clazz = Class.forName(host.getConfigClass());
    LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
    context.addLifecycleListener(listener);
	
	// 设置context属性,name和path这里是/springmvc
    context.setName(cn.getName());
    context.setPath(cn.getPath());
    context.setWebappVersion(cn.getVersion());
    context.setDocBase(cn.getBaseName() + ".war");
    // 将context添加到父容器host中并启动context
    host.addChild(context);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

四、StandardContext

1、initInternal初始化

  主要内容调用父类ContainerBase的initlntemal方法,ContainerBase的initlntemal方法上面说过了,就是创建了一个线程池,用于启动子容器。

2、startInternal启动

  与之前容器不同的是,不再调用父类ContainerBase的startInternal方法,对于子类和容器通道的启动都在StandContext类的startInternal方法中。StandContext启动很复杂,涉及很多知识面,我这里只列举出一些比较重要的点。

// 
@Override
protected synchronized void startInternal() throws LifecycleException {

	...

	// ContextConfig监听器
	// 解析web.xml或注解里Servlet、Filter、Listener
	// 并创建对应的Wrapper
	fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

    // 启动Context子节点Wrapper。
    for (Container child : findChildren()) {
        if (!child.getState().isAvailable()) {
            child.start();
        }
    }

    // 启动Context的pipeline(下个章节单独将)
    if (pipeline instanceof Lifecycle) {
        ((Lifecycle) pipeline).start();
    }

    // 将Context的Web资源集合添加到ServletContext。
    if (ok) {
        getServletContext().setAttribute(Globals.RESOURCES_ATTR, getResources());
    }

    // 将Jar包扫描器添加到ServletContext。
    if (ok) {
        getServletContext().setAttribute(
                JarScanner.class.getName(), getJarScanner());
    }

    // 实例化application域监听器,并执行
    if (ok) {
        if (!listenerStart()) {
            log.error(sm.getString("standardContext.listenerFail"));
            ok = false;
        }
    }

    // 实例化FilterConfig、Filter并调用Filter.init()。
    if (ok) {
        if (!filterStart()) {
            log.error(sm.getString("standardContext.filterFail"));
            ok = false;
        }
    }

    // 对于loadOnStartup大于等于0的Wrapper,调用Wrapper.load()
    // 该方法负责实例化Servlet,并调用Servlet.init()进行初始化。
    if (ok) {
        if (!loadOnStartup(findChildren())){
            log.error(sm.getString("standardContext.servletFail"));
            ok = false;
        }
    }
	
	...

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

2.1、监听器ContextConfig

  • context启动中修改状态,触发监听器configureStart方法
// ContextConfig类方法
@Override
public void lifecycleEvent(LifecycleEvent event) {
	...
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();// Context容器启动中触发调用方法
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }
	...
}

protected synchronized void configureStart() {
	...
	// 核心方法,解析web.xml和注解
    webConfig();
    ...
}        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

解析web.xml或注解里Servlet、Filter、Listener

  • 获取/WEB-INF/web.xml的资源输入流
  • 解析web.xml中的Servlet、Filter、Listener添加到webXml中
  • 筛选/WEB-INF/classes路径下所有@WebServlet、@WebFilter、@WebListener添加到webXml中
// ContextConfig类方法
protected void webConfig() {
	...
	// xml和注解里的Servlet、Filter、Listener都会添加到这里
    WebXml webXml = createWebXml();

    // 获取/WEB-INF/web.xml的资源输入流
    InputSource contextWebXml = getContextWebXmlSource();
    // 解析xml中的Servlet、Filter、Listener
    if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
        ok = false;
    }
	
	if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
	   // 筛选/WEB-INF/classes路径下所有@WebServlet、@WebFilter、@WebListener
	   processClasses(webXml, orderedFragments);
	}	
	
	// 添加wrapper为context的子容器
	configureContext(webXml);
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 递归循环/WEB-INF/classes路径下的Class,根据注解处理
// ContextConfig类方法
protected void processClass(WebXml fragment, JavaClass clazz) {
    AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
    if (annotationsEntries != null) {
        String className = clazz.getClassName();
        // 遍历类注解处理Servlet、Filter或Listener
        for (AnnotationEntry ae : annotationsEntries) {
            String type = ae.getAnnotationType();
            if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
                processAnnotationWebServlet(className, ae, fragment);
            }else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
                processAnnotationWebFilter(className, ae, fragment);
            }else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
                fragment.addListener(className);
            } else {
                // Unknown annotation - ignore
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

创建StandardWrapper并添加为StandardContext的子容器

  • 创建StandardWrapper并设置启动项和初始化参数等
  • 最后将StandardWrapper添加到context的子容器集合中,并进行初始化和启动
// ContextConfig类方法
private void configureContext(WebXml webxml) {
    // 添加web.xml中的监听器
    for (String listener : webxml.getListeners()) {
        context.addApplicationListener(listener);
    }
    // 其余代码
    for (ServletDef servlet : webxml.getServlets().values()) {
        // 获取web.xml中配置的servlet
        Wrapper wrapper = context.createWrapper();
        // 启动项设置
        if (servlet.getLoadOnStartup() != null) {
            wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
        }
        wrapper.setName(servlet.getServletName());
        // 配置servlet初始化参数
        Map<String,String> params = servlet.getParameterMap();
        for (Entry<String, String> entry : params.entrySet()) {
            wrapper.addInitParameter(entry.getKey(), entry.getValue());
        }
        // 设置Servlet的权限定类名,以后会通过它反射获取空的Servelt对象
        wrapper.setServletClass(servlet.getServletClass());
        // 添加为context的子容器
        context.addChild(wrapper);
    }
    for (Entry<String, String> entry :
            webxml.getServletMappings().entrySet()) {
        context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }
    // 其余代码
}

// StandardContext类方法
@Override
public Wrapper createWrapper() {

    Wrapper wrapper = null;
    if (wrapperClass != null) {
        try {
            wrapper = (Wrapper) wrapperClass.getConstructor().newInstance();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error("createWrapper", t);
            return null;
        }
    } else {
        wrapper = new StandardWrapper();
    }
    ...
    return wrapper;
}
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

2.2、ServletContext的实现类

  • 根据ServletContext创建上下文ApplicationContext对象
// StandardContext类方法
@Override
public ServletContext getServletContext() {

    if (context == null) {
        context = new ApplicationContext(this);
        if (altDDName != null)
            context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
    }
    return (context.getFacade());

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 所以获取ServletContext实际是获取实现类ApplicationContextFacade
  • 门面模式,对外不暴露ApplicationContext,在ApplicationContextFacade里设置对外提供的方法属性,实际调用的ApplicationContext的方法属性
// ApplicationContext类方法
private final ServletContext facade = new ApplicationContextFacade(this);
protected ServletContext getFacade() {
	return (this.facade);
}

// ApplicationContextFacade类
public class ApplicationContextFacade implements
	 org.apache.catalina.servlet4preview.ServletContext {
    private final ApplicationContext context;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2.3、实例化ServletContextListener监听器并执行

public boolean listenerStart() {
    // 其余代码
    // 遍历所有监听器找出ServletContextListener类别的
    for (Object instance : instances) {
        if (!(instance instanceof ServletContextListener)) {
            continue;
        }
        ServletContextListener listener = (ServletContextListener) instance;
        try {
            fireContainerEvent("beforeContextInitialized", listener);
            // 调用其contextInitialized方法
            if (noPluggabilityListeners.contains(listener)) {
                listener.contextInitialized(tldEvent);
            } else {
                listener.contextInitialized(event);
            }
            fireContainerEvent("afterContextInitialized", listener);
        } 
        // 其余代码
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 实现ServletContextListener接口,并添加@WebListener注解,就是一个监听器
public interface ServletContextListener extends EventListener {
	// ServletContext创建时调用(context启动时)
    public void contextInitialized(ServletContextEvent sce);
	// ServletContext销毁时调用
    public void contextDestroyed(ServletContextEvent sce);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

2.4、实例化Filter并初始化

  • 获取xml和注解中解析出来的Filter的map集合遍历
  • 实例化和初始化Filter
  • 将Filter添加到filterConfigs中,以后接受对应Servlet请求时会遍历调用
// StandardContext类方法
private HashMap<String, FilterDef> filterDefs = new HashMap<>();

private HashMap<String, ApplicationFilterConfig> filterConfigs =
        new HashMap<>();
public boolean filterStart() {
	...
	for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
	    String name = entry.getKey();
	
	    try {
	        ApplicationFilterConfig filterConfig =
	                new ApplicationFilterConfig(this, entry.getValue());
	        filterConfigs.put(name, filterConfig);
	    } catch (Throwable t) {
	...
	    }
	}
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
// ApplicationFilterConfig类
ApplicationFilterConfig(Context context, FilterDef filterDef)
        throws xxxException {

    super();

    this.context = context;
    this.filterDef = filterDef;
    // Allocate a new filter instance if necessary
    if (filterDef.getFilter() == null) {
        getFilter();
    } else {
        this.filter = filterDef.getFilter();
        getInstanceManager().newInstance(filter);
        initFilter();
    }
}

Filter getFilter() throws xxxException {

	...

    // 实例化Filter
    String filterClass = filterDef.getFilterClass();
    this.filter = (Filter) getInstanceManager().newInstance(filterClass);
	
	// 调用Filter的init方法
    initFilter();

    return (this.filter);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

2.5、实例化loadOnStartup>0的Servlet并初始化

  在web.xml中我们对于需要启动时加载的servlet会配置1,这个功能的实现也是在StandardContext#startInternal中完成的。

public boolean loadOnStartup(Container children[]) {
    // 获取所有loadOnStartup>0的Wrapper放入数字为key的map,这样数字越小越先被实例化
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup < 0) {
            continue;
        }
        Integer key = Integer.valueOf(loadOnStartup);
        ArrayList<Wrapper> list = map.get(key);
        if (list == null) {
            list = new ArrayList<>();
            map.put(key, list);
        }
        list.add(wrapper);
    }

    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
            } 
        }
    }
    return true;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 获取所有loadOnStartup>=0的Wrapper放入数字为key的map,这样数字越小越先被实例化
  • 具体的实现是由对应wrapper的load方法完成的,下面介绍StandardWrapper再进入

3、stopInternal停止

  • 调用filter的销毁方法destroy
  • 调用监听器的销毁方法contextDestroyed
@Override
protected synchronized void stopInternal() throws LifecycleException {
	// 给正在进行的异步请求一个完成的机会
	long limit = System.currentTimeMillis() + unloadDelay;
	while (inProgressAsyncCount.get() > 0 && System.currentTimeMillis() < limit) {
	    try {
	        Thread.sleep(50);
	    } catch (InterruptedException e) {
	        log.info(sm.getString("standardContext.stop.asyncWaitInterrupted"), e);
	        break;
	    }
	}
	
	// 将状态设置为 STOPPING 后,上下文将报告自身不可用,并且任何正在进行的异步请求都将超时
	setState(LifecycleState.STOPPING);

	// 调用子容器的stop方法
    final Container[] children = findChildren();
    for (int i = 0; i < children.length; i++) {
        children[i].stop();
    }

    // 调用filter的销毁方法destroy
    filterStop();

	// 调用监听器的销毁方法contextDestroyed
    listenerStop();
	
	...
}
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

4、destroyInternal销毁

  核心内容调用父类ContainerBase的destroyInternal方法。

五、StandardWrapper

  StandardWrapper是在StandardContext的监听器ContextConfig里解析web.xml或@WebServlet注解类内容创建而来。最后再根据StandardWrapper内容创建Servlet。
  StandardWrapper没有重写initInternal方法,而startInternal方法核心内容也是调用父类方法,接着上面实例化loadOnStartup>0的Servlet并初始化内容,查看load()方法。

// StandardWrapper类方法
public synchronized void load() throws ServletException {
    // 创建当前Servlet对象,并初始化
    instance = loadServlet();
    // 其余代码
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

1、创建Servlet

  • 根据web.xml配置或@WebServlet注解获取的Servelt的权限定类名servletClass反射获取对象,并强转为Servlet类型
    • 这里说明,Servelt必须继承HttpServelt,否则强转Servelt报错
  • 调用servlet的init方法传入的参数为ServletConfig接口实现类StandardWrapperFacade
    • 与ServletContext实际是获取实现类ApplicationContextFacade一样,都采用门面模式
    • 同样使用StandardWrapperFacade类将ServletConfig接口实现类StandardWrapper包装一下
    • 这样对外只暴露ServletConfig接口的方法,具体实现类StandardWrapper的方法属性则访问不到
// StandardWrapper类方法
public synchronized Servlet loadServlet() throws ServletException {
	...
	
    Servlet servlet;
    try {
		...
        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
        	// servletClass是创建StandardWrapper时候,set进来的
        	// 反射获取Servelt对象
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (ClassCastException e) {
			...
        } 

		...
		// 调用servlet的init方法
        initServlet(servlet);
		
    } finally {
		...
    }
    return servlet;

}

// StandardWrapper类属性,创建StandardWrapper时,就赋值了此属性
protected final StandardWrapperFacade facade = new StandardWrapperFacade(this);

// 初始化方法
private synchronized void initServlet(Servlet servlet)
        throws ServletException {
    try {
        if( Globals.IS_SECURITY_ENABLED) {
			...
        } else {
        	// 传入StandardWrapperFacade调用初始化方法
            servlet.init(facade);
        }
    } catch (UnavailableException f) {
		...
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
// 门面模式,将StandardWrapper放入StandardWrapperFacade对象里
// 对外只提供ServletConfig接口相关方法
public final class StandardWrapperFacade implements ServletConfig {

    private final ServletConfig config;
    
    public StandardWrapperFacade(StandardWrapper config) {
        super();
        this.config = config;
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2、ServletConfig

  • 注解方式创建Servelt一般继承HttpServlet类,类图如下

在这里插入图片描述

  • Servlet接口中定义了init方法,GenericServlet实现init方法
  • 上面说到的servlet.init(facade);实际时调用GenericServlet抽象类的init方法,然后将StandardWrapperFacade传递进来赋值config
  • 这样看来平常使用的ServletConfig实际时StandardWrapperFacade类中的StandardWrapper
public abstract class GenericServlet implements Servlet, ServletConfig,
        java.io.Serializable {

    private transient ServletConfig config;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        // 空实现,我们自定义的Servelt可以去覆盖无参init方法
        this.init();
    }

    public void init() throws ServletException {
        // NOOP by default
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

六、Mapper组件

  说起Mapper组件需要回到StandardService的启动方法startInternal。顶级容器engine启动,使用线程池异步调用多个子容器StandardHost的start方法,host的启动,调用多个子容器StandardContext的start方法(包括监听器、过滤器、Servlet的实例化和初始化)。可以说顶级容器engine的启动所有的子容器都将启动。接下来需要组装Mapper组件(请求url和Servlet映射)。

protected final MapperListener mapperListener = new MapperListener(this);

// StandardService类方法
@Override
protected void startInternal() throws LifecycleException {
	...

    // 顶级容器engine启动
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }
	
	...
	
	// 启动mapperListener,则时组装Mapper
    mapperListener.start();

	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这里插入图片描述

  • 直接查看注册核心方法
// MapperListener类方法
private void registerContext(Context context) {

	...
    List<WrapperMappingInfo> wrappers = new ArrayList<>();

    for (Container container : context.findChildren()) {
        prepareWrapperMappingInfo(context, (Wrapper) container, wrappers);

        if(log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.registerWrapper",
                    container.getName(), contextPath, service));
        }
    }

    mapper.addContextVersion(host.getName(), host, contextPath,
            context.getWebappVersion(), context, welcomeFiles, resources,
            wrappers);

	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

  prepareWrapperMappingInfo用于准备注册到mapper下的wrapper,这儿mapper对于wrapper的支持是wrapper的包装对象WrapperMappingInfo。而一个context可能有多个wrapper,所以WrapperMappingInfo是一个list。
  简单来说就是将映射url、wrapper名字和资源只读标记等信息组合成对象添加到wrappers中。

private void prepareWrapperMappingInfo(Context context, Wrapper wrapper, List<WrapperMappingInfo> wrappers) {
    String wrapperName = wrapper.getName();
    boolean resourceOnly = context.isResourceOnlyServlet(wrapperName);
    String[] mappings = wrapper.findMappings();
    for (String mapping : mappings) {
        boolean jspWildCard = (wrapperName.equals("jsp") && mapping.endsWith("/*"));
        wrappers.add(new WrapperMappingInfo(mapping, wrapper, jspWildCard, resourceOnly));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  最后addContextVersion方法又各种转化嵌套,对于我们自己定义的Servlet的Mapper映射对象的位置如下。
在这里插入图片描述
  每个Service都有一个Mapper,如此看来,Mapper对象则记录了所有应用项目下的MappedWrapper(请求映射和Servelt对应的Wrapper),这样以后拿着请求mapping映射即可从Mapper中找到对应的Servelt。


总结

  至此,整个容器的启动过程就介绍完了,可以看到整个流程是由Server起步直到Wrapper结束。
  其中Server代表的是整个tomcat应用,Service代表的是server.xml中的service节点。而后续的Engine与Host都是service中的子节点。
  Context代表了webapps下的每个应用,子容器Wrapper表示web应用中的每个servlet。Context中的start方法中会创建当前web应用的ServletContext,实例化ServletContextListener监听器并执行,实例化Filter并调用初始化方法,实例化需要启动时加载的Servlet(loadOnStartup>0)并调用初始化方法。
  最后容器启动后,组装每个应用下请求url和Servlet映射Mapper组件,后续请求时候需要。

在这里插入图片描述

文章知识点与官方知识档案匹配,可进一步学习相关知识
Java技能树首页概览147154 人正在系统学习中
注:本文转载自blog.csdn.net的冬天vs不冷的文章"https://blog.csdn.net/qq_35512802/article/details/136582762"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top