spring与tomcat的关系逆袭前后的源码设计分析
Posted 弓箭手IN上海
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring与tomcat的关系逆袭前后的源码设计分析相关的知识,希望对你有一定的参考价值。
简介
1. 从Tomcat启动spring
1.1 tomcat给外部系统的机会
onStartup(Set<Class<?>> c, ServletContext ctx)。参数是所有实现@HandlesTypes指明接口的实现类,与ServletContext 。`
> 有一点奇怪,为何tomcat不少管一点,只调用A,让A自己找所有的Bs?毕竟B类型自己说了算的。
1.2 spring如何对接
- 对接ServletContainerInitializer
Spring-web中的meta-inf里面的文本文件里写的org.springframework.web.SpringServletContainerInitializer。这个类的注解是@HandlesTypes(WebApplicationInitializer.class) ,它的onStartup方法就是实现化所有的WebApplicationInitializer.class实现类,并调用它们的initializer.onStartup(servletContext);
servletContext是tomcat给过来的,现在交给了所有的WebApplicationInitializer.class实现类。它并没有核心功能,如同一个中介一样。
- 对接@HandlesTypes(WebApplicationInitializer.class)
Spring为了方便使用,引入了一个抽象类来实现WebApplicationInitializer.class,也就是 org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer 。因为我们最终要按自己的配置要求扩展它,代替web.xml的配置就是在这里。
因此当部署到 Servlet 3.0 容器中的时候,容器通过@HandlesTypes会自动发现它,通过中介SpringServletContainerInitializer,来配置Servlet上下文servletContext。
- AbstractAnnotationConfigDispatcherServletInitializer要你实现什么哪些抽象方法?
方法一:Class<?>[] getRootConfigClasses();
得到@Configuration注解的类,给createRootApplicationContext()来用。我们知道这个注解通常可以生成AnnotationConfigWebApplicationContext类型的一个spring容器。这个是根容器。
方法二:Class<?>[] getServletConfigClasses();
得到@Configuration注解的类,给createServletApplicationContext()用。也是用来生成AnnotationConfigWebApplicationContext类型的spring容器,这个会是一个Servlet所属的子容器。目前还没有和根容器关联。
- AbstractAnnotationConfigDispatcherServletInitializer类机制是怎么的?
补全了抽象方法,我们还是要知道这个类被中介调用的,中介调它onStartup方法,传入servletContext。这个方法主要有两段功能组合,一个是父类中,一个是本类中。
```java
//<!------------------------onStartup方法解析(两段功能):----------------->
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
registerDispatcherServlet(servletContext);
}
//----父类super.onStartup功能:
//先产生根spring容器,再把容器被包装进一个对servletContext监听的ContextLoaderListener。它实现接口的contextInitialized与contextDestroyed两个动作由servletContext触发。前者会把根spring容器作为servletContext中的一个KV项。但servletContext何时初始化要等先被配置好。web.xml出有ContextLoaderListener的配置。
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
//getRootApplicationContextInitializers(),一般不用。
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);
}
//----子类中registerDispatcherServlet的功能:
//产生dispatcherServlet与它的MVC容器。最后把dispatcherServlet注册进servletContext。并设置启动,mapping等信息。在WEB.XML中也有这样的设置项目。
WebApplicationContext servletAppContext = createServletApplicationContext();
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);//new一个dispatcherServlet。
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
```
- 出现的DispatcherServlet特别说明一下
Servlet一般有init(),service(req,res),destroy()等方法。DispatcherServlet持有MVC容器,很好利用容器中的mapping与controller对象处理业务了。先关注一下init();真正的功能在initServletBean()中的initWebApplicationContext()。它注册进了servletContext,就可以从中拿到root窗口,并用setParent(rootContext)方法设置好它所持有的mvc容器的父级spring容器,这样@controller对象就好从父级中找到@service对象了。
- 上面把web服务启动后,主要功能都设置好了。
1.3 总结
### 目标
Tomcat只给外部应用一个时机,让外部配置servletContext。而spring的目标是把两种容器(共用根容器与每个servlet的单独子容器)及DispatcherServlet等东东加进servletContext去,让DispatcherServlet用作请求转发处理。
### 实现
WebServer启动时,会启动一个文本文件中SPI的ServletContainerInitializer实现,把servletContext给它处理。这个spring的中介会实例化最终用户配置的一个AbstractDispatcherServletInitializer,把servletContext给它处理。
AbstractDispatcherServletInitializer处理时会根据配置类产生根容器,并使用一个监听在servletContext.init()时把根容器加为servletContext中的一个KV项。
然后它根据另一个MVC配置类产生子容器与持有它的DispatcherServlet,并注册到servletContext。等DispatcherServlet.init()时会关联上面说的父容器。
1.4 引申
前面介绍的AbstractAnnotationConfigDispatcherServletInitializer用起来很简单,只要设置两个spring容器的配置类就可以了,父子容器就都有了,用着爽。但要自己进行些处理就麻烦点了,你可以继承抽象类的父类多些灵活性。另外这个文章:https://my.oschina.net/521cy/blog/702864【零配置即使用Java Config + Annotation】中,没有去继承的抽象类,自己实现了相关接口,并详细介绍了与web.xml的对比进行配置,可以参考。其核心的功能还是一样的。如果你想配置多个Servlet,或者DispatcherServlet,都是比较容易实现的了。
> 这个文章中的方法:onStartup(ServletContext container),后面的container名称不妥,ServletContainer与ServletContext是不同层次的东西,前者更大,这样写名不副实。
2. 从spring启动Tomcat
这个就是springboot的方式,用main启动,使用内嵌Tomcat。
2.1 spring的反客为主的思路
spring的根容器中都是核心业务,按说Tomcat只是一个暴露通讯方式,即可以用Tomcat,也可以用其它Servlet容器。还可以不用Servlet容器,比如WebApplicationType.REACTIVE类型,会绕过servlet容器。按说它还可以进一步适配各种通讯协议供外部使用核心业务。这就是反客为主。
> 注:看过一个区块链教程中,HTTPService httpService = new HTTPService(blockService, p2pService);用http通讯服务去整合核心服务与P2P通讯,明显不妥当。应该用核心去整合通讯方式并适配多种方式才是稳定的。
既然以spring中的业务为主,它就会在启动中带动相关的其它外部应用模块,比如Tomcat容器的启动。
2.2 springboot的启动
通常我们的应用中,在有@SpringBootApplication的主类的main中调用:SpringApplication.run(*.class),进而调用到下面的方法:
```java
//1。启动后调用的方法:
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//配置一个new出来的SpringApplication,并运行,产生一个springIoc容器。
return new SpringApplication(primarySources).run(args);
}
//2。上面的方法中的run里面:就是生成一个容器及常见的操作:prepareContext,refreshContext,afterRefresh
//而new 操作,根据类判断,可以产生三种容器,一般是servlet类型的特殊spring容器AnnotationConfigServletWebServerApplicationContext,这里还没确定是tomcat或者jetty呢
...
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
...
//3。AnnotationConfigServletWebServerApplicationContext父类的refresh中会调用onRefresh,里面有一句createWebServer();这里就开始生成Web服务器了。
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
```
2.3 产生WebServer工厂的过程
仅会生成特定的WebServer,并产生一个初始化工具(ServletContextInitializer实现类)给它。有点像新成立一个分公司,却只派了一个业务指导过去。
看工具接口名字就知道是用来初始化ServletContext的。ServletContext的层次在Tomcat中并不高,上面还有container,且看分析。
```java
//1。createWebServer();中主要有这两句,用一个工厂来生成WebServer,工厂包含mock的共有4种。同时把getSelfInitializer得到的一个实现了ServletContextInitializer的初始化工具给它。
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
```
```java
//2。这个工具的写法有点独特this::selfInitialize,主要是实现了onStartup(是ServletContext servletContext接口)方法。方法体如下,却看不到方法名字:
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
//3。上面的方法中有两个地方说明一下:
prepareWebApplicationContext(servletContext):主要是把自己这个spring根容器注册到servletContext中,简单粗暴,不象前者要在监听servletContext时才设置。
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
getServletContextInitializerBeans():
//方法中说明为:By default this method will first attempt to find
* { ServletContextInitializer}, { Servlet}, { Filter} and certain
* { EventListener} beans.
//这就是从根容器中找到所有实现了ServletContextInitializer接口的类,用来注册servlet/filter等东东进入ServletContext。与上面的selfInitialize是一个接口,不过是其内部调用的,作用不同。
```
上面产生一个WebServer的工厂,另外就是传入了一个工具。这个工具被执行时,除了自己处理外,又从spring容器中找了一堆其它的工具来处理。所有的这两层工具都实现了ServletContextInitializer,不过前者注册根spring容器,后者注册servlet等东西。这些工具都在等着onStartup才 运行。onStartup后面会讲到。
2.4 产生TomcatServer的过程
工具们传了进来,具体又传给了谁?
```java
public WebServer getWebServer(ServletContextInitializer... initializers) {
...
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);
}
//1。前面都是产生嵌入的Tomcat及它的内部对象。后面两句是重点。先分析第一个。
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
//2。再产生了一个Web应用,放在host中。工具initializers又传给了它。
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
...
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
...
File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
context.setDocBase(docBase.getAbsolutePath());
...
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
host.addChild(context);
//3。这句是重点,继续传initializers进去。
configureContext(context, initializersToUse);
postProcessContext(context);
}
//4。configureContext()的主要内容如下,产生了一个ServletContainerInitializer接口的实现TomcatStarter,它赋给了TomcatEmbeddedContext的目的用来监听WebServer的启动的,而那些工具给了它备用。
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
//5。TomcatStarter作为ServletContainerInitializer,被WebServer启动调用其onStartup,同时会得到servletContext,最外面传入的工具备用在此,就是来初始化servletContext的。前面设置KV根少了一个监控ServletContext的东东,这里又多了一个监控WebServer的东东(叫ServletContainerInitializer这个名字表明了监控的目的,感觉完整的应该叫InitializeServletContainer_ServerListener吧:)。
//6。return getTomcatWebServer(tomcat);
//这句之前都是处理tomcat内部的host->TomcatEmbeddedContext->TomcatStarter。这里是外部包了一层,通常我们适配多种产品,都会分别外包一层,以抽象出公共的对象,让外部无感使用。这句内部有一句:
this.tomcat.start();
//正式启动了tomcat了。前面配置好的ServletContainerInitializer开始工作了,传过来ServletContext了,ServletContextInitializer也都可以工作了。
```
> 该springboot中的接口`ServletContextInitializer`和前面介绍的`Spring Web`的另外一个接口`WebApplicationInitializer`看起来几乎一模一样。而且都被不同的ServletContainerInitializer接口类使用。但二者使用目的不同,初始化的目标不一样。`Spring Web`中,`WebApplicationInitializer`也是针对`Servlet 3.0+`环境,设计用于程序化配置`ServletContext`,跟传统的`web.xml`相对或者配合使用,`WebApplicationInitializer`实现类会被`SpringServletContainerInitializer`标识,从而被tomcat自动检测和调用。
2.5 第二层次的ServletContextInitializer工具们都在哪,怎么用?
独特this::selfInitialize所产生的第一层次工具,把根spring容器记入ServletContext的KV项,又从容器中找二层工具。哪么都有哪些二层工具呢?怎么用呢?
springboot是自动配置机制的,重点关注这三个:
> 1.EmbeddedServletContainerAutoConfiguration
> 注入容器bean,根据当前包扫描,默认tomcat
> 2.DispatcherServletAutoConfiguration
> 默认dispatchServlet配置
> 3.WebMvcAutoConfiguration
看看package org.springframework.boot.autoconfigure.web.servlet中的:
```java
(Ordered.HIGHEST_PRECEDENCE)
false) (proxyBeanMethods =
(type = Type.SERVLET)
(DispatcherServlet.class)
(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
...
//DispatcherServlet,不再介绍了。
(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
...
return dispatcherServlet;
}
//DispatcherServletRegistrationBean
(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
...
}
//----------------------------------------------------------
//DispatcherServletRegistrationBean的基父类中实现了ServletContextInitializer,所以它是二层工具。本类主要设置path。
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet>
public class ServletRegistrationBean<T extends Servlet> extends DynamicRegistrationBean<ServletRegistration.Dynamic> {
public abstract class DynamicRegistrationBean<D extends Registration.Dynamic> extends RegistrationBean {
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
//本质还是ServletContextInitializer,onStartup方法被调用。
这些类的调用如下:
onStartup方法-->
register(description, servletContext);-->
D registration = addRegistration(description, servletContext);-->
servletContext.addServlet(name, this.servlet);
```
DispatcherServlet是实现implements ApplicationContextAware接口的,当然就会自动感知spring容器。它持有的就是根容器。不再是之前说的mvc子容器了。
2.6 springmvc容器没有了?
```java
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
```
没看到SpringMvc容器的代码,以下文章都提到了一个容器的问题:
https://segmentfault.com/a/1190000017327469【[深入Spring Boot:Spring Context的继承关系和影响](https://segmentfault.com/a/1190000017327469)】
https://www.jianshu.com/p/6a869eabfe78【SpringMvc在SpringBoot环境和Web环境中上下文的关系】
在Web环境中是由Spring和SpringMvc两个容器组成的,在SpringBoot环境中只有一个容器AnnotationConfigEmbeddedWebApplicationContext。
> 2.2.0.BUILD-SNAPSHOT中的根容器叫AnnotationConfigServletWebServerApplicationContext,1.5.2中还是叫AnnotationConfigEmbeddedWebApplicationContext,看到都有WebApplicationContext,是为web应用而生吧。自动配置就只用这么一个容器了。
2.7 引申(多个容器)
默认是一个容器,也可以搞多个SpringMvc容器的,当你需要:
[Spring Boot with multiple DispatcherServlet, each having their own @Controllers](https://stackoverflow.com/questions/30670327/spring-boot-with-multiple-dispatcherservlet-each-having-their-own-controllers)时。
https://stackoverflow.com/questions/30670327
```java
(exclude=DispatcherServletAutoConfiguration.class)
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
public ServletRegistrationBean foo() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(FooConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/foo/*");
servletRegistrationBean.setName("foo");
return servletRegistrationBean;
}
public ServletRegistrationBean bar() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(BarConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/bar/*");
servletRegistrationBean.setName("bar");
return servletRegistrationBean;
}
}
```
2.8 总结
### 目标
在产生根spring容器的同时,产生一个嵌入式的tomcat对象,把初始化ServletContext的所有两个层次的ServletContextInitializer工具们传进去。一层工具把根容器注册好,二层工具注册dispatcherServlet等东东。
### 实现
ServletContextInitializer工具们被传递到tomcat内部比较深的地方,由监听Webserver启动的ServletContainerInitializer的实现类TomcatStarter所持有。
这过程中要判断spring容器类型是servlet,再判断用Tomcat,然后真正工厂来实现化Tomcat及它内部的一些类,还要扩展内部的一个类,通过它才能把带着工具的TomcatStarter放进去。等Tomcat真正启动了,TomcatStarter用工具处理给它的ServletContext。
3. 回顾
前面分析了一通,但还需要从总体上分析一下作者的设计思路。
3.1 对象与生命周期
**主要对象:**
ServletContext:并非一个servlet对应一个ServletContext。而是一个web应用(webApp)对应一个ServletContext实例,这个实例是应用部署启动后,servlet容器为应用创建的。ServletContext实例包含了所有servlet共享的资源信息。通过提供一组方法给servlet使用,用来和servlet容器通讯,比如获取文件的MIME类型、分发请求、记录日志等。
ServletContainer:可以当作提供servlet功能的WebServer。
**生命周期处理:**
WebServer启动spring 情况下,调用ServletContainerInitializer的启动方法onStartup,给了外部一个配置这个web应用对应的ServletContext的机会。
- 当onStartup时,产生了根容器,但具体让一个ContextLoaderListener去监听ServletContext的初始化完成操作时候写入根容器到ROOT的KV值。等于是产生了根容器,但要等时机。(让我配置它,我先准备好材料,等contextInitialized这个时机点配置上去)
- onStartup时,接着产生dispatcherServlet和它的容器,dispatcherServlet带着它的容器并注册到ServletContext中去。dispatcherServlet也有一个init()机会,这时候去找根容器,作为自己带的容器的父容器。(我先把材料放进去,你用它的时候初始化一下它就可以用了)
说明ServletContext初始化完成,要早于dispatcherServlet的init();。初始化应该晚于放servlet进去,加材料只是配置。一般设计一个类的生命周期,参考spring,有配置,再初始化及正式运行,最后销毁,重要的生命节点要通知监听者。简单的说就是先装配好,再监听,适当时候再进一步处理。
**疑问:**
- 为何不在装配时设置KV?我们知道ServletContext加KV值实际上就是webApp的全局共享变量,随时可以加,所以这操作都不能算在初始化中吧。
- 但tomcat启动,通知一个ServletContainerInitializer实现类来处理ServletContext。既然是配置context,为何不叫ServletContextInitializer呢?也许启动给不同的webApp都同时进行ServletContext设置,多个context就不能叫ServletContextInitializer了,或者加个s,或者按更上层对象来命名吧。
在springboot中,它为Tomcat准备了一堆ServletContextInitializer对象,这是spring里用来处理context的接口,很明显这些要是处理一个WebApp的ServletContext的,只关心这个。
启动Tomcat前,给它要求生成了Connector/getHost等内部对象。还生成一个TomcatEmbeddedContext对象,host.addChild(context);这句加入到host中,然后通过它为中介,把一个ServletContainerInitializer接口对象TomcatStarter设置进去,这是前面提到过的接口,都是监控Webserver启动的。Tomcat启动了就通知到TomcatStarter了,并给它一个真正的context来处理,TomcatStarter早就持有一个多层次的ServletContextInitializer对象,就可以两个层次处理ServletContext了。
3.2 设计思考
两种情况下,真正设置tomcat的ServletContext都是从ServletContainerInitializer的被调用onStartup开始的。
前者由Tomcat启动从文件中找的对象,再通过实现了WebApplicationInitializer的AbstractAnnotationConfigDispatcherServletInitializer处理;它的接口名字与类名字感觉差别比较大,给tomcat调用是为了初始化WebApplication的,而在这个过程中又要产生Servlet并配置进ServletContext,名字如果叫WebApplication2ServletContextInitializer更准确吧。设置KV值还要通过监听来等个机会进行不知道为啥这麻烦?不过KV一定要放在ServletContext,被可能的多个MVC容器共享使用。
后者由springboot启动tomcat并设置ServletContainerInitializer实现类进去,再由tomcat反过来调用ServletContainerInitializer实现类的onStartup()方法开始真正配置ServletContext。后面处理ServletContext的多个类的接口统一叫ServletContextInitializer名字很准确,功能专一。也不再监听去等ServletContext的初始化后的机会设置KV值了,其中一个ServletContextInitializer直接一步就设置好了,其它的ServletContextInitializer都从总容器中拿。复杂的是onStartup前面的过程。
两种情况下,dispatcherServlet一旦被注册进了ServletContext,就由tomcat接手了三个生命周期。不同的是,前者dispatcherServlet带着新生成的mvc容器,并在init时找到父容器。后者因为aware了根容器了,就带着根容器进去的,父容器还是自己了。可能因为有了springboot自动配置机制的便利吧,一个容器就很好按条件自动配置内部的Bean了,之前为啥不搞一个容器呢?担心mvc容器特殊Bean多,可以专门继承一个用啊?
以上是关于spring与tomcat的关系逆袭前后的源码设计分析的主要内容,如果未能解决你的问题,请参考以下文章
Java毕业设计+现成产品 —>基于springboot+vue+redis前后端分离家具商城平台系统(源码+论文初稿可运行)15主要设计:用户登录注册商城分类商品浏览查看购物车订单支付
Spring Boot企业级开发前后端分离博客系统+Thymeleaf实战+Jpa数据持久化实战+全文检索实战+架构设计与分层+API设计
源码时代UI干货分享|创意剪纸风格海报设计,逆袭设计大神的必备技能
基于springboot+vue+redis前后端分离家具商城平台系统项目(源码+论文初稿可以直接运行)015主要设计:用户登录注册商城分类商品浏览查看购物车订单支付以及后台的管理
SpringBoot+Vue+Redis前后端分离家具商城平台系统(源码+论文初稿直接运行《精品毕设》)15主要设计:用户登录注册商城分类商品浏览查看购物车订单支付以及后台的管理