my.life.logging.Blog

Конфигурация приложения Spring MVC (почти) без использования XML

Доброе время суток, господа джависты!

Думаю, я не буду далек от истины, если скажу, что XML сейчас – это главный бич нашего с вами программистского Java поколения!

Что уж там говорить, когда все эти открывающие и закрывающие XML-теги, спецсимволы и “verbosity” даже больше, чем у Java, могут любого, даже самого уравновешенного кодера, довести до белого каления. А отсутствие какой бы-то ни было полноты по Тьюрингу и возможности отладки в XML часто заставляет плакать от безысходности при написании даже самых простых скриптов Ant. А в скольких разработческих стрессах, испорченном настроении и отсутствии иммунитета виноват проклятый XML! И я даже слышал об одном бедном программисте, который чуть было не повесился после того, как ему поручили написать код трансформации документа XML в документ XHTML с помощью XSLT-преобразования, с обязательной проверкой входного документа при помощи XSD-схемы!..

А что выбираешь ты?

Но время проходит, тучи рассеиваются…

Многие Java технологии начинают осознавать пагубные последствия своего пристрастия к XML, и меняются в лучшую сторону. Так, в последних версиях Spring Framework и Servlet API 3.0 было сделано очень много подвижек в сторону отказа от XML. Так что теперь мы можем даже говорить о возможности конфигурирования приложения Spring MVC почти без использования XML. Как осуществить подобную конфигурацию на практике, мы сейчас и рассмотрим.

Ищущие спасения от цепких пут коварного XML, да обрещут его!

А если серьезно, то познакомившись с программной конфигурацией Spring MVC я нашел ее более естественной, лаконичной и уж точно имеющей право на существование наряду с конфигурацией Spring при помощи XML файлов.

Проектные зависимости

К великому сожалению, начать придется все-таки с еще одного использования XML. Дело в том, что нам нужно описать зависимости нашего проекта. Обычно для этого используют Maven, который, как вам должно быть известно, погряз в XML более, чем полностью.

Итак, в нашем примере будут использоваться:

  • Servlet API 3.0.1
  • Spring Framework 3.2.2

Соответствующие разделы в pom.xml выглядят следующим образом:

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  <!-- ... -->

  <properties>
      <spring.version>3.2.2.RELEASE</spring.version>
  </properties>

  <dependencies>
      <!-- Зависимость от Servlet API 3.0 -->
      <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.0.1</version>
          <scope>provided</scope>
      </dependency>
  
      <!-- Зависимости Spring 3 -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-core</artifactId>
          <version>${spring.version}</version>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
          <version>${spring.version}</version>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>${spring.version}</version>
      </dependency>
  </dependencies>

  <!-- ... -->

Хорошая новость состоит в том, что уже сейчас существуют вполне сложившиеся технологии для автоматизации сборки приложений, свободные от XML. Но разберемся с этим как-нибудь в другой раз. Проигран бой, но не война! Лучше посмотрим, что все-таки предлагает нам Spring.

XML-независимый Spring

В это трудно поверить, но спецификация сервлетов версии 3.0 позволяет нам отказаться1 от использования в приложении казавшегося до этого вечным файла-дескриптора web.xml. Cделать это можно следующим образом:

  • Создать в приложении класс, реализующий интерфейс javax.servlet.ServletContainerInitializer. В методе onStartup(Set<Class<?>>, ServletContext) этого класса с помощью Java-кода можно описать все сервлеты, фильтры и листенеры, существующие в приложении.
  • Не забыть указать в файле META-INF/services/javax.servlet.ServletContainerInitializer полное имя класса-реализации ServletContainerInitializer.

Улучшениями в спецификации сервлетов незамедлил воспользоваться наш любимый Spring Framework. Начиная с версии Spring 3.1, если заглянуть в вышеупомянутый файл META-INF/services/javax.servlet.ServletContainerInitializer внутри spring-web-[3.1 и выше].jar, то можно обнаружить там полное имя класса org.springframework.web.SpringServletContainerInitializer, который, как не трудно догадаться, как раз и является реализацией интерфейса ServletContainerInitializer.

Класс SpringServletContainerInitializer находит в CLASSPATH приложения все классы, реализующие другой интерфейс – org.springframework.web.WebApplicationInitializer – и делегирует им всю работу по инициализации контекста Spring MVC без использования XML.

В качестве единственного параметра в переопределяемый метод onStartup(ServletContext) класса-реализации WebApplicationInitializer передается контекст сервлета, инициализацию которого в этом методе собственно и нужно произвести:

GoodbyeXmlAppInitializer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class GoodbyeXmlAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) throws ServletException {
        // Создание корневого контекста Spring
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(AppConfig.class);

        // Листенер для управления жизненным циклом корневого контекста Spring
        container.addListener(new ContextLoaderListener(rootContext));

        // Создание контекста Spring для сервлета-диспетчера Spring MVC
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(WebMvcConfig.class);

        // Регистрация сервлета-диспетчера Spring MVC
        ServletRegistration.Dynamic dispatcher =
                container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        // Отдельный маппинг для главной страницы приложения
        dispatcher.addMapping("/index");

        // Установка параметров контейнера
        container.setInitParameter("defaultHtmlEscape", "true");

        // Регистрация других сервлетов и фильтров

        // Например, фильтра для установки кодировки символов приложения
        FilterRegistration charEncodingFilterReg =
                container.addFilter("CharacterEncodingFilter", CharacterEncodingFilter.class);
        charEncodingFilterReg.setInitParameter("encoding", "UTF-8");
        charEncodingFilterReg.setInitParameter("forceEncoding", "true");
        charEncodingFilterReg.addMappingForUrlPatterns(null, false, "/*");

        // ...
    }

}

Напомню, что в любом приложении Spring MVC существует, как минимум, два контекста Spring:

  • корневой контекст для описания бинов приложения, имеющих по большей части отношение к бизнес-логике,
  • контекст сервлета-диспетчера Spring MVC, который используется для бинов, относящихся только к Web MVC-части приложения2, но в котором при этом наследуются и могут быть переопределены все бины корневого контекста.

В нашем примере мы тоже объявили два этих контекста, воспользовавшись экземплярами класса AnnotationConfigWebApplicationContext. Как и раньше, при конфигурировании при помощи web.xml, мы сообщаем приложению о существовании корневого контекста через объявление листенера типа ContextLoaderListener. Контекст же Spring MVC передается в качестве параметра при объявлении сервлета-диспетчера DispatcherServlet.

Что касается добавления бинов в какой-либо из контекстов Spring, то можно, конечно, регистрировать их по-отдельности с помощью метода AnnotationConfigWebApplicationContext#register(Class<?>[]), или посредством сканирования пакетов (метод AnnotationConfigWebApplicationContext#scan(String[])), но самый эффективный способ – это создание отдельного класса-конфигурации. Этот класс должен быть помечен аннотацией @Configuration, а зарегистрировать в контексте его можно при помощи того же самого метода AnnotationConfigWebApplicationContext#register(Class<?>[]).

Например, для корневого контекста объявление класса-конфигурации будет выглядеть следующим образом:

AppConfig.java
1
2
3
@Configuration
public class AppConfig {
}

Все преимущества использования отдельного класса с аннотацией @Configuration лучше рассмотреть на примере класса для объявления бинов Spring MVC – WebMvcConfig.

Web MVC-конфигурация “на бобах”

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

WebMvcConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "net.shafranov.spring.noxml.web" })
public class WebMvcConfig {

    @Bean
    public InternalResourceViewResolver internalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/pages/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

И пусть вас не сбивает с толку небольшой размер класса – чуть больше 10 строк. На самом деле, здесь закодировано очень многое. Настолько многое, что мне даже не хватит смелости в деталях обо всем написать. Но по порядку:

  1. Для начала разберемся с @EnableWebMvc. Эта аннотация аналогична элементу XML-конфигурации <mvc:annotation-driven />, и совершает она на 100% всю ту же самую, прямо скажем, немаленькую работу. То есть она регистрирует в контексте Spring MVC ключевые компоненты DefaultAnnotationHandlerMapping и AnnotationMethodHandlerAdaptor, и устанавливает для них некоторые разумные значения по умолчанию. На полный перечень “разумных значений по умолчанию” можно взглянуть в официальной документации Spring Framework.
  2. C помощью аннотации @ComponentScan мы сообщаем классу-конфигурации о нашем намерении сканировать пакет net.shafranov.spring.noxml.web на предмет нахождения в нем компонентов Spring. Все найденные в вышеназванном пакете аннотированные классы будут добавлены в контекст Spring MVC.
  3. Ну, и, наконец, в отдельном методе класса, к которому применяется аннотация @Bean, мы объявляем бин типа InternalResourceViewResolver3, который позволяет указать директорию в проекте, где лежат JSP-файлы, которые будут использоваться в качестве представлений для контроллеров Spring MVC.

Возможен ли мир без web.xml?

После завершения конфигурации, чтобы проверить, что все работает, опишем в соответствующих местах в приложении контроллер и JSP-представление для него:

net/shafranov/spring/noxml/web/controller/GoodbyeXmlController.java
1
2
3
4
5
6
7
8
9
10
@Controller
public class GoodbyeXmlController {

    @RequestMapping(value = "/index", method = RequestMethod.GET)
    public String printGoodbye(ModelMap model) {

        model.addAttribute("message", "Goodbye, XML!");
        return "goodbye";
    }
}
WEB-INF/pages/goodbye.jsp
1
2
3
4
5
<html>
<body>
    <h1>${message}</h1>
</body>
</html>

Теперь мы можем даже запустить наш пример и оценить результат:

Окно приложения Spring MVC

И все это вообще без какого-либо намека на web.xml. Не верите – убедитесь сами!

Ложка дегтя

Все бы ничего, но если мы попробуем ввести в адресной строке браузера URL приложения http://127.0.0.1:8080/goodbye-xml/ вместо полного адреса страницы контроллера http://127.0.0.1:8080/goodbye-xml/index, то мы не получим ровным счетом никакого результата, кроме ошибки 404. Очевидно, это происходит потому, что маппинг страницы /index, предполагающейся за главную в нашем примере, не совпадает ни с одним из URL’ов, из которых по умолчанию состоит список welcome-file-list сервера Tomcat.

А быстрый поиск в Google сообщает нам печальную новость о том, что некоторые из частей web.xml нельзя поменять, пользуясь лишь программной конфигурацией, описанной в спецификации Servlet 3.0. Среди этих частей: параметр display-name, тайм-аут сессии, маппинги для страниц ошибок… и welcome-file-list.

Таким образом, если в приложении необходимо задать значение, отличное от значения по умолчанию, для одного из вышеперечисленных параметров, придется все-таки создавать файл web.xml.

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

Главное – не забыть с помощью правильно подобранных атрибутов корневого тега <web-app /> cообщить приложению о его Servlet 3.0-ной природе:

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
            http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <display-name>Goodbye XML Application</display-name>

    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>

    <welcome-file-list>
        <welcome-file>index</welcome-file>
    </welcome-file-list>

</web-app>

Вот такая вот XML-петрушка!..

Исходные коды

Полностью исходные тексты нашего небольшого примера доступны в архиве вот по этой ссылке.


  1. Правда отказаться пока только по большей части, но об этом чуть позже, не будем омрачать радость пусть и не полной, но все-таки победы над web.xml.

  2. Например, контроллеры (controllers) - компоненты соответствующие букве C в аббревиатуре MVC; компоненты, назначающие обработчиков для входных запросов (handler mappings); распознаватели представлений приложения (view resolvers) и т.д.

  3. Стоит помнить, что имя метода в данном случае соответствует имени бина.

Comments