learn-tech/专栏/深入拆解TomcatJetty/28新特性:SpringBoot如何使用内嵌式的Tomcat和Jetty?.md
2024-10-16 09:22:22 +08:00

10 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        28 新特性Spring Boot如何使用内嵌式的Tomcat和Jetty
                        为了方便开发和部署Spring Boot在内部启动了一个嵌入式的Web容器。我们知道Tomcat和Jetty是组件化的设计要启动Tomcat或者Jetty其实就是启动这些组件。在Tomcat独立部署的模式下我们通过startup脚本来启动TomcatTomcat中的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 { 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中。

  1. ServletRegistrationBean

同时Spring Boot也提供了ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean这三个类分别用来注册Servlet、Filter、Listener。假如要注册一个Servlet可以这样做

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

这段代码实现的方法返回一个ServletRegistrationBean并将它当作Bean注册到Spring中因此你需要把这段代码放到Spring Boot自动扫描的目录中或者放到@Configuration标识的类中。

  1. 动态注册

你还可以创建一个类去实现前面提到的ServletContextInitializer接口并把它注册为一个BeanSpring 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 {

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 {

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

}

}

本期精华

今天我们学习了Spring Boot如何利用Web容器的API来启动Web容器、如何向Web容器注册Servlet以及如何定制化Web容器除了给Web容器配置参数还可以增加或者修改Web容器本身的组件。

课后思考

我在文章中提到通过ServletContextInitializer接口可以向Web容器注册Servlet那ServletContextInitializer跟Tomcat中的ServletContainerInitializer有什么区别和联系呢

不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。