【Spring Boot】9.嵌入式servlet容器.

简介

咱们已经知道,使用SpringBoot启动web应用并不须要配置tomcat,就能够直接使用,实际上是springboot使用的是tomcat做为嵌入式的servlet容器致使的,这称做嵌入式的servlet容器,这是怎么一回事,springboot的内部都作了些什么呢?前端

问题

  1. 如何定制和修改servlet容器的相关配置?
  2. SpringBoot能不能支持其余的Servlet容器?

修改相关配置

1. 经过全局配置文件application.properties修改

修改server对象的值相关属性就能够了(ServerProperties)。java

通用的Servlet容器设置

server.port = 8081
server.context-path=/myweb

修改tomcat相关的配置

server.tomcat.xxxx=cccc

2. 经过配置类

编写一个WebServerFactoryCustomize类型的servlet组件,注意,我这里是2.x版本,若是是1.x的话应该是EmbeddedServletContainerCustomizer,教程里是1.0的 ,不过总体差异不大,差很少是同样的用法:web

MyConfig.class

@Configuration
public class MyConfig implements WebMvcConfigurer {

    @Bean
    // 定制嵌入式的servlet容器相关规则
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
        return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                factory.setPort(8085);
            }
        };
    }
}

注册servlet三大组件

咱们知道servletd的三大组件分别为:Servlet、Filter、Listener。因为咱们如今打包是jar形式,以jar方式启动嵌入式的tomcat,不是标准的web目录结构,标准目录下有一个WEB-INF/web.xml,咱们通常会在web.xml中注册三大组件,而jar形式该怎么注册呢?spring

注册Servlet

要注册Servlet,只需在SpringBoot容器中注册一个名为ServletRegistrationBean的组件便可,查看源码,其某个构造函数以下所示,分别表明咱们须要传入的servlet,以及映射的路径。apache

org.springframework.boot.web.servlet.ServletRegistrationBean.class

public ServletRegistrationBean(T servlet, String... urlMappings) {
        this(servlet, true, urlMappings);
    }

所以,咱们能够这样作:tomcat

  1. 自定义一个servlet

servlet/MyServlet.class

package com.zhaoyi.springboot.restweb.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 {
    @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("I'm Myservlet!");
    }
}

而后,再将该Servlet绑定到ServletRegistrationBean组件并添加到容器中。

config/MyServerConfig.class

package com.zhaoyi.springboot.restweb.config;

import com.zhaoyi.springboot.restweb.servlet.MyServlet;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyServerConfig {

    @Bean
    // 配置servlet容器
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
        return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                factory.setPort(8085);
            }
        };
    }

    /**
     * 配置自定义Servlet组件
     * @return
     */
    @Bean
    public ServletRegistrationBean myServlet(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(), "/myServlet");
        return registrationBean;
    }
}

访问地址:localhost:8085/myServlet,便可获得反馈springboot

I'm Myservlet!

上一节有一个配置容器的配置(将内嵌容器的启动端口号修改成8085),我将其移动到了MyServerConfig.class中,留意一下。服务器

注册Filter

以后的两大组件的注册方式其实就和Servlet注册的方式大同小异了,咱们看看怎么作就好了。先来自定义一个Filter,咱们须要实现javax.servlet.Filter接口,该接口的源码以下所示:app

javax.servlet.Filter.class

package javax.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

能够看到,有两个默认方法,所以,咱们实现该接口,只需实现其doFilter方法便可。maven

filter/MyFilter.class

package com.zhaoyi.springboot.restweb.filter;

import javax.servlet.*;
import java.io.IOException;

public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("this is my filter...");
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

filterChain.doFilter放行请求,咱们能够看到Chain(链条)的字样,在实际生产中其实咱们须要定义不少Filter,他们在应用中造成一个链条,依次过滤,因此颇有chain的味道。

接下来,咱们将该Filter注册到容器中,并设置须要过滤的映射路径:

config/MyServerConfig.class

// Filter
    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new MyFilter());
        filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/index","/myFilter"));
        return filterFilterRegistrationBean;
    }

这样,咱们访问localhost:8085/indexlocalhost:8085/myFilter这些路径的时候,就会在控制台打印以下信息:

this is my filter...

而其余的路径则不受影响,代表过滤器生效了。

注册Listener

listener/MyListener.class

package com.zhaoyi.springboot.restweb.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("contextInitialized... application start ....");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("contextDestroyed... application end ....");
    }
}

config/MyServerConfig.class

package com.zhaoyi.springboot.restweb.config;

import com.zhaoyi.springboot.restweb.filter.MyFilter;
import com.zhaoyi.springboot.restweb.listener.MyListener;
import com.zhaoyi.springboot.restweb.servlet.MyServlet;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.Arrays;

@Configuration
public class MyServerConfig {

    @Bean
    // servlet
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
        return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                factory.setPort(8085);
            }
        };
    }



    /**
     * 配置自定义Servlet组件
     * @return
     */
    @Bean
    public ServletRegistrationBean servletRegistrationBean(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(), "/myServlet");
        return registrationBean;
    }

    // Filter
    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new MyFilter());
        filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/index","/myFilter"));
        return filterFilterRegistrationBean;
    }

    // Listener
    @Bean
    public ServletListenerRegistrationBean servletListenerRegistrationBean(){
        ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean(new MyListener());
        return servletListenerRegistrationBean;
    }
}

在应用启动的时候,能够看到控制台打印

contextInitialized... application start ....

咱们点击左下角的Exit按钮,注意不是红色方块按钮退出的时候,能够看到控制台打印

contextDestroyed... application end ....

Spring Boot帮咱们自动配置SpringMVC的时候,自动的注册了SpringMVC的前端控制器,DispatcherServlet,查看DispatcherServletAutoConfiguration的源码

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.class

@Bean(
            name = {"dispatcherServletRegistration"}
        )
        @ConditionalOnBean(
            value = {DispatcherServlet.class},
            name = {"dispatcherServlet"}
        )
        public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
            DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, this.webMvcProperties.getServlet().getPath());
            // 默认拦截: / 全部请求,包括静态资源,可是不拦截JSP请求。注意/*会拦截JSP
            // 能够经过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径
            registration.setName("dispatcherServlet");
            registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
            if (this.multipartConfig != null) {
                registration.setMultipartConfig(this.multipartConfig);
            }

            return registration;
        }

使用其余的容器:Jetty(长链接)

tomcat、Undertow、Netty、Jetty。Netty应该是后来的版本加入的支持,这里就不在阐述了。咱们关注其余三个便可。

SpringBoot支持:tomcat jetty undertow,其中tomcat是默认使用的.而使用tomcat 的缘由是项目引入了web启动场景包,该场景包默认引用的就是tomcat容器,所以,假若咱们想要换成其余的容器,要在dependencies中排除默认的tomcat场景包,加入其余的包便可。

project.pom

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

进入其中,查看关联引用能够找到对应的tomcat场景引入包

spring-boot-starter-web.xxxx.pom

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.1.1.RELEASE</version>
      <scope>compile</scope>
    </dependency>

若是咱们还需继续研究下去的话,会发现当前tomcat场景启动器包所用的tomcat版本为9.x版本,比较新:

spring-boot-starter-tomcat-xxxx.pom

...

<dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-el</artifactId>
      <version>9.0.13</version>
      <scope>compile</scope>
    </dependency>
...

也就是说,若是咱们将tomcat-starter排除,而后在pom文件中引入其余的servlet容器场景包便可。

pom.xml

<dependencies>
        <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>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
        ...

解下来直接运行项目,咱们会发现,除了启动容器变成了jetty,其余的一切按正常配置运行:

Jetty started on port(s) 8085 (http/1.1) with context path '/'

undertow和jetty如出一辙的方式,直接吧jetty改成undertow就好了。继续启动:

Undertow started on port(s) 8085 (http) with context path ''

接下来咱们分析spring boot的内在工做原理,在此以前,别忘了换回tomcat做为内嵌容器。

要换回tomcat容器,只需将排除代码块<exclusions>以及其余内嵌容器场景包引入代码删除便可。

在这里仍是推荐学习一下maven相关知识,推荐书籍 Maven实战

嵌入容器配置原理

org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration.class

@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties({ServerProperties.class})
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
    public EmbeddedWebServerFactoryCustomizerAutoConfiguration() {
    }

    @Configuration
    @ConditionalOnClass({HttpServer.class})
    public static class NettyWebServerFactoryCustomizerConfiguration {
        public NettyWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
            return new NettyWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration
    @ConditionalOnClass({Undertow.class, SslClientAuthMode.class})
    public static class UndertowWebServerFactoryCustomizerConfiguration {
        public UndertowWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
            return new UndertowWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration
    @ConditionalOnClass({Server.class, Loader.class, WebAppContext.class})
    public static class JettyWebServerFactoryCustomizerConfiguration {
        public JettyWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
            return new JettyWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration
    @ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
    public static class TomcatWebServerFactoryCustomizerConfiguration {
        public TomcatWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
        }
    }
}

从其中就能够看出,该组件注册各个嵌入式Servlet容器的时候,会根据当前对应的某个class是否位于类路径上,才会实例化一个Bean,也就是说,咱们导入不一样的包,则会致使这里根据咱们导入的包生成对应的xxxxWebServerFactoryCustomizer组件。这些组件在一样的路径下定义了具体的信息

  • JettyWebServerFactoryCustomizer
  • NettyWebServerFactoryCustomizer
  • TomcatWebServerFactoryCustomizer
  • UndertowWebServerFactoryCustomizer

咱们以嵌入式的tomcat容器工厂TomcatWebServerFactoryCustomizer为例进行分析,当咱们引入了tomcat场景启动包后,springboot就会为咱们注册该组件。咱们查看其源码:

org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer

public class TomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered {
...
}

该工厂类配置了tomcat的基本环境。其中:

public TomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
        this.environment = environment;
        this.serverProperties = serverProperties;
    }

传入了咱们提供的环境信息以及服务器配置信息。

修改配置

咱们以前讲过能够经过WebServerFactoryCustomizer这个定制器帮咱们修改容器的配置。如今咱们能够看到也能够经过修改ServerProperties.想要定制servlet容器,给容器中添加一个WebServerFactoryCustomizer类型的组件就能够了。

步骤:

  1. SpringBoot根据导入的依赖状况给容器中添加相应的嵌入式容器工厂,好比WebServerFactoryCustomizer
  2. 容器中某个组件要建立对象就会被后置处理器WebServerFactoryCustomizerBeanPostProcessor就会工做。(这里版本不同,有点难以理解。)
  3. 后置处理器,从容器中获取全部的WebServerFactory类型的Factory,例如咱们以前配置的ConfigurableWebServerFactory,调用定制器的定制方法。

嵌入容器启动原理

何时建立嵌入式的servlet容器工厂? 何时获取嵌入式的Servlet容器并启动tomcat?

如下过程可经过断点慢慢查看。

  1. SpringBoot应用运行run方法;
  2. SpringBott刷新Ioc容器,即建立Ioc容器对象并初始化容器,包括建立咱们容器中的每个组件;根据不一样的环境(是web环境吗)建立不一样的容器。
  3. 刷新2中建立好的容器(进行了不少步刷新)
  4. web ioc容器会建立嵌入式的Servlet容器:createEmbeddedServletContainer().
  5. 获取嵌入式的Servlet容器工厂,接下来就是从ioc容器中获取咱们以前配置的哪种类型的组件,后置处理器一看是这个对象,就获取全部的定制器来先定制servlet容器的相关配置;
  6. 就进入到咱们以前分析的流程;
  7. 利用容器工厂获取嵌入式的Servlet容器;
  8. 启动serlvet容器;
  9. 以上步骤仅仅是onRefresh()方法而已,先启动嵌入式的servlet容器(tomcat),而后才将ioc容器中剩下的没有建立出的对象获取出来;

一句话,ioc容器启动时,会建立嵌入式的servlet容器(tomcat).

相关文章
相关标签/搜索