В комментариях к заметке Пишем простой RESTful веб-сервис на Spring Web MVC прозвучал довольно интересный вопрос, суть которого сводится к следующему: как сервер приложений находит все классы, реализующие интерфейс javax.servlet.ServletContainerInitializer, и сколько времени это занимает. Попробуем разобраться.
Часть 8 спецификации Servlet 3.0 вводит понятие "подключаемой возможности" ("plugability features"), что существенно упрощает структуру веб-приложения, а так же подключение дополнительных фреймворков. Однако, данные возможности требуют времени на сканирование JAR-архивов и файлов с классами приложения. Спецификация требует, чтобы данное сканирование производилось по-умолчанию, однако его можно частично или полностью избежать в зависимости от используемого сервера приложений.
Сканирования зависимостей и классов приложения требуют следующие возможности:
В принципе, на Stack Overflow дан довольно подробный и развернутый ответ на вопрос о том, как сервер приложений ищет классы инициализации контекста. В каталоге META-INF/services архива с библиотекой должен находиться файл javax.servlet.ServletContainerInitializer, содержащий перечисление полных имен классов, реализующих интерфейс javax.servlet.ServletContainerInitializer (по одному имени на строке). Например:
Аналогичным образом можно зарегистрировать слушатели, находящиеся непосредственно в коде самого веб-приложения, при этом файл 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 выглядит следующим образом (комментарии и импорты убраны):
Метод 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.
Если после добавления приведенного выше файла web.xml запустить Apache Tomcat с демонстрационным примером, содержащим 240 Мб библиотек, то время запуска приложения составит 41.1 - 41.3 c. Видно, что время запуска уменьшилось, но незначительно, т.к. большую часть времени занимает построение контекста Spring-приложения (если предпочитаемый вами сервер приложений позволяет настроить отложенную инициализацию приложения при первом к нему обращении пользователя, то изменение времени запуска будет более существенным). Атрибут metadata-complete не способен защитить от сканирования библиотек для поиска классов, реализующих интерфейсы или являющихся наследниками классов, переданных в качестве свойств аннотаций @HandlesTypes в SCI.
Элемент <absolute-ordering> файла web.xml определяет какие JAR-файлы с веб-фрагментами (в соответстви с именами в их файлах WEB-INF/web-fragment.xml) должны быть просканированы для поиска SCI, фрагментов и аннотаций. Если элемент <absolute-ordering> пуст, то ничего сканироваться не будет.
Так как аннотация @HandlesTypes игнорируется, то класс ApplicationInitializer рассматриваемого примера не вызывается, соответственно сервлет DispatcherServlet не регистрируется. Чтобы приложение работало, сконфигурировать сервлет нужно в дескрипторе развертывания web.xml:
Время запуска сконфигурированного таким образом приложения, содержащего 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 не добавили.
Понравилось сообщение - подпишитесь на блог
- Какие компоненты ищет сервер приложений при запуске.
- Servlet Container Initializer (CSI).
- Сколько времени занимает сканирование.
- Отключаем поиск компонентов веб-приложения.
- Отключаем поиск SCI.
- Выводы.
- Используемые материалы.
Какие компоненты ищет сервер приложений при запуске
Часть 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 - является таким компонентом.
- SCI (javax.servlet.ServletContainerInitializer);
- Старые возможности, впервые предложенные в ранних версиях спецификации:
- Tag Library Descriptor (TLD), определяет библиотеки тегов. Сервер приложений ищет файлы META-INF/**/*.tld.
- 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 не добавили.
Используемые материалы
- How do I make Tomcat startup faster?.
- How servlet container finds WebApplicationInitializer implementations.
Понравилось сообщение - подпишитесь на блог
6 комментариев:
Только маленький вопрос на счет 6Мб против 6Кб, а не связано ли это с тем, что Spring достаточно Tomcat(в смысле, контейнер сервлетов), а для чистого Java EE нужен контейнер приложений который и будет содержать те мегабайты библиотек, которые предоставляет Spring?
Ну т.е. в случае Spring Framework мы носим сервер приложений внутри каждого WAR-архива. Когда приложение одно - ок, а когда их хотя бы десяток?
Здравствуйте! Возможно узнать Вашу почту? Хотелось бы задать Вам несколько вопросов по поводу SOA.
Здравствуйте! Как имя блога до первой точки @gmail.com
Мне кажется некорректно сравнивать 6Мб против 6Кб, забывая, что куча функционала и реализаций спецификаций лежит в App-сервере. Тогда надо сравнить Tomcat+Spring против WAS+CDI.
Точка зрения заключается в том, что 6 МБ Spring Framework мы вынуждены носить с собой в каждом приложении (если не используем shared-libs, а кто их использует из любителей Spring?) и копировать на сервер при каждом развертывании для разработки, тестирования или отладки. Библиотеки же полноценного сервера приложений доступны всегда, сами приложения получаются очень маленькими, с минимумом зависимостей, соответственно разворачиваются и стартуют очень быстро.
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!