SpringBoot(七) -- 嵌入式Servlet容器
一.嵌入式Servlet容器
在传统的开发中,我们在完成开发后需要将项目打成war包,在外部配置好TomCat容器,而这个TomCat就是Servlet容器.在使用SpringBoot开发时,我们无需再外部配置Servlet容器,使用的是嵌入式的Servlet容器(TomCat).如果我们使用嵌入式的Servlet容器,存在以下问题:
1.如果我们是在外部安装了TomCat,如果我们想要进行自定义的配置优化,可以在其conf文件夹下修改配置文件来实现.在使用内置Servlet容器时,我们可以使用如下方法来修改Servlet容器的相关配置:
(1)例如我们可以使用server.port=80来修改我们的启用端口号为80;及我们可以通过修改和Server有关的配置来实现(ServerProperties)
(2)修改通用的设置server.XXX;
(3)修改和Tomcat相关的设置:server.tomcat.xxx
2.我们可以编写一个EmbeddedServletContainerCustomizer(嵌入式Servlet容器的定制器):
@Bean public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){ return new EmbeddedServletContainerCustomizer() { //定制嵌入式的Servlet容器相关的规则 @Override public void customize(ConfigurableEmbeddedServletContainer container) { container.setPort(8083); //设置端口为8083 } }; }
二.注册Servlet Filter Listener
我们可以分别使用ServletRegisterationBean FilterRegisterationBean ServletListenerRegisterationBean完成这三大组件的注册
--Servlet
package com.zhiyun.springboot.web_restfulcrud.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author : S K Y * @version :0.0.1 */ 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.zhiyun.springboot.web_restfulcrud.config; import com.zhiyun.springboot.web_restfulcrud.servlet.MyServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author : S K Y * @version :0.0.1 */ @Configuration public class MyServerConfig { //注册三大组件 @Bean public ServletRegistrationBean servletRegistrationBean() { return new ServletRegistrationBean(new MyServlet(), "/myServlet"); } }
--Filter
package com.zhiyun.springboot.web_restfulcrud.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import java.io.IOException; /** * @author : S K Y * @version :0.0.1 */ 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() { } }
@Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new MyFilter()); filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello", "/myServlet")); return filterRegistrationBean; }
--Listener
package com.zhiyun.springboot.web_restfulcrud.listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; /** * @author : S K Y * @version :0.0.1 */ 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项目销毁"); } }
@Bean public ServletListenerRegistrationBean servletListenerRegistrationBean() { return new ServletListenerRegistrationBean<>(new MyListener()); }
--由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件.注册三大组件可以采取这样的方法.
--SpringBoot帮我们自动配置SpringMVC的是惠普,自动的注册SpringMVC的前端控制器:DispatcherServlet.默认拦截"/"所有资源包括静态资源,但是不拦截JSP请求,"/*"会拦截JSP.我们可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径.
三.使用其他的嵌入式容器
SpringBoot还支持Jetty(适合开发长连接的应用,例如聊天室),Undertow(不支持JSP):
1.默认使用了TomCat
2.切换使用其他Servlet容器,首先需要排除其中的spring-boot-starter-web -->spring-boot-starter-tomcat依赖,而后则可以引入其他的Servlet容器
<!--引入Web模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency> <!--引入其他的Servlet容器--> <dependency> <artifactId>spring-boot-starter-jetty</artifactId> <groupId>org.springframework.boot</groupId> </dependency>
四.嵌入式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() */ 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 */ 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 startDaemonAwaitThread(); } catch (Exception ex) { containerCounter.decrementAndGet(); throw ex; } } catch (Exception ex) { throw new EmbeddedServletContainerException( "Unable to start embedded Tomcat", ex); } } }
--我们对嵌入式容器的配置修改是如何生效的:
1.修改ServerProperties中的属性
2.嵌入式Servlet容器定制器:EmbeddedServletContainerCustomizer,帮助我们修改了Sevlet容器的一些默认配置,例如端口号;在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.ServerProperties也是配置器,因此其配置流程如下:
(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.webIOC容器会创建嵌入式的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.ServletContainerInitializer的文件,文件的内容就是ServletContainerInitializer实现的全类名.
(3)可以使用@HandlesTypes注解来实现,容器在应用启动的时候,加载我们所感兴趣的类.
4.启动流程:
(1)启动Tomcat服务器,Spring的Web模块中存在该文件:
(2)SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)所标注的所有这个类型的类都传入到onStartup方法的集合中为这些不是接口不是抽象类类型的类创建实例;
(3)每一个WebApplicationInitializer的实现类都调用自己的onStartup方法.
(4)相当于我们的SpringServletContainerInitializer的类会被创建对象,并执行onStartup方法;
(6)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应用