SpringBoot Series — анализ принципов реализации Embedded Tomcat

Spring Boot Tomcat

Оригинальная ссылка:Анализ принципа реализации встроенного Tomcat в SpringBoot

Для веб-проекта SpringBoot основным флагом зависимости является starter spring-boot-starter-web Модуль spring-boot-starter-web не имеет кода в весенней загрузке, но содержится только в pom.xml Некоторые зависимости, включая веб , webmvc, tomcat и т. д.:

<dependencies>
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-json</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.hibernate.validator</groupId>
    	<artifactId>hibernate-validator</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-web</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-webmvc</artifactId>
    </dependency>
</dependencies>

Контейнер веб-службы Spring Boot по умолчанию — tomcat.Если вы хотите использовать Jetty для замены Tomcat, вы можете обратиться к официальной документации, чтобы решить эту проблему.

web, webmvc, tomcat и т. д. обеспечивают рабочую среду веб-приложений, а spring-boot-starter — это переключатель, позволяющий этим рабочим средам работать (поскольку spring-boot-starter косвенно представит spring-boot-autoconfigure ).

Автоматическая настройка веб-сервера

В модуле spring-boot-autoconfigure есть класс автоконфигурации ServletWebServerFactoryAutoConfiguration, который имеет дело с WebServer.

ServletWebServerFactoryAutoConfiguration

Фрагмент кода выглядит следующим образом:

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration

Два условия указывают, что текущая операционная среда является веб-службой, основанной на стандартной спецификации сервлета:

  • ConditionalOnClass(ServletRequest.class): указывает, что в настоящее время должна быть зависимость servlet-api.
  • ConditionalOnWebApplication(type = Type.SERVLET): только веб-приложение на основе сервлета.

@EnableConfigurationProperties(ServerProperties.class): конфигурация ServerProperties включает общие свойства конфигурации, такие как server.port.

пройти через @ImportИмпортируйте классы автоматической конфигурации, связанные со встроенным контейнером, включая EmbeddedTomcat, EmbeddedJetty и EmbeddedUndertow.

В целом, класс автоматической настройки ServletWebServerFactoryAutoConfiguration в основном выполняет следующие функции:

  • Импортируется внутренний класс BeanPostProcessorsRegistrar, который реализует ImportBeanDefinitionRegistrar, который может реализовать ImportBeanDefinitionRegistrar для регистрации дополнительных BeanDefinitions.
  • Сначала импортируется ServletWebServerFactoryConfiguration.EmbeddedTomcat и другая конфигурация встроенного контейнера (в основном мы фокусируемся на конфигурации, связанной с tomcat).
  • ServletWebServerFactoryCustomizer, TomcatServletWebServerFactoryCustomizer зарегистрированы два компонента типа WebServerFactoryCustomizer.

Ниже приводится подробный анализ этих моментов.

BeanPostProcessorsRegistrar

Код внутреннего класса BeanPostProcessorsRegistrar выглядит следующим образом (часть кода опущена):

public static class BeanPostProcessorsRegistrar
    implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
    // 省略代码
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
        if (this.beanFactory == null) {
            return;
        }
        // 注册 WebServerFactoryCustomizerBeanPostProcessor
        registerSyntheticBeanIfMissing(registry,
                                       "webServerFactoryCustomizerBeanPostProcessor",
                                       WebServerFactoryCustomizerBeanPostProcessor.class);
        // 注册 errorPageRegistrarBeanPostProcessor
        registerSyntheticBeanIfMissing(registry,
                                       "errorPageRegistrarBeanPostProcessor",
                                       ErrorPageRegistrarBeanPostProcessor.class);
    }
    // 省略代码
}

В приведенном выше коде зарегистрированы два bean-компонента, один WebServerFactoryCustomizerBeanPostProcessor и один errorPageRegistrarBeanPostProcessor; оба они реализуют BeanPostProcessor-подобный интерфейс и принадлежат постпроцессору bean-компонента, который используется для добавления некоторой логической обработки до и после инициализации Боб.

  • WebServerFactoryCustomizerBeanPostProcessor: Функция состоит в том, чтобы вызывать те WebServerFactoryCustomizer, которые вводятся вышеприведенным классом автоматической конфигурации при инициализации WebServerFactory, а затем вызывать метод настройки в WebServerFactoryCustomizer для обработки WebServerFactory.
  • errorPageRegistrarBeanPostProcessor: аналогично предыдущему, но обрабатывает ErrorPageRegistrar.

Вот краткий обзор кода в WebServerFactoryCustomizerBeanPostProcessor:

public class WebServerFactoryCustomizerBeanPostProcessor
		implements BeanPostProcessor, BeanFactoryAware {
    // 省略部分代码
    
    // 在 postProcessBeforeInitialization 方法中,如果当前 bean 是 WebServerFactory,则进行
    // 一些后置处理
    @Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		if (bean instanceof WebServerFactory) {
			postProcessBeforeInitialization((WebServerFactory) bean);
		}
		return bean;
	}
    // 这段代码就是拿到所有的 Customizers ,然后遍历调用这些 Customizers 的 customize 方法
    private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
		LambdaSafe
				.callbacks(WebServerFactoryCustomizer.class, getCustomizers(),
						webServerFactory)
				.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
				.invoke((customizer) -> customizer.customize(webServerFactory));
	}
    
    // 省略部分代码
}

Два компонента Customizer Bean, зарегистрированные в классе автоконфигурации

Эти два кастомайзера фактически обрабатывают некоторые значения конфигурации и привязывают их к своим соответствующим фабричным классам.

WebServerFactoryCustomizer

Привяжите значение конфигурации serverProperties к экземпляру объекта ConfigurableServletWebServerFactory.

@Override
public void customize(ConfigurableServletWebServerFactory factory) {
    PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
    // 端口
    map.from(this.serverProperties::getPort).to(factory::setPort);
    // address
    map.from(this.serverProperties::getAddress).to(factory::setAddress);
    // contextPath
    map.from(this.serverProperties.getServlet()::getContextPath)
        .to(factory::setContextPath);
    // displayName
    map.from(this.serverProperties.getServlet()::getApplicationDisplayName)
        .to(factory::setDisplayName);
    // session 配置
    map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
    // ssl
    map.from(this.serverProperties::getSsl).to(factory::setSsl);
    // jsp
    map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
    // 压缩配置策略实现
    map.from(this.serverProperties::getCompression).to(factory::setCompression);
    // http2 
    map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
    // serverHeader
    map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
    // contextParameters
    map.from(this.serverProperties.getServlet()::getContextParameters)
        .to(factory::setInitParameters);
}

TomcatServletWebServerFactoryCustomizer

По сравнению с приведенным выше, этот настройщик в основном имеет дело со значениями конфигурации, связанными с Tomcat.

@Override
public void customize(TomcatServletWebServerFactory factory) {
    // 拿到 tomcat 相关的配置
    ServerProperties.Tomcat tomcatProperties = this.serverProperties.getTomcat();
    // server.tomcat.additional-tld-skip-patterns
    if (!ObjectUtils.isEmpty(tomcatProperties.getAdditionalTldSkipPatterns())) {
        factory.getTldSkipPatterns()
            .addAll(tomcatProperties.getAdditionalTldSkipPatterns());
    }
    // server.redirectContextRoot
    if (tomcatProperties.getRedirectContextRoot() != null) {
        customizeRedirectContextRoot(factory,
                                     tomcatProperties.getRedirectContextRoot());
    }
    // server.useRelativeRedirects
    if (tomcatProperties.getUseRelativeRedirects() != null) {
        customizeUseRelativeRedirects(factory,
                                      tomcatProperties.getUseRelativeRedirects());
    }
}

WebServerFactory

Интерфейс маркера для фабрик, создающих веб-серверы.

архитектура класса

На приведенном выше рисунке показаны все отношения структуры классов WebServerFactory -> TomcatServletWebServerFactory.

TomcatServletWebServerFactory

TomcatServletWebServerFactory — это реализация фабричного класса для получения Tomcat в качестве веб-сервера.Основным методом является getWebServer, который получает экземпляр объекта веб-сервера.

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    // 创建一个 Tomcat 实例
    Tomcat tomcat = new Tomcat();
    // 创建一个 Tomcat 实例工作空间目录
    File baseDir = (this.baseDirectory != null) ? this.baseDirectory
        : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    // 创建连接对象
    Connector connector = new Connector(this.protocol);
    tomcat.getService().addConnector(connector);
    // 1
    customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    // 配置 Engine,没有什么实质性的操作,可忽略
    configureEngine(tomcat.getEngine());
    // 一些附加链接,默认是 0 个
    for (Connector additionalConnector : this.additionalTomcatConnectors) {
        tomcat.getService().addConnector(additionalConnector);
    }
    // 2
    prepareContext(tomcat.getHost(), initializers);
    // 返回 webServer
    return getTomcatWebServer(tomcat);
}
  • 1. CustomizeConnector: установите порт, protocolHandler, uriEncoding и т. д. для соединителя. Логика построения соединителя в основном состоит в том, чтобы выбрать протокол в выборе NIO и APR, а затем подумать о создании экземпляра и принудительно использовать его в ProtocolHandler.
  • 2. prepareContext Это означает не подготовку контекстной информации текущей операционной среды Tomcat, а подготовку StandardContext, то есть подготовку веб-приложения.

Подготовьте контейнер контекста веб-приложения

Для Tomcat каждый контекст сопоставляется с веб-приложением, поэтому prepareContext сопоставляет веб-приложение с TomcatEmbeddedContext, а затем добавляет его в Host.

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
    File documentRoot = getValidDocumentRoot();
    // 创建一个 TomcatEmbeddedContext 对象
    TomcatEmbeddedContext context = new TomcatEmbeddedContext();
    if (documentRoot != null) {
        context.setResources(new LoaderHidingResourceRoot(context));
    }
    // 设置描述此容器的名称字符串。在属于特定父项的子容器集内,容器名称必须唯一。
    context.setName(getContextPath());
    // 设置此Web应用程序的显示名称。
    context.setDisplayName(getDisplayName());
    // 设置 webContextPath  默认是   /
    context.setPath(getContextPath());
    File docBase = (documentRoot != null) ? documentRoot
        : createTempDir("tomcat-docbase");
    context.setDocBase(docBase.getAbsolutePath());
    // 注册一个FixContextListener监听,这个监听用于设置context的配置状态以及是否加入登录验证的逻辑
    context.addLifecycleListener(new FixContextListener());
    // 设置 父 ClassLoader
    context.setParentClassLoader(
        (this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
        : ClassUtils.getDefaultClassLoader());
    // 覆盖Tomcat的默认语言环境映射以与其他服务器对齐。
    resetDefaultLocaleMapping(context);
    // 添加区域设置编码映射(请参阅Servlet规范2.4的5.4节)
    addLocaleMappings(context);
    // 设置是否使用相对地址重定向
    context.setUseRelativeRedirects(false);
    try {
        context.setCreateUploadTargets(true);
    }
    catch (NoSuchMethodError ex) {
        // Tomcat is < 8.5.39. Continue.
    }
    configureTldSkipPatterns(context);
    // 设置 WebappLoader ,并且将 父 classLoader 作为构建参数
    WebappLoader loader = new WebappLoader(context.getParentClassLoader());
    // 设置 WebappLoader 的 loaderClass 值
    loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
    // 会将加载类向上委托
    loader.setDelegate(true);
    context.setLoader(loader);
    if (isRegisterDefaultServlet()) {
        addDefaultServlet(context);
    }
    // 是否注册 jspServlet
    if (shouldRegisterJspServlet()) {
        addJspServlet(context);
        addJasperInitializer(context);
    }
    context.addLifecycleListener(new StaticResourceConfigurer(context));
    ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
    // 在 host 中 加入一个 context 容器
    // add时给context注册了个内存泄漏跟踪的监听MemoryLeakTrackingListener,详见 addChild 方法
    host.addChild(context);
    //对context做了些设置工作,包括TomcatStarter(实例化并set给context),
    // LifecycleListener,contextValue,errorpage,Mime,session超时持久化等以及一些自定义工作
    configureContext(context, initializersToUse);
    // postProcessContext 方法是空的,留给子类重写用的
    postProcessContext(context);
}

Как видно из вышеизложенного, WebappLoader может изменить значение loaderClass с помощью двух методов setLoaderClass и getLoaderClass. Таким образом, это означает, что мы можем сами определить класс, который наследует webappClassLoader, чтобы заменить реализацию по умолчанию, поставляемую с системой.

Инициализировать TomcatWebServer

В конце метода getWebServer нужно построить TomcatWebServer.

// org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
    // new 一个 TomcatWebServer
    return new TomcatWebServer(tomcat, getPort() >= 0);
}
// org.springframework.boot.web.embedded.tomcat.TomcatWebServer
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
    Assert.notNull(tomcat, "Tomcat Server must not be null");
    this.tomcat = tomcat;
    this.autoStart = autoStart;
    // 初始化
    initialize();
}

Вот в основном метод инициализации, который запустит службу tomcat

private void initialize() throws WebServerException {
    logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
    synchronized (this.monitor) {
        try {
            // 对全局原子变量 containerCounter+1,由于初始值是-1,
    // 所以 addInstanceIdToEngineName 方法内后续的获取引擎并设置名字的逻辑不会执行
            addInstanceIdToEngineName();
			// 获取 Context 
            Context context = findContext();
            // 给 Context 对象实例生命周期监听器
            context.addLifecycleListener((event) -> {
                if (context.equals(event.getSource())
                    && Lifecycle.START_EVENT.equals(event.getType())) {
                    // 将上面new的connection以service(这里是StandardService[Tomcat])做key保存到
                    // serviceConnectors中,并将 StandardService 中的connectors 与 service 解绑(connector.setService((Service)null);),
                    // 解绑后下面利用LifecycleBase启动容器就不会启动到Connector了
                    removeServiceConnectors();
                }
            });
            // 启动服务器以触发初始化监听器
            this.tomcat.start();
            // 这个方法检查初始化过程中的异常,如果有直接在主线程抛出,
            // 检查方法是TomcatStarter中的 startUpException,这个值是在 Context 启动过程中记录的
            rethrowDeferredStartupExceptions();
            try {
                // 绑定命名的上下文和classloader,
                ContextBindings.bindClassLoader(context, context.getNamingToken(),
                                                getClass().getClassLoader());
            }
            catch (NamingException ex) {
                // 设置失败不需要关心
            }

			// :与Jetty不同,Tomcat所有的线程都是守护线程,所以创建一个非守护线程
            // (例:Thread[container-0,5,main])来避免服务到这就shutdown了
            startDaemonAwaitThread();
        }
        catch (Exception ex) {
            stopSilently();
            throw new WebServerException("Unable to start embedded Tomcat", ex);
        }
    }
}

Поиск контекста — это фактически поиск веб-приложения в Tomcat. хост-контейнера, если тип контейнера — контекст, он будет возвращен напрямую.

private Context findContext() {
    for (Container child : this.tomcat.getHost().findChildren()) {
        if (child instanceof Context) {
            return (Context) child;
        }
    }
    throw new IllegalStateException("The host does not contain a Context");
}

Процесс запуска Tomcat

Запуск Tomcat выполняется в методе инициализации TomcatWebServer.

// Start the server to trigger initialization listeners
this.tomcat.start();

Метод запуска org.apache.catalina.startup.Tomcat:

public void start() throws LifecycleException {
    // 初始化 server
    getServer();
    // 启动 server
    server.start();
}

Инициализировать сервер

Инициализация сервера фактически создает экземпляр объекта StandardServer.Для сервера в Tomcat см. инструкции в приложении.

public Server getServer() {
	// 如果已经存在的话就直接返回
    if (server != null) {
        return server;
    }
	// 设置系统属性 catalina.useNaming
    System.setProperty("catalina.useNaming", "false");
	// 直接 new 一个 StandardServer
    server = new StandardServer();
	// 初始化 baseDir (catalina.base、catalina.home、 ~/tomcat.{port})
    initBaseDir();

    // Set configuration source
    ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));

    server.setPort( -1 );

    Service service = new StandardService();
    service.setName("Tomcat");
    server.addService(service);
    return server;
}

резюме

Процесс встраивания Tomcat в SpringBoot разобран выше, процесс этот на самом деле не сложный, то есть в процессе обновления контекста Spring запускается контейнер Tomcat, и текущее приложение привязывается к Context, а затем к Host добавляется. . На следующем рисунке показан стек выполнения программы и время инициализации и запуска встроенного Tomcat.

Весь процесс кратко описан ниже:

  • Зарегистрируйте связанные Bean-компоненты с помощью пользовательской конфигурации, включая некоторые фабрики и постпроцессоры и т. д.
  • На этапе обновления контекста выполните создание WebServer, где необходимо использовать Bean, зарегистрированный на предыдущем этапе.
    • В том числе создание ServletContext
    • создать экземпляр веб-сервера
  • Создайте экземпляр Tomcat, создайте коннектор Connector
  • Привязка применяется к ServletContext, добавляются связанные слушатели в рамках жизненного цикла, а затем к хосту добавляется контекст.
  • Создайте экземпляр веб-сервера и запустите службу Tomcat

Метод Fatjar SpringBoot не обеспечивает логику реализации совместного использования Tomcat, то есть два запуска FATJAT могут создавать только один экземпляр Tomcat (включая Connector и Host).Из предыдущего анализа мы знаем, что каждое веб-приложение (соответствующее приложение FATJAT ) Экземпляр Сопоставляется с Контекстом; для режима войны несколько Контекстов могут быть смонтированы под Хостом.

Приложение: Описание компонента Tomcat

имя компонента инструкция
Server Представляет весь контейнер сервлета, поэтому в среде выполнения Tomcat имеется только один экземпляр сервера.
Service Служба представляет собой набор из одного или нескольких соединителей, которые совместно используют один и тот же контейнер для обработки своих запросов. В одном экземпляре Tomcat может содержаться любое количество экземпляров Service, они независимы друг от друга.
Connector Коннектор Tomcat используется для мониторинга и преобразования запросов Socket, а также для передачи запросов чтения Socket в контейнер для обработки.Он поддерживает разные протоколы и разные методы ввода-вывода.
Container Контейнер представляет собой класс объектов, которые могут выполнять клиентские запросы и возвращать ответы.В Tomcat существуют разные уровни контейнеров: Engine, Host, Context, Wrapper.
Engine Engine представляет весь механизм сервлета. В Tomcat Engine является объектом контейнера самого высокого уровня.Хотя Engine не является контейнером, который непосредственно обрабатывает запросы, он действительно является точкой входа для получения целевого контейнера.
Host Как тип контейнера Host представляет виртуальную машину в механизме сервлета (т.е. Engine) и связан с сетевым именем сервера, таким как доменное имя. Клиенты могут использовать это сетевое имя для подключения к серверу, это имя должно быть зарегистрировано на DNS-сервере.
Context Контекст — это тип контейнера, используемый для представления ServletContext.В спецификации сервлета ServletContext представляет собой независимое веб-приложение.
Wrapper Обертка — это контейнер для представления сервлетов, определенных в веб-приложениях.
Executor Представляет пул потоков, который может совместно использоваться компонентами Tomcat.