SpringBoot--嵌入式Servlet容器
作者:互联网
一、嵌入式Servlet容器
在传统的Web开发中,需要将项目打成 war 包,在外部配置部署好 Tomcat 服务器,而这个 Tomcat 就是 Servlet 容器,在使用 SpringBoot 开发时,我们无需再外部部署 Servlet 容器,使用的是嵌入式(内置) Servlet 容器( Tomcat ),如果我们使用嵌入式 Servlet 容器,存在以下问题:
-
1、如果我们是在外部安装了 Tomcat ,如果我们想要进行自定义的配置优化,可以在其 conf 文件夹下修改配置文件来实现,或者通过在 pom.xml 中导入 Tomcat 插件实现。
-
2、而在使用内置 Servlet 容器时,我们可以使用如下方法来修改 Servlet 容器的相关配置:
- (1)例如我们可以在 SpringBoot 项目中的 application.yml 文件中编写 server.port=80 来修改我们的启用端口为80 ;
- (2)可以通过修改和 Servlet 有关的配置来实现( ServetProperties );
- (3)可以通过修改通用的设置: server.xxx ;
- (4)可以通过修改和Tomcat相关的设置: server.tomcat.xxx ;
- (5)可以编写一个 EmbeddedServletContainerCustomizer (嵌入式 Servlet 容器的定制器)
@Bean public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){ return new EmbeddedServletContainerCustomizer() { //定制嵌入式的Servlet容器相关的规则 @Override public void customize(ConfigurableEmbeddedServletContainer container) { container.setPort(8083); //设置端口为8083 } }; }
二、注册 Servlet 、 Filter 、 Listener
-
由于 SpringBoot 默认是以 jar 包的方式通过嵌入式 Servlet 容器来启动 SpringBoot 的 Web 应用,没有 web.xml 文件,我们可以分别使用 ServletRegisterationBean 、 FilterRegisterationBean 、 ServletListenerRegisterationBean 完成 Web 三大组件的注册
-
—— Servlet
package com.itheima.Servlet; import ```javax.servlet.ServletException; import ```javax.servlet.http.HttpServlet; import ```javax.servlet.http.HttpServletRequest; import ```javax.servlet.http.HttpServletResponse; import ```java.io.IOException; public class MyServlet extends HttpServlet { //处理get()请求 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Hello MyServlet"); } }
package com.itheima.config; import com.itheima.Servlet.MyServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyServerConfig { //注册三大组件之一:Servlet @Bean public ServletRegistrationBean servletRegistrationBean() { return new ServletRegistrationBean(new MyServlet(), "/myServlet"); } }
-
—— Filter
package com.itheima.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ```javax.servlet.*; import ```java.io.IOException; public class MyFilter implements Filter { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { logger.debug("自定义的Filter启用了!"); chain.doFilter(request, response); } @Override public void destroy() { } }
package com.itheima.config; import com.itheima.Filter.MyFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import ```java.util.Arrays; @Configuration public class MyFilterConfig { //注册三大组件之一:Filter @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new MyFilter()); filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello", "/myServlet")); return filterRegistrationBean; } }
-
—— Listener
package com.itheima.Listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ```javax.servlet.ServletContextEvent; import ```javax.servlet.ServletContextListener; public class MyListener implements ServletContextListener { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void contextInitialized(ServletContextEvent sce) { logger.debug("contextInitialized...当前web应用启动了"); } @Override public void contextDestroyed(ServletContextEvent sce) { logger.debug("contextDestroyed...当前web项目销毁"); } }
package com.itheima.config; import com.itheima.Listener.MyListener; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyListenerConfig { //注册三大组件之一:Listener @Bean public ServletListenerRegistrationBean servletListenerRegistrationBean() { return new ServletListenerRegistrationBean<>(new MyListener()); } }
-
-
SpringBoot 帮我们自动配置 SpringMVC ,自动的注册 SpringMVC 的前端控制器: DispatcherServlet ,默认拦截 "/" 所有资源,包括静态资源,但是不拦截 JSP 请求, "/*" 会拦截 JSP ,我们可以通过 server.servletPath 来修改 SpringMVC 前端控制器默认拦截的请求路径
三、嵌入式 Servlet 容器的自动配置原理
在 SpringBoot 中拥有如下自动配置类 EmbeddedServletContainerAutoConfiguration :
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(BeanPostProcessorsRegistrar.class)
public class EmbeddedServletContainerAutoConfiguration {}
- 该类就是嵌入式的 Servlet 容器自动配置类
/**
* Nested configuration if Tomcat is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
- 如果我们导入了 Servlet 的相关依赖,那么我们就会存在 Servlet.class 类以及 Tomcat.class 类,并且容器中不存在 EmbeddedServletContainerFactory 嵌入式容器工厂(用户自定义的 Servlet 容器工厂,那么该配置就会生效),在嵌入式容器工厂中定义了如下类:
public interface EmbeddedServletContainerFactory {
/**
Gets a new fully configured but paused {@link EmbeddedServletContainer} instance.
Clients should not be able to connect to the returned server until
{@link EmbeddedServletContainer#start()} is called (which happens when the
{@link ApplicationContext} has been fully refreshed).
@param initializers {@link ServletContextInitializer}s that should be applied as
the container starts
@return a fully configured and started {@link EmbeddedServletContainer}
@see EmbeddedServletContainer#stop()
*/
/**
获取一个新的完全配置但暂停的 {@link EmbeddedServletContainer} 实例。
在调用 {@link EmbeddedServletContainer#start()} 之前,客户端应该无法连接到返回的服务器(这在 {@link ApplicationContext} 已完全刷新时发生)。
@param 初始化器 {@link ServletContextInitializer} 应在容器启动时应用
@return 完全配置并启动 {@link EmbeddedServletContainer}
@see EmbeddedServletContainer#stop()
*/
EmbeddedServletContainer getEmbeddedServletContainer(
ServletContextInitializer... initializers);
}
- 获取嵌入式的 Servlet 容器,在 SpringBoot 的默认实现中存在如下的实现:
- 以嵌入式 Tomcat 容器工厂为例:
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
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);
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 getTomcatEmbeddedServletContainer(tomcat);
}
- 可以发现其内部使用 Java 代码的方式创建了一个 Tomcat ,并配置了 Tomcat 工作的基本环境,最终返回一个嵌入式的 Tomcat 容器
/**
Create a new {@link TomcatEmbeddedServletContainer} instance.
@param tomcat the underlying Tomcat server
@param autoStart if the server should be started
*/
/**
创建一个新的 {@link TomcatEmbeddedServletContainer} 实例。
@param tomcat 底层是 Tomcat 服务器
@param autoStart 是否应该启动服务器
*/
public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
initialize();
}
private void initialize() throws EmbeddedServletContainerException {
TomcatEmbeddedServletContainer.logger
.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
try {
// Remove service connectors to that protocol binding doesn't happen yet
// 尚未删除与该协议绑定的服务连接器
removeServiceConnectors();
// Start the server to trigger initialization listeners
// 启动服务器触发初始化监听器
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
// 我们可以直接在主线程中重新抛出失败异常
rethrowDeferredStartupExceptions();
Context context = findContext();
try {
ContextBindings.bindClassLoader(context, getNamingToken(context),
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
// 与 Jetty 不同,所有 Tomcat 线程都是守护线程。我们创建一个阻塞的非守护进程来停止立即关闭
startDaemonAwaitThread();
}
catch (Exception ex) {
containerCounter.decrementAndGet();
throw ex;
}
}
catch (Exception ex) {
throw new EmbeddedServletContainerException(
"Unable to start embedded Tomcat", ex);
}
}
}
-
我们对嵌入式容器的配置修改是如何生效的:
- 1、修改 ServletProperties 中的属性
- 2、嵌入式 Servlet 容器定制器: EmbeddedServletContainerCustomizer ,帮助我们修改了 Servlet 容器的一些默认配置,例如端口号;在 EmbeddedServletContainerAutoConfiguration 中导入了一个名为 BeanPostProcessorsRegistrar ,给容器中导入一些组件即嵌入式 Servlet 容器的后置处理器。后置处理器表示的是在 bean 初始化前后(创建完对象,还没有赋予初始值)执行初始化工作。
- 3、 EmbeddedServletContainerAutoConfiguration 为嵌入式 Servlet 容器的后置处理器的自动配置类,其存在如下类:
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof ConfigurableEmbeddedServletContainer) { postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean); } return bean; }
private void postProcessBeforeInitialization( ConfigurableEmbeddedServletContainer bean) { for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) { customizer.customize(bean); } }
private Collection<EmbeddedServletContainerCustomizer> getCustomizers() { if (this.customizers == null) { // Look up does not include the parent context this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>( this.beanFactory .getBeansOfType(EmbeddedServletContainerCustomizer.class, false, false) .values()); Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE); this.customizers = Collections.unmodifiableList(this.customizers); } return this.customizers; }
- 4、获取到了所有的定制器,调用了每一个定制器的 customer 方法来给 Servlet 容器进行属性赋值
- 5、 ServletProperties 也是配置器,因此其配置流程如下:
- (1) SpringBoot 根据导入的依赖情况,添加响应的配置容器工厂 EmbeddedServletCustomerFactory
- (2)容器中某个组件要创建对象就会惊动后置处理器
- (3)只要是嵌入式的 Servlet 容器工厂,后置处理器就工作,从容器中获取所有的 EmbeddedServletContainerCustomizer 调用定制器的定制方法
四、嵌入式 Servlet 容器启动原理
获取嵌入式 Servlet 容器工厂:
- 1、 SpringBoot 引用启动运行 run 方法
- 2、 refreshContext(context) : SpringBoot 刷新容器并初始化容器,创建容器中的每一个组件,如果是 Web 应用,创建 web 的 IOC 容器 AnnotationConfigEmbeddedWebApplicationContext ,如果不是则创建 AnnotationConfigApplicationContext
- 3、 refreshContext(context) :刷新刚才创建好的容器
- 4、 onRefresh() : web 的 IOC 容器重写了 onRefresh 方法
- 5、 web 的 IOC 容器会创建嵌入式的 servlet 容器: createEmbeddedServletContainer()
- 6、获取嵌入式的 servlet 容器工厂: EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory()
- 7、使用容器工厂获取嵌入式的 Servlet 容器
- 8、嵌入式的 Servlet 容器创建对象,并启动 servlet 容器
五、使用外置的 Servlet 容器
嵌入式 Servlet 容器
- 优点:简单、快捷
- 缺点:默认不支持JSP,优化定制复杂(使用定制器,自定义配置 Servlet 容器的创建工厂)
外部的 Servlet 容器:外面安装 Tomcat -应用 war 包的方式打包
我们使用 war 包的形式创建 SpringBoot 工程可以发现其目录结构如下:
- 创建项目 webapp 路径及 web-XML 文件
- 部署 Tomcat 服务器
-
创建步骤:
- (1)必须创建一个 war 项目
- (2)将嵌入式的 Tomcat 指定为 provided
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>
- (3)必须编写一个 SpringBootServletInitializer 的子类,目的就是调用 config 方法
package com.skykuqi.springboot.exteralservlet; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.support.SpringBootServletInitializer; public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { //传入SpringBoot应用的主程序 return application.sources(ExteralservletApplication.class); } }
六、外置 Servlet 容器的启动原理
-
1、 jar 包:当我们的应用是使用 SpringBoot 的 jar 包形式的话,我们可以直接通过执行 SpringBoot 主类的 main 方法,启动 IOC 容器,创建嵌入式的 Servlet 容器
-
2、 war 包:启动服务器,服务器启动 SpringBoot 应用,启用 IOC 容器
-
3、在 Servlet3.0 中有一项规范:
- (1)服务器启动( web 应用启动)会创建当前 web 应用里面每一个 jar 包里面 ServletContainerInitializer 的实例
- (2) ServletContainerInitializer 的实现必须放在 META-INF/services 文件夹下,该文件夹下还必须有一个文件名为 ```javax.servlet.ServletContainerInitialize 的文件,文件的内容就是 ServletContainerInitializer 实现的全类名
- (3)可以使用 @HandlesTypes 注解来实现,容器在应用启动的时候,加载我们所感兴趣的类
-
4、启动流程:
-
(1)启动 Tomcat 服务器, Spring 的 Web 模块中存在该文件:
-
(2) SpringServletContainerInitialize 将 @HandlesTypes(WebApplicationInitializer.class) 所标注的所有这个类型的类都传入到 onStartup 方法的集合中为这些不是接口不是抽象类类型的类创建实例
-
(3)每一个 WebApplicationInitializer 的实现类都调用自己的 onStartup 方法
-
(4)相当于我们的 SpringServletContainerInitializer 的类会被创建对象,并执行 onStartup 方法
-
(5) SpringServletContainerInitializer 执行 onStartup 的时候会创建容器
protected WebApplicationContext createRootApplicationContext( ServletContext servletContext) { SpringApplicationBuilder builder = createSpringApplicationBuilder(); StandardServletEnvironment environment = new StandardServletEnvironment(); environment.initPropertySources(servletContext, null); builder.environment(environment); builder.main(getClass()); ApplicationContext parent = getExistingRootWebApplicationContext(servletContext); if (parent != null) { this.logger.info("Root context already created (using as parent)."); servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null); builder.initializers(new ParentContextApplicationContextInitializer(parent)); } builder.initializers( new ServletContextApplicationContextInitializer(servletContext)); builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class); builder = configure(builder); SpringApplication application = builder.build(); if (application.getSources().isEmpty() && AnnotationUtils .findAnnotation(getClass(), Configuration.class) != null) { application.getSources().add(getClass()); } Assert.state(!application.getSources().isEmpty(), "No SpringApplication sources have been defined. Either override the " + "configure method or add an @Configuration annotation"); // Ensure error pages are registered if (this.registerErrorPageFilter) { application.getSources().add(ErrorPageFilterConfiguration.class); } return run(application); }
- 将创建 RootApplicationContext 容器,在创建容器时会进行如下操作
- a.创建 SpringApplicationBuilder
- b.在18行调用了 configer() ,将 SpringBoot 的主程序类传入了进来
- c.使用 builder 创建一个 Spring 应用
- 将创建 RootApplicationContext 容器,在创建容器时会进行如下操作
-
参考博客:https://www.cnblogs.com/skykuqi/p/12000060.html
标签:容器,SpringBoot,Tomcat,--,嵌入式,import,Servlet,public 来源: https://www.cnblogs.com/cxy-lxl/p/16609238.html