【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).

相關文章
相關標籤/搜索