понедельник, 25 января 2016 г.

Spring Framework: влияние сканирования зависимостей на время запуска веб-приложения

В комментариях к заметке Пишем простой RESTful веб-сервис на Spring Web MVC прозвучал довольно интересный вопрос, суть которого сводится к следующему: как сервер приложений находит все классы, реализующие интерфейс javax.servlet.ServletContainerInitializer, и сколько времени это занимает. Попробуем разобраться.


Какие компоненты ищет сервер приложений при запуске


Часть 8 спецификации Servlet 3.0 вводит понятие "подключаемой возможности" ("plugability features"), что существенно упрощает структуру веб-приложения, а так же подключение дополнительных фреймворков. Однако, данные возможности требуют времени на сканирование JAR-архивов и файлов с классами приложения. Спецификация требует, чтобы данное сканирование производилось по-умолчанию, однако его можно частично или полностью избежать в зависимости от используемого сервера приложений.

Сканирования зависимостей и классов приложения требуют следующие возможности:

  • Впервые предложенные в спецификации Servlet 3.0:

    • SCI (javax.servlet.ServletContainerInitializer);

    • Веб-фрагменты (META-INF/web-fragment.xml);

    • Ресурсы веб-приложения, собранные в JAR-файлах (META-INF/resources/*);

    • Аннотации, определяющие компоненты веб-приложения (@WebServlet и т.д.);

    • Аннотации, определяющие компоненты для сторонних библиотек, инициализируемые с помощью SCI (аннотации, которые определены в аннотации @HandlesTypes на SCI-классе. Класс конфигурации контекста приложения Spring Framework из примера - ApplicationInitializer - является таким компонентом.

  • Старые возможности, впервые предложенные в ранних версиях спецификации:

    • Tag Library Descriptor (TLD), определяет библиотеки тегов. Сервер приложений ищет файлы META-INF/**/*.tld.

Servlet Container Initializer (CSI)


В принципе, на Stack Overflow дан довольно подробный и развернутый ответ на вопрос о том, как сервер приложений ищет классы инициализации контекста. В каталоге META-INF/services архива с библиотекой должен находиться файл javax.servlet.ServletContainerInitializer, содержащий перечисление полных имен классов, реализующих интерфейс javax.servlet.ServletContainerInitializer (по одному имени на строке). Например:

psamolysov.demo.spring.restws.ServletContextInitializer1
psamolysov.demo.spring.restws.ServletContextInitializer2

Аналогичным образом можно зарегистрировать слушатели, находящиеся непосредственно в коде самого веб-приложения, при этом файл javax.servlet.ServletContainerInitializer должен находиться в каталоге WEB-INF/classes/META-INF/services.


Класс, реализующий интерфейс javax.servlet.ServletContainerInitializer, может быть аннотирован с помощью @javax.servlet.annotation.HandlesTypes. В данной аннотации передается массив классов и интерфейсов, которые сами по себе или их наследники и реализации будут переданы в качестве первого аргумента метода #onStartup(Set<Class<?>> c, ServletContext ctx класса, который ею аннотирован. Например, код класса SpringServletContainerInitializer из Spring Framework выглядит следующим образом (комментарии и импорты убраны):

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

 @Override
 public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
   throws ServletException {

  List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

  if (webAppInitializerClasses != null) {
   for (Class<?> waiClass : webAppInitializerClasses) {
    // Be defensive: Some servlet containers provide us with invalid classes,
    // no matter what @HandlesTypes says...
    if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
      WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
     try {
      initializers.add((WebApplicationInitializer) waiClass.newInstance());
     }
     catch (Throwable ex) {
      throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
     }
    }
   }
  }

  if (initializers.isEmpty()) {
   servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
   return;
  }

  AnnotationAwareOrderComparator.sort(initializers);
  servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);

  for (WebApplicationInitializer initializer : initializers) {
   initializer.onStartup(servletContext);
  }
 }

}

Метод onStartup() данного класса получает множество классов, реализующих интерфейс WebApplicationInitializer, именно данный интерфейс присутствует в качестве свойства аннотации @HandlesTypes, которой отмечен класс SpringServletContainerInitializer. Логика метода onStartup() довольно проста: сначала из всех переданных значений выбираются только те, которые не являются интерфейсами и абстрактными классами, создаются их экземпляры и складываются в список. Затем полученный список сортируется по порядку, заданному аннотациями @org.springframework.core.annotation.Order и javax.annotation.Priority. Инициализация контекста делегируется элементам списка.

Сколько времени занимает сканирование


Измерим время запуска сервера приложений Apache Tomcat с простым примером использования Spring Framework.

Обсуждаемый пример требует 7 Мб библиотек, при этом время запуска приложения составляет 4.3 - 4.6 с. на моем Lenovo T440 с i5-4300U @ 1.90 GHz.

Если добавить в каталог WEB-INF/lib еще 240 Мб зависимостей, например из Eclipse SDK, чтобы в данном каталоге было 700 JAR-файлов, то приложение будет запускаться за 43 - 44 с..

Отключаем поиск компонентов веб-приложения


Поиск компонентов веб-приложения (например, сервлетов, аннотированых @WebServlet) можно отключить, что особо актуально для приложений, построенных с использованием фреймворков подобных Spring Web MVC или JSF, т.к. данные фреймворки предлагают единственный сервлет-контроллер, через который должно проходить все взаимодействие с пользователем. Для этого потребуется файл web.xml, атрибут metadata-complete тега web-app данного файла должен быть выставлен в true.

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1" metadata-complete="true">

</web-app>

Если после добавления приведенного выше файла web.xml запустить Apache Tomcat с демонстрационным примером, содержащим 240 Мб библиотек, то время запуска приложения составит 41.1 - 41.3 c. Видно, что время запуска уменьшилось, но незначительно, т.к. большую часть времени занимает построение контекста Spring-приложения (если предпочитаемый вами сервер приложений позволяет настроить отложенную инициализацию приложения при первом к нему обращении пользователя, то изменение времени запуска будет более существенным). Атрибут metadata-complete не способен защитить от сканирования библиотек для поиска классов, реализующих интерфейсы или являющихся наследниками классов, переданных в качестве свойств аннотаций @HandlesTypes в SCI.

Отключаем поиск SCI


Элемент <absolute-ordering> файла web.xml определяет какие JAR-файлы с веб-фрагментами (в соответстви с именами в их файлах WEB-INF/web-fragment.xml) должны быть просканированы для поиска SCI, фрагментов и аннотаций. Если элемент <absolute-ordering> пуст, то ничего сканироваться не будет.

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1" metadata-complete="true">

    <absolute-ordering /> 
</web-app>

Так как аннотация @HandlesTypes игнорируется, то класс ApplicationInitializer рассматриваемого примера не вызывается, соответственно сервлет DispatcherServlet не регистрируется. Чтобы приложение работало, сконфигурировать сервлет нужно в дескрипторе развертывания web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1" metadata-complete="true">

    <absolute-ordering />
    
    <servlet>
        <servlet-name>dispatcher-servlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>        
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                psamolysov.demo.spring.restws.ApplicationConfig
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher-servlet</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping> 
</web-app>

Время запуска сконфигурированного таким образом приложения, содержащего 240 Мб библиотек, составляет 9.1 - 9.4 с. Разница колоссальная! Если удалить лишние библиотеки и оставить только то, что действительно необходимо приложению, сохранив при этом приведенный выше web.xml, то время запуска приложения снижается до 3.1 - 3.2 c. против 4.3 - 4.6 с. для приложения, сконфигурированного с помощью аннотаций.

Выводы


Из проведенного мини-исследования можно сделать следующие, довольно таки очевидные выводы. Если ваше приложение небольшое, содержит минимум зависимостей, то использование современных методик является предпочтительным выбором. С помощью SCI можно строить расширяемые модульные приложения при этом не используя XML для их настройки, вся метаинформация может храниться в коде самих приложений. Впрочем, я признаю, что это - вопрос спорный и в некоторых случаях конфигурация, описанная с помощью XML может быть более наглядной.

Однако, если приложение разрослось, то слишком много времени начинает тратиться на сканирование зависимостей с целью поиска метаинформации. С одной стороны приложения, особенно в продуктивной среде, не должны перезапускаться даже каждый месяц, не то что по нескольку раз в день, но с другой, при внедрении современных практик разработки и тестирования, приложения должны запускаться практически мгновенно с целью экономии времени и, возможно это даже более важно, мотивации дорогостоящих специалистов. Мало кто обладает складом характера, позволяющем ему сидеть и смотреть несколько минут в черный экран, пока приложение стартует, скорее всего человек отвлечется на Хабрахабр или, например, Блог Сурового, и вернется к работе гораздо позже, нежели приложение запустится.

Не могу не заметить так же, что избавлением от головной боли как с зависимостями вообще, так и с их непомерным разрастанием является максимальное использование Java EE. Для сравнения, даже рассматриваемый в данной статье маленький пример, реализованный с помощью CDI занимает 6 Кб, в то время как версия, основанная на Spring Framework, - 6 Мб или в 1000 раз больше. А ведь мы даже Hibernate не добавили.

Используемые материалы



Понравилось сообщение - подпишитесь на блог

6 комментариев:

Unknown комментирует...

Только маленький вопрос на счет 6Мб против 6Кб, а не связано ли это с тем, что Spring достаточно Tomcat(в смысле, контейнер сервлетов), а для чистого Java EE нужен контейнер приложений который и будет содержать те мегабайты библиотек, которые предоставляет Spring?

Pavel Samolysov комментирует...

Ну т.е. в случае Spring Framework мы носим сервер приложений внутри каждого WAR-архива. Когда приложение одно - ок, а когда их хотя бы десяток?

Unknown комментирует...

Здравствуйте! Возможно узнать Вашу почту? Хотелось бы задать Вам несколько вопросов по поводу SOA.

Pavel Samolysov комментирует...

Здравствуйте! Как имя блога до первой точки @gmail.com

Unknown комментирует...

Мне кажется некорректно сравнивать 6Мб против 6Кб, забывая, что куча функционала и реализаций спецификаций лежит в App-сервере. Тогда надо сравнить Tomcat+Spring против WAS+CDI.

Pavel Samolysov комментирует...

Точка зрения заключается в том, что 6 МБ Spring Framework мы вынуждены носить с собой в каждом приложении (если не используем shared-libs, а кто их использует из любителей Spring?) и копировать на сервер при каждом развертывании для разработки, тестирования или отладки. Библиотеки же полноценного сервера приложений доступны всегда, сами приложения получаются очень маленькими, с минимумом зависимостей, соответственно разворачиваются и стартуют очень быстро.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!