понедельник, 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?

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

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

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

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

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

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

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

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

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

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

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

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