1. 程式人生 > 其它 >Spring Boot如何使用內嵌式的Tomcat和Jetty?

Spring Boot如何使用內嵌式的Tomcat和Jetty?

為了方便開發和部署,Spring Boot 在內部啟動了一個嵌入式的 Web 容器。我們知道 Tomcat 和 Jetty 是元件化的設計,要啟動 Tomcat 或者 Jetty 其實就是啟動這些元件。在 Tomcat 獨立部署的模式下,我們通過 startup 指令碼來啟動 Tomcat,Tomcat 中的 Bootstrap 和 Catalina 會負責初始化類載入器,並解析server.xml和啟動這些元件。

在內嵌式的模式下,Bootstrap 和 Catalina 的工作就由 Spring Boot 來做了,Spring Boot 呼叫了 Tomcat 和 Jetty 的 API 來啟動這些元件

。那 Spring Boot 具體是怎麼做的呢?而作為程式設計師,我們如何向 Spring Boot 中的 Tomcat 註冊 Servlet 或者 Filter 呢?我們又如何定製內嵌式的 Tomcat?今天我們就來聊聊這些話題。

Spring Boot 中 Web 容器相關的介面

既然要支援多種 Web 容器,Spring Boot 對內嵌式 Web 容器進行了抽象,定義了 WebServer 介面:

public interface WebServer {
    void start() throws WebServerException;
    void stop() throws WebServerException;
    int getPort();
}

各種 Web 容器比如 Tomcat 和 Jetty 需要去實現這個介面。

Spring Boot 還定義了一個工廠 ServletWebServerFactory 來建立 Web 容器,返回的物件就是上面提到的 WebServer。

public interface ServletWebServerFactory {
    WebServer getWebServer(ServletContextInitializer... initializers);
}

可以看到 getWebServer 有個引數,型別是 ServletContextInitializer。它表示 ServletContext 的初始化器,用於 ServletContext 中的一些配置:

public interface ServletContextInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}

這裡請注意,上面提到的 getWebServer 方法會呼叫 ServletContextInitializer 的 onStartup 方法,也就是說如果你想在 Servlet 容器啟動時做一些事情,比如註冊你自己的 Servlet,可以實現一個 ServletContextInitializer,在 Web 容器啟動時,Spring Boot 會把所有實現了 ServletContextInitializer 介面的類收集起來,統一調它們的 onStartup 方法

為了支援對內嵌式 Web 容器的定製化,Spring Boot 還定義了 WebServerFactoryCustomizerBeanPostProcessor 介面,它是一個 BeanPostProcessor,它在 postProcessBeforeInitialization 過程中去尋找 Spring 容器中 WebServerFactoryCustomizer 型別的 Bean,並依次呼叫 WebServerFactoryCustomizer 介面的 customize 方法做一些定製化。

public interface WebServerFactoryCustomizer<T extends WebServerFactory> {
    void customize(T factory);
}

內嵌式 Web 容器的建立和啟動

鋪墊了這些介面,我們再來看看 Spring Boot 是如何例項化和啟動一個 Web 容器的。我們知道,Spring 的核心是一個 ApplicationContext,它的抽象實現類 AbstractApplicationContext 實現了著名的 refresh 方法,它用來新建或者重新整理一個 ApplicationContext,在 refresh 方法中會呼叫 onRefresh 方法,AbstractApplicationContext 的子類可以重寫這個 onRefresh 方法,來實現特定 Context 的重新整理邏輯,因此 ServletWebServerApplicationContext 就是通過重寫 onRefresh 方法來建立內嵌式的 Web 容器,具體建立過程是這樣的:


@Override
protected void onRefresh() {
     super.onRefresh();
     try {
        //重寫onRefresh方法,呼叫createWebServer建立和啟動Tomcat
        createWebServer();
     }
     catch (Throwable ex) {
     }
}

//createWebServer的具體實現
private void createWebServer() {
    //這裡WebServer是Spring Boot抽象出來的介面,具體實現類就是不同的Web容器
    WebServer webServer = this.webServer;
    ServletContext servletContext = this.getServletContext();
    
    //如果Web容器還沒建立
    if (webServer == null && servletContext == null) {
        //通過Web容器工廠來建立
        ServletWebServerFactory factory = this.getWebServerFactory();
        //注意傳入了一個"SelfInitializer"
        this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
        
    } else if (servletContext != null) {
        try {
            this.getSelfInitializer().onStartup(servletContext);
        } catch (ServletException var4) {
          ...
        }
    }

    this.initPropertySources();
}

再來看看 getWebServer 具體做了什麼,以 Tomcat 為例,主要呼叫 Tomcat 的 API 去建立各種元件:


public WebServer getWebServer(ServletContextInitializer... initializers) {
    //1.例項化一個Tomcat,可以理解為Server元件。
    Tomcat tomcat = new Tomcat();
    
    //2. 建立一個臨時目錄
    File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    
    //3.初始化各種元件
    Connector connector = new Connector(this.protocol);
    tomcat.getService().addConnector(connector);
    this.customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    this.configureEngine(tomcat.getEngine());
    
    //4. 建立定製版的"Context"元件。
    this.prepareContext(tomcat.getHost(), initializers);
    return this.getTomcatWebServer(tomcat);
}

可能好奇 prepareContext 方法是做什麼的呢?這裡的 Context 是指 Tomcat 中的 Context 元件,為了方便控制 Context 元件的行為,Spring Boot 定義了自己的 TomcatEmbeddedContext,它擴充套件了 Tomcat 的 StandardContext:

class TomcatEmbeddedContext extends StandardContext {}

註冊 Servlet 的三種方式

1. Servlet 註解

在 Spring Boot 啟動類上加上 @ServletComponentScan 註解後,使用 @WebServlet、@WebFilter、@WebListener 標記的 Servlet、Filter、Listener 就可以自動註冊到 Servlet 容器中,無需其他程式碼,我們通過下面的程式碼示例來理解一下。

@SpringBootApplication
@ServletComponentScan
public class xxxApplication
{}
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {}

在 Web 應用的入口類上加上 @ServletComponentScan,並且在 Servlet 類上加上 @WebServlet,這樣 Spring Boot 會負責將 Servlet 註冊到內嵌的 Tomcat 中。

2. ServletRegistrationBean

同時 Spring Boot 也提供了 ServletRegistrationBean、FilterRegistrationBean 和 ServletListenerRegistrationBean 這三個類分別用來註冊 Servlet、Filter、Listener。假如要註冊一個 Servlet,可以這樣做:

@Bean
public ServletRegistrationBean servletRegistrationBean() {
    return new ServletRegistrationBean(new HelloServlet(),"/hello");
}

3. 動態註冊

你還可以建立一個類去實現前面提到的 ServletContextInitializer 介面,並把它註冊為一個 Bean,Spring Boot 會負責呼叫這個介面的 onStartup 方法。

@Component
public class MyServletRegister implements ServletContextInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {
    
        //Servlet 3.0規範新的API
        ServletRegistration myServlet = servletContext
                .addServlet("HelloServlet", HelloServlet.class);
                
        myServlet.addMapping("/hello");
        
        myServlet.setInitParameter("name", "Hello Servlet");
    }

}

這裡請注意兩點:

  • ServletRegistrationBean 其實也是通過 ServletContextInitializer 來實現的,它實現了 ServletContextInitializer 介面。

  • 注意到 onStartup 方法的引數是我們熟悉的 ServletContext,可以通過呼叫它的 addServlet 方法來動態註冊新的 Servlet,這是 Servlet 3.0 以後才有的功能。

Web 容器的定製

我們再來考慮一個問題,那就是如何在 Spring Boot 中定製 Web 容器。在 Spring Boot 2.0 中,我們可以通過兩種方式來定製 Web 容器。

第一種方式是通過通用的 Web 容器工廠 ConfigurableServletWebServerFactory,來定製一些 Web 容器通用的引數:

@Component
public class MyGeneralCustomizer implements
  WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
  
    public void customize(ConfigurableServletWebServerFactory factory) {
        factory.setPort(8081);
        factory.setContextPath("/hello");
     }
}

第二種方式是通過特定 Web 容器的工廠比如 TomcatServletWebServerFactory 來進一步定製。下面的例子裡,我們給 Tomcat 增加一個 Valve,這個 Valve 的功能是向請求頭裡新增 traceid,用於分散式追蹤。TraceValve 的定義如下:

class TraceValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {

        request.getCoyoteRequest().getMimeHeaders().
        addValue("traceid").setString("1234xxxxabcd");

        Valve next = getNext();
        if (null == next) {
            return;
        }

        next.invoke(request, response);
    }

}

跟第一種方式類似,再新增一個定製器,程式碼如下:


@Component
public class MyTomcatCustomizer implements
        WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.setPort(8081);
        factory.setContextPath("/hello");
        factory.addEngineValves(new TraceValve() );

    }
}