SpringBoot集成Tomcat

SpringBoot集成Tomcat

一、零配置原理

基于Spring新特性javaConfig,Spring JavaConfig是Spring社区的产品,使用java代码配置Spring IoC容器,不需要使用XML配置。

JavaConfig的优点:

  • 面向对象的配置。配置被定义为JavaConfig类,因此用户可以充分利用Java中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean方法等。
  • 减少或消除XML配置。许多开发人员不希望在XML和Java之间来回切换。JavaConfig为开发人员提供了一种纯Java方法来配置与XML配置概念相似的Spring容器。
  • 类型安全和重构友好。JavaConfig提供了一种类型安全的方法来配置Spring容器。由于Java 5.0对泛型的支持,现在可以按类型而不是按名称检索bean,不需要任何强制转换或基于字符串的查找

二、SpringMVC阶段配置Tomcat

通常都是在maven中创建web环境的项目,然后在web.xml中补充如下配置:

  • ContextLoaderListener:监听servlet启动、销毁,初始化ioc完成依赖注入
  • DispatcherServlet:接收tomcat解析之后的http请求,匹配controller处理业务

在Springmvc阶段中,通常是如下配置:

AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
ac.register(Start.class);
//ac.refresh();

DispatcherServlet servlet = new DispatcherServlet(ac);

使用注解来替代xml配置,如果使用xml的话,如下所示:

applicationContext.xml:

component-scan:扫描 注解替换: @ComponentScan

注解替换: @Configuration +@Bean

Springmvc.xml:

component-scan:扫描 只扫描@Controller 注解替换: @ComponentScan

视图解析器,json转换,国际化,编码。。。。 注解替换: @Configuration +@Bean

代码替换:

@Configuration
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer{

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter builder = new FastJsonHttpMessageConverter();
        converters.add(builder);
    }
}

web.xml

配置字符编码过滤器和DispatherServlet等

三、内嵌Tomcat原理

3.1、SpringBoot中为什么不需要web.xml

Tomcat启动通过SPI机制,找到ServletContainerInitializer对应的实现类,调用其中的onStartup方法。

在onStartup方法中,创建DispatcherServlet对象,然后onStartup方法传入的有ServletContext,说明可以在ServletContext中添加DispatcherServlet对象。

所以web.xml就已经失去了作用,因为之前在web.xml中配置的就是监听器和DispatcherServlet。所以web.xml失去了作用。

3.2、Tomcat的处理之WebApplicationInitializer如何被调用的

将官网上的一段代码拷贝下来:

public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) {
        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);
        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

可以看到自定义类实现了WebApplicationInitializer接口并重写其中的onStartup方法。

首先这个方法和Tomcat中的ServletContainerInitializer长得特别像,而且类中都有onStartup方法。这个时候应该想到在omcat中的ServletContainerInitializer的一个实现类上看到过WebApplicationInitializer

@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        // .........
        // .........        
    }
}

可以看到SpringServletContainerInitializer类上有一个注解,注解中的值是WebApplicationInitializer,那么对于Tomcat来说又是如何将WebApplicationInitializer进行处理的呢?这个时候还是得去看下Tomcat中的处理方式:

那么需要分析一下ServletContainerInitializer的onStartup是如何被加载的:

因为SpringServletContainerInitializer遵守了Tomcat的SPI机制,所以Tomcat会来找到META-INF\services\javax.servlet.ServletContainerInitializer这个类进行加载,在对应的时机进行调用。

那么直到这里,我们可以从onStartup方法上看到参数WebApplicationInitializer。那么现在的问题就是Set集合从哪里传递过来的?

那么从Tomcat中的SPI哪个地方来看呢?直接来看ContextConfig监听器部分来:org.apache.catalina.startup.ContextConfig#lifecycleEvent,然后来到org.apache.catalina.startup.ContextConfig#processServletContainerInitializers中进行处理:

/**
* Scan JARs for ServletContainerInitializer implementations.
*/
protected void processServletContainerInitializers() {

    List<ServletContainerInitializer> detectedScis;
    try {
        /** 
        * 利用Tomcat的SPI机制找到ServletContainerInitializer接口实现类
        */
        WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
        detectedScis = loader.load(ServletContainerInitializer.class);
    } catch (IOException e) {
        // log print
        ok = false;
        return;
    }
    // 遍历
    for (ServletContainerInitializer sci : detectedScis) {
        initializerClassMap.put(sci, new HashSet<Class<?>>());
        // 看到HandlesTypes注解
        HandlesTypes ht;
        try {
            // 找到上面有@HandlesTypes注解的值
            ht = sci.getClass().getAnnotation(HandlesTypes.class);
        } catch (Exception e) {
            // log print
            continue;
        }
        if (ht == null) {
            continue;
        }
        // 获取得到type的值:WebApplicationInitializer.class
        Class<?>[] types = ht.value();
        if (types == null) {
            continue;
        }
        // 开始进行遍历
        for (Class<?> type : types) {
            if (type.isAnnotation()) {
                handlesTypesAnnotations = true;
            } else {
                handlesTypesNonAnnotations = true;
            }
            // 熟悉的Set集合
            Set<ServletContainerInitializer> scis =
                typeInitializerMap.get(type);
            if (scis == null) {
                scis = new HashSet<>();
                // 将value对应的值作为key添加到集合中来
                typeInitializerMap.put(type, scis);
            }
            scis.add(sci);
        }
    }
}

从这里看,WebApplicationInitializer.class会被放入到typeInitializerMap集合中来,而typeInitializerMap是当前类的成员属性

看看成员属性:org.apache.catalina.startup.ContextConfig#typeInitializerMap

Map<Class<?>, Set<ServletContainerInitializer>> typeInitializerMap = new HashMap<>();

注意:在这个Map中会存在KEY(Class)是WebApplicationInitializer.class的类

然后会将typeInitializerMap中的KEY和VALUE保存到org.apache.catalina.core.StandardContext#initializers中来

Map<ServletContainerInitializer,Set<Class<?>>> initializers =new LinkedHashMap<>();

具体转换的代码如下所示:org.apache.catalina.startup.ContextConfig#webConfig

// Step 11. Apply the ServletContainerInitializer config to the
// context
if (ok) {
  // 转换过程
  for (Map.Entry<ServletContainerInitializer,Set<Class<?>>> entry :initializerClassMap.entrySet()) {
    if (entry.getValue().isEmpty()) {
      context.addServletContainerInitializer(entry.getKey(), null);
    } else {
      context.addServletContainerInitializer(entry.getKey(), entry.getValue());
    }
  }
}
//================================================
//================================================
public void addServletContainerInitializer(
  ServletContainerInitializer sci, Set<Class<?>> classes) {
  initializers.put(sci, classes);
}

然后会在org.apache.catalina.core.StandardContext#startInternal中来遍历initializers集合:

// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :initializers.entrySet()) {
  try {
    // 直接调用对应的方法。然后这里的值就会来进行传入
    entry.getKey().onStartup(entry.getValue(),getServletContext());
  } catch (ServletException e) {
    log.error(sm.getString("standardContext.sciFail"), e);
    ok = false;
    break;
  }
}

至此,会来调用WebApplicationInitializer的onStartup方法并将参数传入了进去。

3.3、如何内嵌Tomcat

依然是从官方代码开始进行入手:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

代码中无非是从两个地方开始来进行入手:一个是@SpringBootApplication注解,另外一个是SpringApplication中的run方法。

下面首先来从SpringApplication中的run方法来进行入手分析,直接来到org.Springframework.boot.SpringApplication#run(java.lang.String...)中

public ConfigurableApplicationContext run(String... args) {
	// ......
  try {
    // ......
    // 根据当前条件来创建容器
    // web环境创建AnnotationConfigServletWebServerApplicationContext容器
    context = this.createApplicationContext();
	// ......
    this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    // 刷新容器--->启动Tomcat
    this.refreshContext(context);
	// ......
  } catch (Throwable var10) {
	// ......
  }
}

AnnotationConfigServletWebServerApplicationContext是ServletWebServerApplicationContext类的子类,所以refreshContext方法,直接来到org.Springframework.context.support.AbstractApplicationContext#refresh方法中来:

// Initialize other special beans in specific context subclasses.
onRefresh();
//=========================================
protected void onRefresh() throws BeansException {
  // For subclasses: do nothing by default.
}

这个方法是交给子类来进行实现的,对于Spring来说,是没有任何实现的。而在上面已经创建了一个容器:AnnotationConfigServletWebServerApplicationContext是ServletWebServerApplicationContext类的子类

所以直接来到org.Springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh容器中来

protected void onRefresh() {
  // 首先访问父类的onRefresh方法
  super.onRefresh();
  try {
    // 创建WebServer
    this.createWebServer();
  } catch (Throwable var2) {
    throw new ApplicationContextException("Unable to start web server", var2);
  }
}

那么就看下createWebServer方法

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    // 利用main方法启动肯定是走这一步的
    if (webServer == null && servletContext == null) {
        ServletWebServerFactory factory = getWebServerFactory();
        this.webServer = factory.getWebServer(getSelfInitializer());
    }
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}

这里为什么要判断一下webServer是否为空 || 是否不为空呢?因为Tomcat支持的web容器有:Tomcat、Jetty和Undertow。

在SpringBoot中有好几种启动方式:

  • 1、jar包(main方法);
  • 2、war包(丢到Tomcat的webapp目录下,这个时候ServletContext才是有值的)。

这里就是if....else...判断存在的意义。

分析一行代码:从IOC容器中找到ServletWebServerFactory对应的bean容器

ServletWebServerFactory factory = getWebServerFactory();
// ==================================================
protected ServletWebServerFactory getWebServerFactory() {
  // Use bean names so that we don't consider the hierarchy
  String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
  if (beanNames.length == 0) {
    throw new ApplicationContextException("yyyy");
  }
  if (beanNames.length > 1) {
    throw new ApplicationContextException("xxxx");
  }
  return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

那么问题来了,此时此刻,从IOC容器中找到ServletWebServerFactory对应的bean容器,找到的是哪个ServletWebServerFactory对象?而且ServletWebServerFactory容器对象是在哪里配置的呢?

首先来看下ServletWebServerFactory的实现类:

可以看到有三个实现类,那么我们默认使用的是TomcatServletWebServerFactory。

3.3.1、ServletWebServerFactory的选择

既然是从IOC容器中来找TomcatServletWebServerFactory,那么什么时候添加到IOC容器中的呢?

又是利用Spring中的SPI机制,在Spring启动流程中再来说。在Spring.factories中有对应的配置。对应的路径在META-INF\Spring.factories中

org.Springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration

在ServletWebServerFactoryAutoConfiguration类中

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})
// 如果当前是SERVLET环境,那么就是就导入三个类
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties({ServerProperties.class})
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
  // ......
}

利用@Import注解导入了EmbeddedTomcat类

static class EmbeddedTomcat {

  @Bean
  TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.getTomcatConnectorCustomizers().addAll((Collection)connectorCustomizers.orderedStream().collect(Collectors.toList()));
    factory.getTomcatContextCustomizers().addAll((Collection)contextCustomizers.orderedStream().collect(Collectors.toList()));
    factory.getTomcatProtocolHandlerCustomizers().addAll((Collection)protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
    return factory;
  }
}

当添加到容器中后,所以能够从IOC容器中获取得到对应的TomcatServletWebServerFactory对应的Bean。

3.3.2、DispatcherServlet类是如何添加到容器中的

那么来看一下Tomcat的启动代码:

public WebServer getWebServer(ServletContextInitializer... initializers) {
  if (this.disableMBeanRegistry) {
    Registry.disableRegistry();
  }
  // 创建Tomcat对象
  Tomcat tomcat = new Tomcat();
  File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
  tomcat.setBaseDir(baseDir.getAbsolutePath());
  Connector connector = new Connector(this.protocol);
  connector.setThrowOnFailure(true);
  tomcat.getService().addConnector(connector);
  customizeConnector(connector);
  tomcat.setConnector(connector);
  tomcat.getHost().setAutoDeploy(false);
  configureEngine(tomcat.getEngine());
  for (Connector additionalConnector : this.additionalTomcatConnectors) {
    tomcat.getService().addConnector(additionalConnector);
  }
  prepareContext(tomcat.getHost(), initializers);
  // 启动和阻塞
  return getTomcatWebServer(tomcat);
}
// =============================================
// =============================================
// =============================================


private void initialize() throws WebServerException {
  logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
  synchronized (this.monitor) {
    try {
      addInstanceIdToEngineName();

      Context context = findContext();
      context.addLifecycleListener((event) -> {
        if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
          // Remove service connectors so that protocol binding doesn't
          // happen when the service is started.
          removeServiceConnectors();
        }
      });

      // Start the server to trigger initialization listeners
      // 启动Tomcat
      this.tomcat.start();

      // We can re-throw failure exception directly in the main thread
      rethrowDeferredStartupExceptions();

      try {
        ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
      }
      catch (NamingException ex) {
        // Naming is not enabled. Continue
      }

      // Unlike Jetty, all Tomcat threads are daemon threads. We create a
      // blocking non-daemon to stop immediate shutdown
      // 阻塞代码
      startDaemonAwaitThread();
    }
    catch (Exception ex) {
      stopSilently();
      destroySilently();
      throw new WebServerException("Unable to start embedded Tomcat", ex);
    }
  }
}


// =============================================
// =============================================
// =============================================

private void startDaemonAwaitThread() {
  Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

    @Override
    public void run() {
      // 阻塞代码
      TomcatWebServer.this.tomcat.getServer().await();
    }

  };
  awaitThread.setContextClassLoader(getClass().getClassLoader());
  awaitThread.setDaemon(false);
  // 调用线程中的启动方法
  awaitThread.start();
}

既然Tomcat已经启动起来了,那么对应的DiaptcherServlet类又是如何来进行添加启动的呢?

this.webServer = factory.getWebServer(getSelfInitializer());

在这行代码中,有一个方法入参,那么来看一下这里的方法入参,方法入参为一个lambda表达式

private org.Springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
  return this::selfInitialize;
}
// ==================================================
private void selfInitialize(ServletContext servletContext) throws ServletException {
  prepareWebApplicationContext(servletContext);
  registerApplicationScope(servletContext);
  WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
  // 看到这里!想象到了一个相似的类:ServletContainerInitializer
  // 从IOC容器中找到接口的实现类
  for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
    beans.onStartup(servletContext);
  }
}

selfInitialize方法作为ServletContextInitializer接口中的onStartup方法的方法体,这个时候作为参数的时候还是没有执行这里的lambda表达式的。

那么看一下ServletContextInitializer类,也有onStartup方法:

@FunctionalInterface
public interface ServletContextInitializer {
	void onStartup(ServletContext servletContext) throws ServletException;
}

那么这里也是由Tomcat利用SPI机制调用的吗?在哪里ServletContextInitializer.onStartup被调用的呢?

只有lambda表达式的方法被触发的时候,ServletContextInitializer.onStartup才会被触发。

对应的执行流程如下所示:

ServletContextInitializer#Lambda--execute onStartUp-->selfInitialize方法执行---->ServletContextInitializer.onStartup

然后跟踪源码到:org.Springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer

public WebServer getWebServer(ServletContextInitializer... initializers) {
  // ......
  // 开始来准备Tomcat的上下文
  prepareContext(tomcat.getHost(), initializers);
  return getTomcatWebServer(tomcat);
}
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
  // ......
  // 然后执行到这里
  configureContext(context, initializersToUse);
  postProcessContext(context);
}

而TomcatEmbeddedContext extends StandardContext,而在StandardContext启动的时候会触发ServletContainerInitializer.onStartup方法

protected void configureContext(Context context, ServletContextInitializer[] initializers) {
  // TomcatStarter implements ServletContainerInitializer
  // 但是在META-INF/services下没有找到这个类!
  // 那么对应的onStartup又是如何启动的呢?
  TomcatStarter starter = new TomcatStarter(initializers);
  if (context instanceof TomcatEmbeddedContext) {
    TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
    embeddedContext.setStarter(starter);
    embeddedContext.setFailCtxIfServletStartFails(true);
  }
  // 重点方法
  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()) {
    org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
    tomcatErrorPage.setLocation(errorPage.getPath());
    tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
    tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
    context.addErrorPage(tomcatErrorPage);
  }
  for (MimeMappings.Mapping mapping : getMimeMappings()) {
    context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
  }
  configureSession(context);
  new DisableReferenceClearingContextCustomizer().customize(context);
  for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
    customizer.customize(context);
  }
}

因为TomcatStarter实现了ServletContainerInitializer接口,那么按照以前的思维,就是Tomcat利用SPI机制,来加载META-INF/services目录下对应的实现类进行进行加载,但是当前是没有找到对应的文件的,那么对于TomcatStarter来说,对应的onStartup方法又是如何进行调用的呢?

直接来到org.Springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup方法中:

public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
  // 触发ServletContextInitializer中的lambda方法
    for (ServletContextInitializer initializer : this.initializers) {
      initializer.onStartup(servletContext);
    }
}

那么现在问题就是:org.Springframework.boot.web.embedded.tomcat.TomcatStarter中的onStartup如何被触发的?

public void addServletContainerInitializer(
  ServletContainerInitializer sci, Set<Class<?>> classes) {
  initializers.put(sci, classes);
}

既然利用了initializers来进行了put,那么Tomcat就会在对应的地方来进行执行。但是到了这里和我们的DispatcherServlet又有什么关系呢?那么再来看一下ServletContextInitializer接口的实现类:RegistrationBean,在当前类中也有一个方法(暂时不关注如何调用的)org.Springframework.boot.web.servlet.RegistrationBean#onStartup

public final void onStartup(ServletContext servletContext) throws ServletException {
  String description = getDescription();
  // ....
  register(description, servletContext);
}

来到子类org.Springframework.boot.web.servlet.DynamicRegistrationBean#register中

protected final void register(String description, ServletContext servletContext) {
  D registration = addRegistration(description, servletContext);
  if (registration == null) {
    logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
    return;
  }
  configure(registration);
}

然后来到org.Springframework.boot.web.servlet.ServletRegistrationBean#addRegistration

protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
  String name = getServletName();
  return servletContext.addServlet(name, this.servlet);
}

这里就是来注册一个Servlet,那么这个Servlet是怎么来的呢?

还是来到SpringBoot中的SPI机制,找到DispatcherServletAutoConfiguration,在这个类中可以看到:

@Bean(name = {"dispatcherServletRegistration"})
@ConditionalOnBean(
  value = {DispatcherServlet.class},
  name = {"dispatcherServlet"}
)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
  DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
  registration.setName("dispatcherServlet");
  registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
  multipartConfig.ifAvailable(registration::setMultipartConfig);
  return registration;
}

// ==============================================
// ==============================================
// ==============================================
// ==============================================

@Bean(name = {"dispatcherServlet"})
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
  DispatcherServlet dispatcherServlet = new DispatcherServlet();
  dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
  dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
  dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
  dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
  dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
  return dispatcherServlet;
}

而对应的DispatcherServletRegistrationBean就是要注入DispatcherServlet!

四、总结

Tomcat内嵌

DispatcherServlet的装配

posted @ 2021-08-08 18:03  写的代码很烂  阅读(93)  评论(0编辑  收藏  举报