Глава 13 Spring in Action 2th edition |
Содержание
|
Обработка веб-запросов
В данной главе рассматриваются
- Отображение запросов в Контроллеры Spring
- Явное связывание параметров форм
- Валидация заполняемых форм
- Связывания исключений с Видами
Как у разработчика JEE, у Вас более чем вероятно, есть опыт разработки web-приложения. На самом деле, для многих разработчиков Java ЕЕ, создание web-приложений является их основным занятием. Если у вас подобный опыт, таки есть, Вы уже наверное хорошо осведомлены о тех довольно специфических проблемах, которые приходится решать в подобных системах. И все они лишь усугубляются независимой природой протокола HTTP.
Веб-фреймворк Spring разработан, чтобы помочь вам в решении всех этих проблем. На основе шаблона Model-View-Controller (MVC) , Spring MVC поможет вам построить web-приложения, которые будут являться столь же гибкими и слабо связанными, как сам Spring Framework.
В этой главе, и следующих главах мы будем изучать веб-фреймворк Spring MVC . В этой главе мы сосредоточимся на той части Spring MVC, который обрабатывает запросы. Вы увидите, как расширить богатый исходный набор Spring классов Контроллера, чтобы справится практически с любыми web-функциями, требуемыми вашему приложению. Вы также увидите, как классы "обработчиков-маппинга" в Spring делают легкой работу связывая URL шаблонов с конкретными реализациями Контроллеров. Главе 14 будут будет посвящена тому, как использовать Spring MVC для получения Видов формирующих и отправляющих ответ сервера обратно пользователю.
Однако прежде чем углубляться в особенности реализации Контроллеров и обработчиков-мапинга, давайте окинем беглым взглядом Spring MVC, построив нашу первую, пусть небольшую но полную web-функциональность.
Source(s): Глава 13 Spring in Action 2th edition
Обзор Spring MVC
Видели ли вы когда нибудь детскую игру "Мышеловка"? Это довольно сумасшедшая игра. Цель ее - отправить небольшой стальной шарик через ряд дурацких приспособлений для того, чтобы вызвать срабатывание мышеловки. Путь шарика проходит по разным видами сложных конструкций, начиная от скатывания извилистой рампой, чтобы затем с игрушечных качелей запрыгнуть на миниатюрное колесо обозрения, а потом быть выброшенным из крошечного ведерка ударом резинового башмачка. Он проходит через все это, просто чтобы защелкнуть пружиной ловушки, бедных, ничего не подозревающих пластиковых мышей.
На первый взгляд, можно подумать, что MVC Spring Framework во многом похож на эту "Мышеловку". Только вместо того чтобы перемещать шарик через различные пандусы, детские качели-шаталки, и колеса, Spring запутанными кругами перемещает запросы, между Диспетчер-Сервлетом, Обработчиками Маппинга, Контроллерами, Арбитами Видов.
Но не стоит слишком отождествлять Spring MVC с игрой Мышеловка в стиле Руби Голдберга. Каждый из компонентов в Spring MVC выполняет вполне конкретную цель. Начнем изучение Spring MVC, изучив жизненный цикл типичного запроса.
Source(s): Глава 13 Spring in Action 2th edition
Один день из жизни Запроса
Каждый раз, когда пользователь нажимает на ссылку или отправляет форму в веб-браузере, Запрос идет на работу. Суть работы Запроса сводится к тому, что он - курьер. Подобно, почтовому перевозчику или человеку из Федеральной экспресс-доставки , Запрос живет, перенося информацию из одного места в другое.
Запрос, весьма занятой парень. С того момента, когда он покидает браузер до момента, когда вернется ответ, Запрос сделает несколько остановок, каждый раз, сбросив часть информации, и, подобрав что-то взамен. Рисунок 13.1 показывает все остановки, что делает Запрос .
Когда Запрос покидает браузер, он несет в себе информацию о том, что пользователь - попросил. По крайней мере, Запрос непременно будет нести запрошенный URL. Но оно может также иметь дополнительные данные, например такие как информация, представленная в форме которую заполнил пользователь.
- (1) Первой остановкой в разъездах Запроса, является DispatcherServlet Spring. Как и большинство Java реализаций MVC фреймворков, Spring MVC пропускает все входящие запросы через один сервлет фронт-контроллера. Фронт-контроллер является общим шаблоном веб-приложений , в котором одному сервлету делегирована ответственность за все запросы к остальным компонентам приложения (выполняющим фактическую обработку). В случае с Spring MVC, фронт-контроллером является DispatcherServlet;
- (2) Работа DispatcherServlet в том, чтобы отправить запрос - Контроллер-у(Controller). В Spring MVC, Контроллер это Spring-компонент, который обрабатывает запрос. Но типичное приложение может иметь несколько Контроллеров и DispatcherServlet-у требуется помощь в принятии решения, в какой-же Контроллер отправить запрос. Таким образом, DispatcherServlet консультирует c одинм или несколькими - Обработчиками-маппинга(handler mappings), чтобы выяснить, где будет следующая остановка Запроса . Обработчик-маппинга при принятии своего решения будет руководствоваться в первую очередь URL переданным в пришедшем запросе;
- (3) Как только соответствующий Контроллер был определен, DispatcherServlet посылает Запрос в счастливый путь к выбранному Контроллеру. В Контроллере, Запрос отдаст часть своей полезной нагрузки (информацию, заполненной пользователем формы) и будет терпеливо ждать пока Контроллер обработает эту информацию. (На самом деле, хорошо разработанный Контроллер, почти или практически не занимается обработкой этой информации, а вместо себя делегирует ответственность за бизнес-логику одному или нескольким служебным объектам);
- (4) Логика работы Контроллера часто приводит к тому, что некоторая информация, должна быть передана назад пользователю и отображаться в его браузере. Эта информация называется - Моделью данных, или просто Моделью(Model). Но отправки обратно "сырой" информации из Модели, пользователю - недостаточно, перед отправкой информация должен быть отформатирована и представлена в удобном для пользователя формате, как правило, в HTML. Для этого информация должна быть передана в один из - Видов(View), которым обычно являются JSP. Итак, последнее, что Контроллер должен сделать, это упаковать Модель данных и имя Вида в объект ModelAndView. Затем он отсылает наш Запрос, вместе с новой "посылкой" - объектом ModelAndView, назад в DispatcherServlet. Как следует из названия, объектом ModelAndView содержит Модель данных, а также некий намек на то, какой из Видов будет использован для отображения результата.
- (5) Так, чтобы Контроллер не был жестко связан с с каким либо конкретным Видом, объект ModelAndView не несет ссылку на фактический JSP. Вместо этого он несет только "логическое имя" Вида, используемое затем для поиска фактического Вида. Последний и будет производить в результате своей работы, необходимый нашему браузеру HTML. После того как ModelAndView доставлен в DispatcherServlet, последний использует помощь - Арбитра Вида(view resolver), чтобы найти заветные JSP фактического Вида .
- (6) Теперь, когда DispatcherServlet знает, какой фактический Вид будет отображать результаты, работа Запроса подходит к концу. Его конечная остановка - у реализации Вида (вероятно, JSP), куда он доставляет Модель данных. С момента получением фактическим Видом - Модели данных работа нашего трудолюбивого Запроса завершена.... .
Вид, будет использовать полученную Модель данных для генерации отображения страницы, которую затем будет доставлять обратно в браузере, другой (уже не столь трудолюбивый) курьер - объект Ответ (Response).
Мы обсудим каждый из этих шагов более подробно в этой и следующей главе. Но первое что необходимо, это сконфигурировать DispatcherServlet для использования
Настраиваем DispatcherServlet
В основе Spring MVC является DispatcherServlet, сервлет, который функционирует в качестве фронт-контроллер в Spring MVC. Как и любой сервлет, DispatcherServlet должен быть настроен в файле web.xml ваших веб-приложений. Поместите следующее объявление <servlet> в web.xml файл приложения:
<servlet>
<servlet-name>roadrantz</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-name> данное сервлету является важным. По умолчанию, когда DispatcherServlet загружен, он будет загружать контекст Spring приложения из файла XML, чье имя основано на имени сервлета. В этом случае, поскольку сервлет называется roadrantz, то и DispatcherServlet будет пытаться загрузить контекст приложения из файла с именем roadrantz-servlet.xml.
Далее вы должны указать, какие адреса будут обработаны DispatcherServlet. Добавьте следующие <servlet-mapping> в web.xml, чтобы DispatcherServlet обрабатывал все URL-адреса, которые заканчиваются на .htm :
<servlet-mapping>
<servlet-name>roadrantz</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
Таким образом, вам, наверное любопытно, почему мы выбрали именно этот шаблон URL-адреса. Это может быть потому, что весь контент производимый нашим приложение - HTML. А может быть потому что мы хотим, обмануть наших друзей, заставляя их думать что все наше приложение состоит из статических HTML-файлов. А может быть еще и потому, что .do - это какое-то глупое расширения.
Но правда заключается в том, что URL шаблон в принципе произволен и мы могли бы выбрали любой URL шаблон для DispatcherServlet. Наша основная причина выбора *.htm в том то, что этот шаблон, используемый в соответствии с соглашением в большинстве приложений Spring MVC, которые производят HTML-контент. Обоснование этой конвенции является то, что если контент производится в HTML и т.д. URL должен отражать сей факт.
Теперь, когда DispatcherServlet настраивается в web.xml и ему задан URL-маппинг, вы вероятно готовы, чтобы начать писать веб-слой приложения. Однако, есть еще одна вещь, которую мы рекомендуем вам добавить в web.xml.
Разделение контекста приложения
Как мы уже упоминали ранее, DispatcherServlet будет загружать контекст Spring приложение из одного файла XML, имя которого основывается на его <servlet-name>. Но это не значит, что вы не можете разделить ваш контекст приложения на несколько файлов XML. На самом деле, мы рекомендуем вам разделить ваш контекст приложения с применением слоев, как показано на рисунке 13.2.
Настроенный, DispatcherServlet уже загружает roadrantz-servlet.xml. Следовательно, вы можете поместить все <bean> приложения определений в roadrantz-servlet.xml, но в итоге этот файл станет довольно громоздким. Разделение его на логические части с применением слоев может упростить поддержку приложения, сохраняя каждый из конфигурационных файлов Spring ответственным только за один слой приложения. Это также позволяет легко заменять слой конфигурации, не затрагивая другие слои (например замена roadrantz-data.xml файла, использующего Hibernate , одноименным файлом для использования iBATIS).
Поскольку конфигурационным файл DispatcherServlet является roadrantz-servlet.xml, имеет смысл, чтобы этот файл содержал <bean>-определения, относящихся к контроллерам и другим компонентам Spring MVC. Что же касается сервисных бинов и бинов слоя данных, разумно было бы поместить их определения в roadrantz-service.xml и roadrantz-data.xml, соответственно.
Настройка загрузчика контекста
Чтобы убедиться, что все эти конфигурационные файлы загружены, вам необходимо настроить загрузчик контекста в файле web.xml. Загрузчик контекста загружает конфигурационные файлы контекста в дополнение к тому, что загружает сам DispatcherServlet. Наиболее часто используемый загрузчик контекста - сервлет-слушатель называемый ContextLoaderListener который настраивается в web.xml следующим образом:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
ПРИМЕЧАНИЕ
- Некоторые веб-контейнеры, не инициализируют слушателей до сервлетов - что очень важно при загрузке определения контекста Spring! Если ваше приложение будет развернуто к старом веб-контейнер, который придерживается спецификации Servlet 2.2 или 2.3, которые не инициализируют слушателей перед сервлетами, вы должны использовать ContextLoaderServlet вместо ContextLoaderListener.
При настройке ContextLoaderListener , вы должны указать ему, местоположение файлов конфигурации Spring для загрузки. Если не указано иное, контекст загрузчик будет искать файл конфигурации Spring в /WEB-INF/applicationContext.xml. Но это расположение не поддается разделению контекста приложения с применение слоев, так что вы, вероятно, захотите изменить это поведение. Вы можете указать один или несколько конфигурационные файлы контекста Spring для загрузки, установив параметра contextConfigLocation в контексте сервлета:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/roadrantz-service.xml
/WEB-INF/roadrantz-data.xml
/WEB-INF/roadrantz-security.xml
</param-value>
</context-param>
Параметр ContextConfigLocation задается как список путей (относительно корня веб-приложения). Как показано здесь, загрузчик контекста будет использовать contextConfigLocation чтобы загрузить три конфигурационных файла контекста: один для слоя безопасности, один для слоя сервисов, и один для слоя данных.
DispatcherServlet теперь настроен и готов к отправке запросов в веб-слой приложения. Но веб-слой ведь еще не был построен! Не волнуйтесь. Мы будем строить много веб-слоев в этой главе. Давайте начнем обзор с того, как все части Spring MVC вместе собираются производить веб-функциональность.
Spring MVC в двух словах
Каждое веб-приложение имеет домашнюю страницу. Это необходимо чтобы иметь отправную точку в приложении. Это дает пользователям место для старта и знакомое место, чтобы туда вернуться, если заблудишься. В противном случае, они будут крутиться вокруг да около, кликать по ссылкам, разочаруются и, вероятно, в конечном итоге уйдут на другой веб-сайт.
Приложение RoadRantz не является исключением из "правила домашней страницы" . Потому, нет лучшего места, чтобы приступить к разработке веб-слоя нашего приложения, чем его домашняя страницы. При ее создании , мы быстро ознакомимся с азами Spring MVC.
Как вы помните одно из требований к RoadRantz: домашняя страница должна отображать список самых последних из введенных тирад. Следующий список шагов определяет минимум, того что вы должны сделать, чтобы построить домашнюю страницу в Spring MVC:
- 1) Написать класс Контроллера, который реализует логику работы страницы. Логика заключается в использовании RantService чтобы получить список последних введенных тирад.
- 2) Настройка Контроллера в файле конфигурации контекста DispatcherServlet (roadrantz-servlet.xml).
- 3) Настройте Арбитр-Вида(View resolver), чтобы связать контроллер с JSP.
- 4) Написать JSP, который будет отображать домашнюю страницу пользователям.
Первым шагом является создание объекта контроллера, который будет обрабатывать запросы к домашней странице . Так, что без дальнейших проволочек, давайте напишем наш первую Spring MVC контроллер.
Создание Контроллера
Когда Вы выходите, чтобы поесть в хорошем ресторане, человек, с которым вы там будете взаимодействовать наиболее близко это официант или официантка. Они примут Ваш заказ, передадут его дальше, к поварам на кухню чтобы те его приготовили, и в конечном счете, вынесут вам - вашу трапезу. И если они хотят достойных чаевых, они принесут вам ее с дружелюбной улыбкой и будут следить за полнотой ваших бокалов. Хотя вы и знаете, что другие люди тоже участвуют в превращении вашего ужина - в приятное событие, но официант или официантка это ваш интерфейс - к кухне.
Точно так же в Spring MVC, контроллер класса, это ваш интерфейс к функциональности приложения. Как показано на рисунке 13.3, контроллер получает запрос, передает его для обработки в сервисные классы (реализующие логику приложения), и в конечном итоге собирает результаты в страницу, которая возвращается к вам в вашем веб-браузере. В связи с этим, контроллер не сильно отличается от обычного HttpServlet или от Action в Struts.
Домашняя страница контроллер приложения RoadRantz относительно проста. Он не принимает никаких параметров запроса и просто выдает список недавно введенных тирады для отображения его на главной странице. Листинг показывает 13.1 HomePageController, этоSpring MVC контроллер, который реализует функциональность домашней страницы.
Листинг 13.1 HomePageController, который формирует список недавних тирад для показа на домашней странице
package com.roadrantz.mvc;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import com.roadrantz.service.RantService;
public class HomePageController extends AbstractController {
public HomePageController() {}
protected ModelAndView handleRequestInternal(
HttpServletRequest request, HttpServletResponse response)
throws Exception {
//Retrieves list of rants
List recentRants = rantService.getRecentRants();
return new ModelAndView("home", //Goes to “home” view
"rants", recentRants); //Returns rants in model
}
private RantService rantService;
public void setRantService(RantService rantService) {
//Injects RantService
this.rantService = rantService;
}
}
Чем контроллер Spring MVC отличается от сервлета или от Struts Action так это - тем, что он настроен просто как обычный JavaBean в контексте Spring приложения . Это означает, что вы можете использовать все преимущества внедрения зависимостей (DI) и Spring AOP с классом контроллера точно так же, как и с любым другим бином.
DI в случае HomePageController, используется для связывания в него RantService. Таким образом HomePageController делегирует ответственность за получения списка последних тирад в связанный RantService.
Введение в ModelAndView
После того как шеф-повар приготовил еду, официант/официантка заберет ее чтобы принести к вашему столу. На выходе, последнее, что они могут сделать, это добавить несколько заключительных штрихов, может быть, веточку петрушки.
Как только бизнес-логика была выполнена сервисными объектами, пришло время для Контроллера, чтобы отправить результаты обратно в браузер. Последнее, что делает handleRequestInternal(), это возвращение объекта - ModelAndView. Класс ModelAndView представляет важное понятие в Spring MVC. На самом деле, каждый метод выполняемый Контроллером должен вернуть ModelAndView. Так что, найдите время, чтобы понять, как этот важный класс работает.
Объект ModelAndView, как следует из названия, полностью инкапсулирует как Вид так и Модель данных, которая будет отображаться этим Видом. В случае HomePageController, объект ModelAndView строится следующим образом:
new ModelAndView("home", "rants", recentRants);
Первый параметр этого конструктора ModelAndView - логическое имя компоненты - Вида , который будет использоваться для отображения информации из этого Контроллера. Здесь, логическое имя Вида - home. Арбитр Вида(view resolver) будет использовать это имя для поиска реального объекта View (вы узнаете больше о Видах и Арбитрах Вида позже в главе 14).
Следующие два параметра представляют собой объект Модели, которая будет передана Виду. Эти два параметра выступают в качестве пары имя-значение. Вторым параметром является имя для объекта Модели, сам объект - Модель дан в качестве третьего параметра. В этом случае Модель содержащая список тирад в переменной recentRants и названная - rants, будет передана в Вид с логическим именем home.
Конфигурирование бина Контроллера
Теперь, когда HomePageController был написан, пришло время настроить его в конфигурационном файле контекста DispatcherServlet (который для RoadRantz называется - roadrantz-servlet.xml). Следующий фрагмент XML объявляет HomePageController:
<bean name="/home.htm"
class="com.roadrantz.mvc.HomePageController">
<property name="rantService" ref="rantService" />
</bean>
Как упоминалось ранее, свойство rantService должен быть введено с реализацией интерфейса RantService. В этой декларации <bean>, мы связываем свойство rantService с ссылкой на другой бин по имени rantService. Сам бин rantService объявлен в другом месте (в roadrantz-service.xml, если быть точными).
Одной вещью, которая, возможно, поразила вас своей странностью является то, что вместо указания идентификатора(id) бина HomePageController, мы указали - имя(name) этого бина . И, чтобы сделать эту вещь еще страннее, мы вместо того чтобы задать "настоящее" имя, задали его как URL-шаблон /home.htm. Здесь имя атрибута выполняет двойные обязанности: служит именем бина и одновременно URL-шаблоном для запросов, которые должны обрабатываться данным Контроллером. Поскольку URL-шаблон имеет специальные символы, которые недопустимы в XML id-атрибута (в частности - слэш (/) символ), то вместо name-атрибута должен быть использован id-атрибут.
Когда на DispatcherServlet поступает запрос с URL, который заканчивается на /home.htm, DispatcherServlet направит запрос в HomePageController для его обработки. Однако следует отметить, что единственной причиной того, что name-атрибут бина используется как его URL-шаблон, является то, что мы пока не настроили бин handlermapping. А Обработчиком-маппинга "по умолчанию" для DispatcherServlet-а, является BeanNameUrlHandlerMapping, который (как мы помним) и использует имя бина, в качестве URL-шаблона. Позже, Вы - увидите, как использовать в Spring и некоторые другие Обработчики-маппинга , позволящие Вам отделить имя бина Контроллера от его URL-шаблона.
Объявление Арбитра Вида
На обратном пути в веб-браузер, результаты веб-операции должны быть представлены в "человеческом" формате. Как официант может положить веточку петрушки на тарелку, чтобы сделать ее вид более презентабельным, так и полученный список тирад должен быть немного "приодет", прежде чем представлять его клиенту. Для этого мы будем использовать JSP-страницу, которая сможет отображать результаты в удобном для пользователя формате.
Но как же Spring узнает, какую именно JSP использовать для визуализации результатов? Как вы помните, одним из значений, возвращаемых в объекте ModelAndView является логическое имя Вида. Хотя логическое имя Вида напрямую не ссылается на конкретные JSP, оно может быть использовано чтобы косвенным путем вывести нас к нужной JSP.
Чтобы помочь Spring MVC выяснить, какую JSP использовать, вам придется объявить в roadrantz-servlet.xml еще один или несколько дополнительных бинов : Арбитры Видов.
Суть работы Арбитра Видов состоит в том, чтобы принять логическое имя Вида возвращаемое в ModelAndView и провести его сопоставление с фактическим Видом. В случае HomePageController, нам необходимо чтобы Арбитр Видов преобразовал имя - home (логическое имя Вида) в имя JSP-файла, который отобразит домашнюю страницу.
Как будет показано - позже , Spring MVC поставляется с несколькими арбитрами Вида из которых можно выбирать. Но для Видов, которые основываются на JSP, нет ничего проще, чем InternalResourceViewResolver:
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/WEB-INF/jsp/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
InternalResourceViewResolver предваряет имя Вида, возвращаемое в ModelAndView значением своего свойства prefix и завершает его значением своего свойства - suffix. Так как HomePageController возвращает имя Вида - home в ModelAndView, InternalResourceViewResolver найдет фактический Вид в /WEBINF/JSP/home.jsp
Создание JSP-Вида
Мы написали Контроллер, который будет обрабатывать запросы к домашней странице и сконфигурировали его в контексте приложения Spring. Он будет консультироваться с бином RantService для поиска последних добавленных тирад. И когда это будет сделано, Контроллер будет посылать результаты в JSP Вид. Так что теперь нам осталось только создать JSP, которая отображает нужную веб-страницу. JSP в листинге 13.2 перебирает список тирад и отображает их на домашней странице.
Листинг 13,2 home.jsp, которая отображает список последних тирад
<%@ page contentType="text/html" %>
<%@ taglib prefix="c" uri="https://java.sun.com/jstl/core" %>
<html>
<head><title>Rantz</title></head>
<body>
<h2>Welcome to RoadRantz!</h2>
<h3>Recent rantz:</h3>
<ul>
<c:forEach items="${rants}" var="rant">
<li><c:out value="${rant.vehicle.state}"/> /
<c:out value="${rant.vehicle.plateNumber}"/> --
Iterates over
<c:out value="${rant.rantText}"/>
list of rants
</li>
</c:forEach>
</ul>
</body>
</html>
Хотя мы не оставили каких-либо эстетических элементов в home.jsp для краткости, она по-прежнему может служить для иллюстрации того, как Модель данных, возвращаемая в ModelAndView может быть использована в работе Вида. В HomePageController, мы поместили список тирад в свойство "Модели" именуемое - rants. Когда home.jsp генерирует домашнюю страницу, код JSP ссылается на список тирад как на ${rants}.
Убедитесь, что имя этой JSP страницы - home.jsp и поместите ее в папку /WEB-INF/jsp веб-приложения. Это то место где InternalResourceViewResolver постараемся ее найти.
Собираем все - вместе
Домашняя страница завершена. Для обработки запросов к этой странице, Вы написали Контроллер. Затем настроили его, так чтобы полагаясь на обработчик-маппинга BeanNameUrlHandlerMapping иметь доступ к Контроллеру через URL шаблон /home.htm. Далее написали простую JSP, которая генерирует домашнюю страницу, и настроили Арбитр Вида для поиска этой JSP. А теперь, как это живет все вместе? Рисунок 13.4 показывает шаги, того, как запрос к странице /home.htm будет выполняться.
Напомним этапы этого процесса:
- (1) DispatcherServlet получает запросы, в которых URL шаблон - /home.htm.
- (2) DispatcherServlet консультируется с BeanNameUrlHandlerMapping чтобы найти Контроллер с name-атрибутом бина - /home.htm; он находит бин HomePageController
- (3) DispatcherServlet отправляет запрос в HomePageController для обработки.
- (4) HomePageController возвращает объект ModelAndView с логическим именем Вида - home и "Моделью данных" в виде списка тирад, в свойстве называемом - rants.
- (5) DispatcherServlet консультируется с Арбитром Вида (сконфигурирован как InternalResourceViewResolver), чтобы найти Вид с логическим именем - home. Арбитр Вида InternalResourceViewResolver возвращает путь к /WEB-INF/jsp/home.jsp.
- (6) DispatcherServlet перенаправляет запрос на JSP с полным именем /WEB-INF/jsp/home.jsp чтобы сгенерировать домашнюю страницу и отобразить ее пользователю.
Теперь, когда вы увидели "большую картину" Spring MVC в целом , давайте замедлим немного ход и более пристально рассмотрим каждую из движущихся частей, участвующих в обслуживании запроса. И начнем мы с обработчика-маппинга..
Сопоставление запросов и Контроллеров
Когда у курьера есть пакет, который должен быть доставлен в конкретный офиса в большом офисном здании, он должен знать, как найти этот офис. В большом офисном здании с большим количеством арендаторов, это было бы сложно, если бы не каталог здания . Каталоги здания часто расположены около лифтов и помогают, тем кто не знаком со зданием быстро найти нужные этаж и комнату.
Таким же образом, когда запрос поступает в DispatcherServlet, там должно быть какой-нибудь каталог, чтобы помочь выяснить, куда запрос должен быть отправлен далее. Обработчик-маппинга помогает DispatcherServlet-у выяснить, в какой Контроллер запрос должен быть направлен. Обработчики-маппинга как правило устанавливают соответствие между URL шаблонами и бинам определенных Контроллеров . Это подобно тому, как URL-адреса отображаются в сервлеты с использованием <servlet-mapping>-элемента в файле web.xml веб-приложения. Или же как Actions в Struts, сопоставляются с URL, используя path-атрибут для <action> в struts-config.xml.
В предыдущем разделе, мы опирались на тот факт, что DispatcherServlet по умолчанию использовал обработчик-маппинга BeanNameUrlHandlerMapping. Этот обработчик было хорош, чтобы начать работу, но он не может подходить для всех случаев жизни. К счастью, Spring MVC предлагает несколько реализаций обработчиков-маппинга на ваш выбор.
Все обработчики-маппинга в Spring MVC это реализации интерфейса org.springframework.web.servlet.HandlerMapping . Spring уже изначально содержит четыре полезные реализации HandlerMapping, как показано в таблице 13.1.
Таблица 13.1 обработчики-маппинга помогают DispatcherServlet-у найти нужный Контроллер для обработки запроса.
Обработчик-маппинга | Как он отображает запросы к Контроллерам | |
---|---|---|
BeanNameUrlHandlerMapping | Сопоставляет Контроллеры с URL-адресами, используя свойство name бина Контроллера. | |
SimpleUrlHandlerMapping | Сопоставляет Контроллеры с URL-адресами, используя карту java.util.Properties определенную в контексте приложения Spring. | |
ControllerClassNameHandlerMapping | Сопоставляет Контроллеры с URL-адресами, используя имя класса контроллера в качестве основы для URL-адреса. | |
CommonsPathMapHandlerMapping | Сопоставляет Контроллеры для запросов с использованием на уровне исходных метаданных размещены в коде контроллера. Метаданных определяется с помощью Jakarta Commons Attributes( https://jakarta.apache.org/commons/attributes). |
Вы уже видели пример того, как BeanNameUrlHandlerMapping в роли обработчик-маппинга по умолчанию, использовался совместно с DispatcherServlet. Давайте посмотрим, как использовать остальные обработчики, начиная с SimpleUrlHandlerMapping.
Использование SimpleUrlHandlerMapping
SimpleUrlHandlerMapping, вероятно, один из самых простых обработчик-маппинга в Spring. Он позволяет сопоставить URL шаблоны непосредственно с Контроллерами без того, чтобы именовать ваши бины каким либо особым образом.
Для примера, рассмотрим следующее объявление SimpleUrlHandlerMapping, ассоциирующее несколько Контроллеров в приложениии RoadRantz с их шаблоны URL:
<bean id="simpleUrlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/home.htm">homePageController</prop>
<prop key="/rantsForVehicle.htm">
rantsForVehicleController
</prop>
<prop key="/rantsForVehicle.rss">
rantsForVehicleControllerRss
</prop>
<prop key="/rantsForDay.htm">rantsForDayController</prop>
<prop key="/login.htm">loginController</prop>
<prop key="/register.htm">registerMotoristController</prop>
<prop key="/addRant.htm">addRantController</prop>
</props>
</property>
</bean>
Свойство mappings в SimpleUrlHandlerMapping связывает в себя карту java.util.Properties (используя <props>). key-атрибут каждого <prop>-элемента, это - URL-шаблон. Так же, как в случае с BeanNameUrlHandlerMapping, все URL-шаблоны - относительные (т.е. относительно URL-адреса <servlet-mapping> в DispatcherServlet) . Значением каждого <prop>-элемента является имя бина Контроллера, который будет обрабатывать запросы к данному URL-шаблону.
В случае, если вам интересно, откуда все те другие контроллеры появились, просто потерпите. К тому времени, эта глава будет пройдена, мы сможем увидеть, большинство из них. Но сначала давайте рассмотрим еще один способ объявить сопоставление Контроллера с URL - использование имени класса контроллеров.
Используем ControllerClassNameHandlerMapping
Часто вы сможете придумать сопоставление ваших Контроллеров , с такими URL шаблонами, которые бы во многом были схожи с именами классов контроллеров. Например, в приложении RoadRantz, это сопоставление rantsForVehicle.htm с RantsForVehicleController , а rantsForDay.htm с RantsForDayController.
Вы обратили внимание на закономерность? Во всех этих случаях, URL-шаблон такой же, как имя класса Контроллера, если убрать из его имени - "Controller" и добавить в конце - .htm Кажется, что URL-шаблоны подобные этим, можно было бы автоматически получать из имен классов соответствующих Контроллеров , просто руководствуясь некими заданными общими правилами, без необходимости явного описания сопоставления.
На самом деле, это и есть примерно то, что делает ControllerClassNameHandlerMapping:
<bean id="urlMapping"
class="org.springframework.web.servlet.mvc.ControllerClassNameHandlerMapping"/>
При настройке ControllerClassNameHandlerMapping, вы говорите DispatcherServlet-у Spring сопоставлять URL-шаблоны для Контроллеров следуя простой конвенции. Вместо явного сопоставления каждого Контроллера и URL-шаблона, Spring будет автоматически сопоставлять Контроллеры с URL-шаблонами, основанными на именах классов этих Контроллера. Рисунок 13.5 иллюстрирует, как для RantsForVehicleController будет сформирован URL-щаблон.
Проще говоря, для получения URL-шаблона, Controller-часть имени класса Контроллера удаляется (если она существует), оставшийся текст переводится в нижний регистр, слэш (/) будет добавлен в начале, и ".htm" добавляется в конце. Следовательно, бин Контроллера, чей класс - RantsForVehicleController будет соответствовать - /rantsforvehicle.htm. Обратите внимание, что весь URL-шаблон преобразуется к строчными буквам, что немного отличается от соглашений, которым мы следовали в случае с SimpleUrlHandlerMapping.
Отображение на основе метаданных
Последним обработчиком-маппинга который мы рассмотрим это CommonsPathMapHandlerMapping. Этот обработчик-маппинга считывает метаданные размещеные в исходном коде Контроллера, чтобы определить сопоставляемый URL. В частности, метаданные, как ожидается, будут в виде org.springframework.web.servlet.handler.commonsattributes.PathMap атрибута скомпилированного в Контроллер с помощью Jakarta Commons Attributes compiler.
Для использования CommonsPathMapHandlerMapping, просто объявить его как <bean> в вашем конфигурационном файле контекста следующим образом:
<bean id="urlMapping" class="org.springframework.web.
servlet.handler.metadata.CommonsPathMapHandlerMapping"/>
Затем пометьте каждый из ваших Контроллеров атрибутом PathMap чтобы объявить URL-шаблон для контроллера. Например, для сопоставления HomePageController с /home.htm, пометьте HomePageController следующим образом:
/**
* @@org.springframework.web.servlet.handler.
commonsattributes.PathMap("/home.htm")
*/
public class HomePageController
extends AbstractController {
…
}
Наконец, вам нужно настроить процесс сборки приложения, включив в него использование Commons Attributes compiler так, чтобы атрибуты были скомпилированы в код вашего приложения. Мы адресуем Вас к домашней странице Commons Attributes compiler ( https://jakarta.apache.org/commons/attributes ) для выяснения подробностей того, как настроить этот компилятор совместно с Ant или Maven.
Совмещение обработчиков-маппинга
Как вы уже увидели, Spring содержит набор полезных обработчиков-маппинга. Но что, если вы не можете однозначно решить, какой-же из них, конкретно, вам - нужен? Например, предположим, что ваше приложение было простым и вы решили использовали BeanNameUrlHandlerMapping. Но оно начинает расти и вы хотите, дополнительно, применить SimpleUrlHandlerMapping. Но можете ли вы смешивать разные обработчики-маппинга в одном приложении? Как выясняется, все классы обработчиков-маппинга реализуют в Spring интерфейс - Ordered . Это означает, что вы можете объявить несколько обработчиков-маппинга в приложении, и задавая в них значение свойства - order, управлять тем самым взаимным приоритетом их выбора (т.е. порядком их использования по отношению друг к другу).
Предположим, вы хотите использовать как BeanNameUrlHandlerMapping так и SimpleUrlHandlerMapping, рядом друг с другом в одном приложении. Тогда, вы должны были бы объявить бины обработчиков-маппинга следующим образом:
<bean id="beanNameUrlMapping"
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="order"><value>1</value></property>
</bean>
<bean id="simpleUrlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="order"><value>0</value></property>
<property name="mappings">
…
</property>
</bean>
Обратите внимание, что чем НИЖЕ значения свойства - order, тем ВЫШЕ - приоритет. В данном случае, значение order у SimpleUrlHandlerMapping оказался меньше, чем у BeanNameUrlHandlerMapping. Это означает, что при попытке сопоставить URL с Контроллером , DispatcherServlet сначала проконсультируется с SimpleUrlHandlerMapping. Консультироваться с BeanNameUrlHandlerMapping он станет, только если обращение к SimpleUrlHandlerMapping не дало никаких результатов.
Обработчики-маппинга в Spring помогают DispatcherServlet-у узнать, какому Контроллеру запрос должен быть направлен. После того, как DispatcherServlet выяснил, куда отослать запрос, это уже дело выбранного Контроллера, его - обработать. Потому, давайте посмотрим, как создавать контроллеры в Spring MVC.
Обработка запросов Контроллерами
Если DispatcherServlet является сердцем Spring MVC, то Контроллеры, это его - мозги. При реализации поведение приложения Spring MVC, вы расширяете один из классов Spring Контроллера. Контроллер получает запросы от DispatcherServlet и выполняет некоторые бизнес-функции от имени пользователя.
Если вы знакомы с другими веб-фреймворками, такими как Struts или WebWork, вы можете признать Контроллеры в Spring примерно эквивалентными по целевому назначению - "действиям"(Actions) в Struts или WebWork. Одним большим отличием между Spring Контроллерами и Struts / WebWork "действиями", однако, является то, что Spring предлагает богатую иерархию Контроллеров (как показано на рисунке 13.6), в сравнении с довольно плоской иерархией "действий" в Struts или WebWork.
На первый взгляд, Рисунок 13.6 может показаться несколько пугающим. Конечно, по сравнению с другими MVC-фреймворками, таких как Jakarta Struts или WebWork, требуется намного больше, чтобы проглотить иерархию Контроллеров Spring. В действительности, однако, это лишь кажущаяся сложность и на самом деле все довольно просто и гибко.
В верхней части иерархии Контроллеров находится интерфейс - Controller. Любой класс, реализующий этот интерфейс может быть использован для обработки запросов через Spring MVC Framework .
Чтобы создать свой собственный Контроллер, все что вам нужно сделать, это написать класс, который реализует этот интерфейс.
Хотя вы конечно могли бы написать класс, который непосредственно реализует интерфейс Controller, гораздо больше шансов что вы расширите один из классов ниже в иерархии. В то время как интерфейс Controller определяет основной контракт между Контроллером и Spring MVC, различные классы Контроллера обеспечивают дополнительную функциональность за рамками этих основ.
Широкий выбор классов Контроллеров это одновременно благословение и проклятие. В отличие от других веб-фреймворков, которые заставят вас работать с единственным типом объекта контроллера (например, Action класс в Struts), Spring позволяет вам выбрать Контроллер, который наиболее подходит для ваших нужд. Тем не менее, с таким обилием классов Контроллера , многие разработчики оказываются перегружены и не знаю, как решить что же и выбрать .
Чтобы помочь вам решить, какой класс Контроллера наследовать в Контроллерах вашего приложения, рассмотрим таблицу 13.2. Как вы можете видеть, классы Spring Контроллеров могут быть сгруппированы в шесть категорий, которые обеспечивают все больше функциональности (и представляют больше сложности) по мере продвижения вниз по таблице. Вы можете также заметить из Рисунка 13.5, что (за исключением ThrowawayController) по мере продвижения вниз Контроллер-иерархии, каждый нижележащий Контроллер основан на функциональности Контроллеров , что над ним.
Таблица 13.2 Spring MVC в выборе класса контроллера.
Тип | Классы | Полезно, когда ... |
---|---|---|
View | ParameterizableViewController UrlFilenameViewController | Ваш Контроллер нужен только для отображения статических Видов - обработки или поиска данных не требуется. |
Simple | Controller (интерфейс) AbstractController | Ваш Контроллер предельно прост и требует немного больше функциональных возможностей, чем предоставляется базисными Java сервлетами . |
Разовый | ThrowawayController | Вы хотите простой способ обработки запросов, как команд (по аналогии с Actions в WebWork). |
MultiAction | MultiActionController | Ваше приложение имеет несколько действий, которые выполняют аналогичные или связанные логики. |
Command | BaseCommandController AbstractCommandController | Ваш Контроллер будет принимать один или несколько параметров из запроса и связывать их в объект. Он также, будет способные выполнять проверки параметров. |
Form | AbstractFormController SimpleFormController | Вы должны отобразить форму ввода для пользователя, а также обрабатывать данные, введенные в форму. |
Wizard | AbstractWizardFormController | Вы хотите, по-шагово проводить пользователя через сложные, многостраничные формы ввода, которые в конечном итоге должны обрабатываться как единая форма. |
Вы уже видели пример простого контроллера, который расширяет AbstractController. В листинге 13.1, HomePageController расширяет AbstractController и получает список последних тирады для отображения на главной странице. AbstractController является идеальным выбором, потому что домашняя страница весьма проста и не принимает ввода от пользователя.
Основывать ваш Контроллер на AbstractController это прекрасно, если вам не нужно особой мощности. Большинство Контроллеров , однако, будут более интересным, принимая параметры и требования проверки этих параметров. В следующих разделах, мы собираемся построить несколько Контроллеров , которые определяют веб-слой приложений RoadRantz за счет расширения других реализаций классов Контроллеров (представленных на рисунке 13.6), начиная с Command-контроллеров.
Обработка Команд
Это довольно обычно для веб-запроса, содержать один или несколько параметров, которые используются сервером для формирования определенных результатов. Например, одним из требований для приложения RoadRantz является способность, отображать список тирад для какого либо одного конкретного транспортного средства.
Конечно, вы можете расширить AbstractController и извлекать параметры нужные вашему Контроллеру непосредственно из HttpServletRequest. Но тогда Вы должны были бы также написать логику, которая превращает данные из запроса в бизнес-объекты, и вам также придется поместить логику проверки этих данных в код Контроллера. Хотя, на самом деле, всей этой логике в Контроллере не место.
В случае, когда вам необходим Контроллер для выполнения действий основанных на параметрах, ваш класс Контроллера должен наследовать класс command-контроллера, такой как - AbstractCommandController
Как показано на рисунке 13.7, command-контроллер автоматически превращает параметры запроса в объект - Команда. Сommand-контроллеры также могут быть связаны с подключаемыми валидаторами для проверки того, являются ли параметры запроса - действительными.
Листинг 13.3 показывает класс command-контроллера RantsForVehicleController, который используется для отображения списка тирад, введенных для конкретного транспортного средства.
Листинг 13.3 RantsForVehicleController, перечисляет все тирады для конкретного транспортного средства
package com.roadrantz.mvc;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;
import com.roadrantz.domain.Vehicle;
import com.roadrantz.service.RantService;
public class RantsForVehicleController
extends AbstractCommandController {
public RantsForVehicleController() {
// Sets command class, name
setCommandClass(Vehicle.class);
setCommandName("vehicle");
}
protected ModelAndView handle(HttpServletRequest request,
HttpServletResponse response, Object command,
BindException errors) throws Exception {
Vehicle vehicle = (Vehicle) command; //Casts command
//object to Vehicle
List vehicleRants = //Uses RantService to retrieve list
rantService.getRantsForVehicle(vehicle)); // of rants
Map model = errors.getModel(); //Creates the
model.put("rants",4 //model
rantService.getRantsForVehicle(vehicle));
model.put("vehicle", vehicle);
//Returns model
return new ModelAndView("vehicleRants", model);
}
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
}
Метод handle() класса RantsForVehicleController является основным используемым методом для AbstractCommandController. Этот метод немного более интересен, чем handleRequestInternal() из AbstractController. В дополнение к HttpServletRequest и HttpServletResponse, handle() принимает Object, который и есть - Команд для Контроллера.
Объект Команды это бин, который предназначенный для хранения параметров запроса легкодоступной форме. Если вы знакомы с Jakarta Struts, вы можете распознать объект Команды в Spring как аналогичный с ActionForm в Struts. Ключевая разница состоит в том, что в отличие от Struts-"команды", которая должна расширять ActionForm, объект Команды в Spring это обычный POJO, которому не нужно наследовать никаких Spring-специфичных классов.
В этом случае объект Команды является экземпляром Vehicle(автомобиля), как это указано в конструкторе Контроллера. Вы также можете опознать Vehicle в качестве доменного класс, который описывает автомобиль в главе 5. Хотя экземпляры Команд и не обязаны быть экземплярами доменных классов, однако это довольно удобно, если конечно - реализуемо. Действительно, Vehicle уже определяет все данные необходимые в RantsForVehicleController. Удобно, и что именно этот же самый тип, необходим методу getRantsForVehicle() класса RantService. Это делает Vehicle практически идеальным выбором для класса Команды.
Перед вызовом метода handle(), Spring будет пытаться выявить соответствие любых параметров, переданных в запросе, со свойствами объекта Команды. Vehicle имеет два свойства: state и plateNumber. Если запрос содержит параметры с такими же именами, их значения будут автоматически привязаны к одноименным свойствам Vehicle.
Как и HomePageController, вы также должны зарегистрировать RantsForVehicleController в roadrantz-servlet.xml:
<bean id="rantsForVehicleController"
class="com.roadrantz.mvc.RantsForVehicleController">
<property name="rantService" ref="rantService" />
</bean>
Сommand-контроллеры позволяют легко обрабатывать запросы с параметрами путем автоматического связывания параметров запроса с полями командных объектов. Параметры запроса могут быть заданы как в виде URL-параметров (что весьма вероятно в случае с RantsForVehicleController) так в виде полей из веб-формы. Хотя сommand-контроллер и может обрабатывать ввод из формы, однако Spring предоставляет другой тип контроллера с лучшей поддержкой для обработки форм. Давайте рассмотрим form-контроллеры Spring следующим шагом.
Обработка содержания форм
В типичных веб-приложение, вы, вероятно, столкнетесь по крайней мере c одной формой, которую необходимо будет заполнить. Когда вы подтверждаете заполнение этой формы, данные, которые вы ввели отправляются на сервер для обработки, и после завершения обработки, вам либо будет выдана страница подтверждающую успешность действия, либо выданы страницы формы с ошибками в вашей отправленной форме, которые необходимо будет исправить.
Базовая функциональность приложения RoadRantz предусматривает возможность вводить напыщенную речь(тираду) о том или ином транспортном средстве. В приложении, пользователю будет представлена форма для ввода его тирады. После отправки этой формы, ожидается, что эта напыщенная речь будет сохранена в базе данных для последующего просмотра.
При реализации процесса отправки тирады, вы могли бы прельститься идеей расширить AbstractController для отображения формы и расширить AbstractCommandController для обработки формы. Это, безусловно, может работать, но будет в конечном итоге более трудным, путем, чем это необходимо. Вы должны были бы поддерживать два разных Контроллера, которые работают в тандеме, чтобы обрабатывать отправку тирад. Не проще было бы иметь всего один контроллер, чтобы обрабатывать как отображение формы так и прием ее ввода?
Тогда, т.н. form-контроллер это именно то, что вам нужно. Form-контроллеры продвинули концепцию command-контроллеров на шаг вперед, как показано на рисунке 13.8, добавив функциональность для отображения формы (когда получен HTTP GET запрос), и обработки ввода формы (когда получен HTTP POST запрос). Кроме того, если возникнут ошибки при обработке формы, Контроллер будет помнить, о потребности в повторном отображении формы, так что пользователь может исправить ошибки и отправить форму еще раз.
Чтобы проиллюстрировать, как работают form-контроллеры , рассмотрим AddRantFormController в листинге 13.4.
Листинг 13.4 контроллер для добавления новых тирады
package com.roadrantz.mvc;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import com.roadrantz.domain.Rant;
import com.roadrantz.domain.Vehicle;
import com.roadrantz.service.RantService;
public class AddRantFormController extends SimpleFormController {
private static final String[] ALL_STATES = {
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FL",
"GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME",
"MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH",
"NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI",
"SC", "SD", "TN", "TX", "UT", "VA", "VT", "WA", "WV", "WI",
"WY"
};
public AddRantFormController() {
setCommandClass(Rant.class); //Sets command
setCommandName("rant"); //class, name
}
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
Rant rantForm = (Rant) super.formBackingObject(request);
rantForm.setVehicle(new Vehicle()); //Sets up Rant command
return rantForm; //with blank Vehicle
}
protected Map referenceData(HttpServletRequest request)
throws Exception {
Map referenceData = new HashMap();
referenceData.put("states", ALL_STATES);//Provides list of
return referenceData; //states for form
}
protected ModelAndView onSubmit(Object command,
BindException bindException) throws Exception {
Rant rant = (Rant) command;
rantService.addRant(rant); // <---Adds new rant
return new ModelAndView(getSuccessView());
}
private RantService rantService;
public void setRantService(RantService rantService) {
// Handling web requests
this.rantService = rantService;
}
}
Хотя это и не очевидно из листинга, но AddRantFormController несет ответственность как за отображения формы для ввода тирад, так и за обработку результатов заполнения этой формы. Когда Контроллер получает HTTP GET запрос, он направит этот запрос в Вид формы. А когда он получает HTTP POST запрос, метод onSubmit() будет обрабатывать содержание формы.
Метод referenceData() не является обязательным, но он очень удобен, когда нужно предоставить любую дополнительную информацию для отображения в форме. В данном случае, нашей форме понадобится перечень Штатов, который будет отображаться (предположительно в выпадающем списке выбора). Таким образом, метод referenceData() класса AddRantFormController добавляет массив строк, содержащий все 50 штатов США, а также округ Колумбия.
При нормальных обстоятельствах, объект Команды, который поддерживает форму, является просто экземпляром класса Команды. В случае AddRantFormController, однако, простого экземпляра Rant не будет. Форма собирается использовать вложенное внутри Rant свойство Vehicle, как часть объекта поддержки для формы . Поэтому было необходимо переопределить formBackingObject() для установки свойства vehicle. В противном случае, NullPointerException будет брошен, при попытке Контроллера связывать свойства state и plateNumber с параметрами запроса.
Метод onSubmit() обрабатывает содержание формы(запрос HTTP POST), передав объект Команды (являющийся экземпляром Rant), в метод addRant() из внедренной ссылки на RantService .
Что не понятно из листинга 13.4, так это то - как этот Контроллер узнает, что следует показывать форму ввода тирад Также не ясно, куда попадет пользователь после успешного добавления тирады. Единственный намек - это то, что результатом вызова для getSuccessView() дается - ModelAndView. Но откуда же берется Вид подтверждения успешности ввода?
SimpleFormController разработан таким образом, чтобы детали Вида, держать как можно подальше от Java-кода Контроллера. Вместо жесткого кодирования объектов ModelAndView, вы настраиваете form-контроллер в файле конфигурации контекста, следующим образом:
<bean id="addRantController"
class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
</bean>
Так же как и другие Контроллеры, бин addRantController связывается с любым сервисом, с который ему возможно, потребуется (например, rantService). Но здесь вы также устанавливаете свойства formView и successView. Свойство formView - это логическое имя Вида отображаемого, в случае когда Контроллер получает запрос HTTP GET (или при любом возникновении ошибок). Аналогично, successView - это логическое имя Вида , служащего для подтверждения, успешной отправки формы. Арбитр Вида , будет использовать эти логические имена, чтобы найти требуемый по ситуации фактический объект Вида (View), для генерации отображаемой страницы.
Проверка формы ввода
Когда AddRantFormController вызывает addRant(), важно убедиться в полноте и правильности всех данных помещаемых, в объект Rant. Вы же не хотите, чтобы пользователи могли ввести только имя Штата не введя при этом номерных знаков (или наоборот). Кроме того, какой смысл в том чтобы правильно ввести и название Штата и номерной знак машины, но при этом не набрать никакого текста тирады? И важно, чтобы пользователь не мог ввести номерной знак, являющийся - недопустимым.
Валидацию в Spring MVC обеспечивает интерфейс org.springframework.validation.Validator. Он объявлен следующим образом:
public interface Validator {
void validate(Object obj, Errors errors);
boolean supports(Class clazz);
}
Реализации этого интерфейса должны изучить поля объекта, переданного в метод validate() и отвергнуть любые недопустимые значения посредством объекта Errors. Метод supports() используется, чтобы помочь Spring определить может ли валидатор использоваться для данного класса.
RantValidator (листинг 13.5) является реализацией Validator используемой для проверки объекта-команды Rant.
Листинг 13.5 Проверка занесения Rant
package com.roadrantz.mvc;
import org.apache.oro.text.perl.Perl5Util;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.roadrantz.domain.Rant;
public class RantValidator implements Validator {
public boolean supports(Class clazz) {
return clazz.equals(Rant.class);
}
public void validate(Object command, Errors errors) {
Rant rant = (Rant) command;
ValidationUtils.rejectIfEmpty(
errors, "vehicle.state", "required.state",
"State is required.");
ValidationUtils.rejectIfEmpty(
errors, "vehicle.plateNumber", "required.plateNumber",
"The license plate number is required.");
ValidationUtils.rejectIfEmptyOrWhitespace( // Validates
errors, "rantText", "required.rantText", // required
"You must enter some rant text."); // fields
validatePlateNumber( // Validates plate
rant.getVehicle().getPlateNumber(), errors); // numbers
}
private static final String PLATE_REGEXP =
"/[a-z0-9]{2,6}/i";
private void validatePlateNumber(
String plateNumber, Errors errors) {
Perl5Util perl5Util = new Perl5Util();
if(!perl5Util.match(PLATE_REGEXP, plateNumber)) {
errors.reject("invalid.plateNumber",
"Invalid license plate number.");
}
}
}
Единственное, что нужно сделать, это настроить AddRantFormController чтобы использовать RantValidator. Вы можете сделать это, связывая RantValidator (показан здесь как внутренний бин) в бин AddRantFormController-а:
<bean id="addRantController"
class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
<property name="validator">
<bean class="com.roadrantz.mvc.RantValidator" />
</property>
</bean>
Когда напыщенная речь введена, если все требуемые свойства установлены, и если номерной знак проходит проверку на корректность, будет вызван метод onSubmit() в AddRantFormController и тирада будет добавлена. Однако, если RantValidator отвергнет любое из полей, пользователю будет возвращен Вид формы, чтобы исправить ошибки.
За счет реализации интерфейса Validator, вы можете программно получить полный контроль над проверкой объектов-Команд в приложении. Это может быть замечательно, если ваши потребности проверки являются сложными и требуют специальной логики.
Тем не менее, в простых случаях, таких как обеспечение обязательных полей и основное форматирование, писать собственную реализацию интерфейса Validator , это как то уж, слишком.... Было бы неплохо, если бы мы могли написать правила проверки - декларативно, место того, чтобы писать их в коде Java. Давайте посмотрим, как использовать декларативные проверки в Spring MVC
Проверка с Commons Validator
Можно услышать жалобу, в адрес Spring MVC, что валидацию посредством интерфейса Validator, даже близко не сравнить, с возможностями проверки доступными в Jakarta Struts. Мы не можем спорить с этим фактом. Jakarta Struts действительно имеет очень хорошие возможности для объявления правил проверки за пределами кода Java. Однако, хорошей новостью, является - то, что мы можем производить подобные декларативные проверки и в Spring MVC.
Но прежде чем вы отправитесь копаться в JavaDoc Spring в поисках декларативной реализации Validator, вы должны знать, что у Spring - НЕТ такого валидатора. На самом деле, Spring вообще не содержит какой-либо реализаций интерфейса Validator, предлагая вам "с нуля" написать свою собственную.
Тем не менее, вам не придется отправляься очень далеко, чтобы найти готовые реализации Validator, поддерживающую декларативную проверку. Проект Spring Modules ( https://springmodules.dev.java.net ) является дочерним проектом Spring, который предоставляет ряд расширений Spring, область действия которых превышает основной проект. Одним из таких расширений является модуль валидации, использующий для обеспечения декларативной проверки Jakarta Commons Validator ( https://jakarta.apache.org/commons/validator ).
Для использования модуля проверки в вашем приложении, вы начнете с того, чтобы сделать файл springmodules-validator.jar доступным в classpath приложения. Если вы используете для сборки Ant, вам необходимо скачать дистрибутив Spring Modules (лично я использую версию 0.6) и найти файл spring-modules-0.6.jar в папке dist. Затем добавьте этот JAR, в <lib>-секцию для <war>-задачи, чтобы убедиться, что файл будет помещен в WEB-INF/lib папку WAR-файла приложения.
Если вы используете Maven 2, чтобы сделать вашу сборку (как это делаю - я), вам нужно добавить следующие <dependency> в pom.xml:
<dependency>
<groupId>org.springmodules</groupId>
<artifactId>springmodules-validation</artifactId>
<version>0.6</version>
<scope>compile</scope>
</dependency>
Вы также должны добавить Jakarta Commons Validator JAR в classpath вашего приложения. В Maven 2, это будет выглядеть следующим образом:
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.1.4</version>
<scope>compile</scope>
</dependency>
Spring Modules обеспечивает реализацию Validator, называемую - DefaultBeanValidator. Этот класс настраивается в roadrantz-servlet.xml следующим образом:
<bean id="beanValidator"
class= "org.springmodules.commons.validator.DefaultBeanValidator">
<property name="validatorFactory" ref="validatorFactory" />
</bean>
Класс DefaultBeanValidator не делает каких-либо фактических работ по проверке. Вместо этого, он делегируте Commons Validator-у проверки значений полей. Как вы можете видеть, DefaultBeanValidator имеет свойство validatorFactory связанное со ссылкой на бин validatorFactory. Этот бин, объявляется, используя следующий XML:
<bean id="validatorFactory"
class="org.springmodules.commons.validator.DefaultValidatorFactory">
<property name="validationConfigLocations">
<list>
<value>WEB-INF/validator-rules.xml</value>
<value>WEB-INF/validation.xml</value>
</list>
</property>
</bean>
DefaultValidatorFactory это класс, который загружает конфигурацию Commons Validator от имени DefaultBeanValidator. Свойство validationConfigLocations принимает список из одного или нескольких файлов конфигураций проверки. Здесь мы использовали его для загрузки двух конфигураций: validator-rules.xml и validation.xml.
Файл validator-rules.xml содержит набор предопределенных правил проверок для общих целей, таких например как проверка электронной почты и номеров кредитных карт. Этот файл поставляется с дистрибутивом Commons Validator, так что вам не придется писать его самостоятельно - просто добавьте данный файл в WEB-INF каталог вашего приложения. Таблица 13.3 перечисляет все правила проверки, имеющиеся в validator-rules.xml.
Таблица 13.3 правил проверки Commons Validator, доступные в файле - validator-rules.xml.
Правило проверки !! Что оно проверяет | |
byte | Это поле содержит значение, которое может быть отнесено к byte |
сreditCard | Это поле содержит строку, которая проходит LUHN-проверку и, таким образом, это допустимый номер для кредитной карты номер |
date | Это поле содержит значение, которое соответствует формату Date |
double | Это поле содержит значение, которое может быть отнесено к double |
Это поле содержит строку, которая, может являться, адресом электронной почты | |
float | Это поле содержит значение, которое может быть отнесено к float |
floatRange | Это поле содержит значение, которое находится в пределах диапазона значений float |
??intRange | Это поле содержит значение, которое попадает в диапазон значений int |
integer | Это поле содержит значение, которое может быть отнесено к int |
long | Это поле содержит значение, которое может быть отнесено к long |
mask | Это поле содержит строковое значение, которое соответствует заданной маске |
maxlength | Это поле содержит не более чем заданное число символов |
minlength | Это поле имеет по крайней мере определенное количество символов |
required | Это поле не пустое |
requiredif | Это поле не пустое, но только если нет другого критерия |
short | Это поле содержит значение, которое может быть отнесено к short |
Другой файл - validation.xml, определяет правила проверки специфичные для конкретного приложения, и применяются непосредственно в приложении RoadRantz. Листинг 13.6 показывает содержимое validation.xml применительно к RoadRantz.
Листинг 13.6 Объявление проверки в RoadRantz
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
"-//Apache Software Foundation//DTD
Commons Validator Rules Configuration 1.1//EN"
"https://jakarta.apache.org/commons/dtds/validator_1_1.dtd">
<form-validation>
<formset>
<form name="rant">
<!--Requires rant text-->
<field property="rantText" depends="required">
<arg0 key="required.rantText" />
</field>
<!--Requires vehicle state -->
<field property="vehicle.state" depends="required">
<arg0 key="required.state" />
</field>
<!--Requires and masks plate number-->
<field property="vehicle.plateNumber"
depends="required,mask">
<arg0 key="invalid.plateNumber" />
<var>
<var-name>mask</var-name>
<var-value>^[0-9A-Za-z]{2,6}$</var-value>
</var>
</field>
</form>
</formset>
</form-validation>
Если содержимое validation.xml выглядит для вас странно знакомым, это вероятно, потому Struts использует те же файлы проверки XML. За кулисами, чтобы делать свою проверки, Struts использует - Commons Validator . А сейчас Spring Modules привнесли декларативную проверку в Spring.
Последнее, что необходимо сделать, это изменить объявление Контроллера, чтобы связать в него новую декларативную реализации Validator-a:
<bean id="addRantController"
class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
<property name="validator" ref="beanValidator" />
</bean>
Основное предположение в работе с SimpleFormController состоит в том, что форма - это одна страница. Это может быть хорошо, когда вы делаете что-то простое, например добавление всего одной тирады. Но что, если ваши формы являются сложными, требуя от пользователя ответить на несколько вопросов? В этом случае, возможно, имеет смысл разбить форму на несколько подразделов и водить пользователей через нее с помощью мастера. Давайте посмотрим, как Spring MVC может помочь вам построить мастера форм.
Обработка сложных форм с помощью мастеров
Еще одна из задуманных возможностей приложения RoadRantz состоит в том, что любой может зарегистрироваться в нем в качестве пользователя (как motorist (автомобилист)в терминах RoadRantz) и затем получать уведомления, если какие-либо тирады прозвучат об их автомобилях. Мы разработали уведомления о тирадах по электронной почте в главе 12. Значит мы также должны предоставить пользователям возможность зарегистрировать себя и свои транспортные средства. Мы могли бы поместить всю форму регистрации автомобилист в единую JSP и расширив SimpleFormController, обрабатывать и сохранять данные. Однако, мы не знаем, сколько автомобилей каждый из пользователей захочет зарегистрировать, и нам придется излишне изощряться, чтобы запрашивать у пользователя неизвестное количество данных о транспортных средствах в рамках одной формы.
Вместо создания одной формы, давайте разобьем регистрация автомобилиста на несколько подразделов и будем проводить нового пользователя через форму с помощью мастера. Предположим, что мы разбиваем вопросы процесса регистрации на три страницы:
- Общая информация пользователей, такие как фамилия, имя, пароль и адрес электронной почты
- Информация об автомобиле (название Штата и номерной знак)
- Подтверждение (возможность для пользователя проконтролировать введенную информацию перед отправкой формы)
К счастью, Spring MVC предлагает AbstractWizardFormController, чтобы помочь нам. AbstractWizardFormController является самым мощным из Контроллеров , которые идут со Spring. Как показано на рисунке 13.9, wizard-контроллер представляет собой особый тип form-контроллера, который собирает данные формы, состоящей из нескольких страниц в один общий объект-команду для последующей ее обработки. Давайте посмотрим, как построить многостраничную форму регистрации с использованием AbstractWizardFormController.
Создание основного wizard-контроллера
Для построения wizard-контроллера, необходимо расширить класс AbstractWizardFormController . Класс MotoristRegistrationController (листинг 13.7) показывает простейший wizard-контроллер, который будет использоваться для регистрации пользователей в RoadRantz.
Листинг 13.7 Регистрация автомобилистов с помощью мастера
package com.roadrantz.mvc;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractWizardFormController;
import com.roadrantz.domain.Motorist;
import com.roadrantz.domain.Vehicle;
import com.roadrantz.service.RantService;
public class MotoristRegistrationController
extends AbstractWizardFormController {
public MotoristRegistrationController() {
setCommandClass(Motorist.class); // Sets command
setCommandName("motorist"); // class, name
}
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
Motorist formMotorist = new Motorist();
List<Vehicle> vehicles = new ArrayList<Vehicle>(); // Creates form
vehicles.add(new Vehicle()); // backing object
formMotorist.setVehicles(vehicles);
return formMotorist;
}
protected Map referenceData(HttpServletRequest request,
Object command, Errors errors, int page) throws Exception {
Motorist motorist = (motorist) command;
Map refData = new HashMap();
if(page == 1 && request.getParameter("_target1") != null) {
refData.put("nextVehicle", // Increments next
motorist.getVehicles().size() - 1); //vehicle pointer
}
return refData;
}
protected void postProcessPage(HttpServletRequest request,
Object command, Errors errors, int page) throws Exception {
Motorist motorist = (Motorist) command;
if(page == 1 && request.getParameter("_target1") != null) {
motorist.getVehicles().add(new Vehicle()); //Adds new
} //blank vehicle
}
protected ModelAndView processFinish(HttpServletRequest request,
HttpServletResponse response, Object command,
BindException errors)
throws Exception {
Motorist motorist = (motorist) command;
// the last vehicle is always blank...remove it
motorist.getVehicles().remove(
motorist.getVehicles().size() - 1);
rantService.addMotorist(motorist); //Adds motorist
return new ModelAndView(getSuccessView(),
"motorist", motorist);
}
// injected
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
// returns the last page as the success view
private String getSuccessView() {
return getPages()[getPages().length-1];
}
}
Как и с любым command-контроллером, вы должны установить класс Команды, при использовании - wizard-контроллера. Здесь MotoristRegistrationController был настроен, на использование Motorist в качестве класса Команды. Из-за того что автомобилист может регистрировать не только одно, но и нескольких транспортных средств, метод formBackingObject() переопределяется для установки свойства vehicles в список объектов Vehicle. Список начинается с чистого объекта Vehicle, чтобы затем его поля заполнять - в форме.
Поскольку пользователь может зарегистрировать любое количество транспортных средств, и список автомобилей будет расти с каждым новым добавленным, Вид формы нуждается в способе знать: который из элементов списка будет являться следующим обрабатываемым элементом. Чтобы сделать индекс следующего автомобиля доступны для формы, метод referenceData() - переопределяется.
Единственным обязательным методом в AbstractWizardFormController является processFinish(). Этот метод вызывается чтобы финализировать форму, когда пользователь закончил ее заполнения (по-видимому, нажав на кнопку "Submit"). В MotoristRegistrationController, метод processFinish() посылает данные заключенные в объекте Motorist методу addMotorist() во внедренном объекте RantService .
Обратите внимание, что в MotoristRegistrationController, нет ничего указывающего на то, что страницы? составляют - форму. Или на порядок появления страниц. Это потому, что AbstractWizardFormController выполняет большую часть работы, связанной с управлением рабочим процессом мастера как бы "под одеялом". Но как все-жеAbstractWizardFormController узнает, что страниц составляют - форму?
Часть из этого может стать более очевидной, если вы увидите - то, как MotoristRegistrationController объявлен в roadrantz-servlet.xml:
<bean id="registerMotoristController"
class="com.roadrantz.mvc.MotoristRegistrationController">
<property name="rantService" ref="rantService" />
<property name="pages">
<list>
<value>motoristDetailForm</value>
<value>motoristVehicleForm</value>
<value>motoristConfirmation</value>
<value>redirect:home.htm</value>
</list>
</property>
</bean>
Вот как мастер узнает, какие страницы составляют форму: список логических имен Видов? присваивается свойству - pages. Эти имена в конечном итоге будет разрешены в объекты Вида - Арбитром Видов. Но пока, мы лишь отметим для себя, что эти логические имена будут преобразованы в имена файлов JSP.
Хотя это это и проясняет как MotoristRegistrationController узнает, какие собственно страницы следует показывать, но это все еще не говорит нам о том, откуда же берется порядок их показа.
Пошагово сквозь страницы формы
Первой страницей отображаемой в любом wizard-контроллере, будет первая страница в списке, который задается свойством - pages . В нашем конкретном случае мастера регистрации "автомобилиста" , первой будет отображена страница - motoristDetailForm.
В вопросе выбора, к какой же следующей странице нужно переходить, объект AbstractWizardFormController консультирует один из его методов - getTargetPage(). Этот метод возвращает целое число, которое является индексом (от нуля) в списке задающем значения свойства - pages .
Реализация по умолчанию getTargetPage() определяет, следующую страницу для перехода, на основе параметра в запросе называемого - name значение которого должен начинается с _target и заканчивается числом. Метод getTargetPage() удаляет _target-префикс параметра и использует оставшееся число как индекс в списке страниц. Например, если запрос имеет параметр, с именем _target2, пользователь получит страницу генерируемую Видом motoristConfirmation.
Знание того, как getTargetPage() - работает, поможет вам настроить кнопки "Вперед" и "Назад" в коде HTML-страниц вашего мастера. Например, предположим, что пользователь находится на странице motoristVehicleForm (индекс = 1). Чтобы создать кнопки "Вперед" и "Назад" на этой странице, достаточно добавить в ее HTML код две submit-кнопки, с соответствующими именами (включающими _target префикс):
<form method="POST" action="feedback.htm">
…
<input type="submit" value="Back" name="_target0">
<input type="submit" value="Next" name="_target2">
</form>
При нажатии кнопки "Назад", параметр с name равным _target0, помещается в запрос отсылаемый обратно в MotoristRegistrationController. Метод getTargetPage() обработает этот параметр и отправить пользователя на страницу motoristDetailForm (индекс = 0). Аналогичным образом, если нажата кнопка "Вперед" , getTargetPage() будет обрабатывать параметр с name равным _target2 и решит отправить пользователю страницу motoristConfirmation (индекс = 2).
Поведение по умолчанию getTargetPage() является достаточным для большинства проектов. Однако, если вы хотели бы изменить это поведение, Вы можете переопределить данный метод.
Завершение мастера
Мы выяснили, как сделать шаг назад или шаг вперед через мастер формы. Но как вы можете сказать Контроллеру, что вы закончили, и что processFinish() метод должен быть вызван?
Для этого есть другой специальный параметр запроса, с name равным - _finish, который указывает, AbstractWizardFormController-у что пользователь закончил заполнение формы и хочет представить информацию для обработки. Так же, как параметры _targetX, _finish может быть использован для создания кнопку "Готово" на странице:
<form method="POST" action="feedback.htm">
…
<input type="submit" value="Finish" name="_finish">
</form>
Когда AbstractWizardFormController увидит _finish-параметр в запросе, он передаст управление методу processFinish() для окончательной обработки формы.
В отличие от других form-контроллеров, AbstractWizardFormController не обеспечивает средств настройки Вида для страницы подтверждения ввода формы. И реализация этой функциональности ложится на нас. Потому, мы добавили метод getSuccessView() в MotoristRegistrationController чтобы вернуть последнюю страницу в списке pages. Так что, после окончания ввода формы, метод processFinish() возвратит ModelAndView с последним по списку Видом из pages, в качестве Вида подтверждения ввода формы.
Отмена мастера
А что делать, если пользователь уже частично пройдя этапы регистрации вдруг решает, что он не хочет, завершать ее в данный момент? Как он может отказаться от сохранения введенных в форме данных?
Помимо очевидного ответа - "он может закрыть свой браузер", вы также можете добавить в форму кнопку "Отмена":
<form method="POST" action="feedback.htm">
…
<input type="submit" value="Cancel" name="_cancel">
</form>
Как вы видите, кнопка "Отмена" должна иметь _cancel в качестве name, чтобы, при ее нажатии, браузер помещал в запрос параметр именуемый - _cancel. Когда AbstractWizardFormController получит этот параметр, он передаст управление в метод processCancel().
По умолчанию, processCancel() бросает исключение о том, что операции отмены не поддерживается. Таким образом, вам необходимо переопределить этот метод, чтобы он (как минимум) отправлял пользователя к любой странице (на ваше усмотрение) если тот нажмет кнопку "Отмена". Данная реализация processCancel() отправляет пользователя в Вид подтверждения ввода формы:
protected ModelAndView processCancel(HttpServletRequest request,
HttpServletResponse response, Object command,
BindException bindException) throws Exception {
return new ModelAndView(getSucessView());
}
Если есть потребность в какой-либо работе по очистке и освобождению ресурсов, выполняемых в случае отмены, вы просто можете разместить этот код в методе processCancel() перед возвращением объекта ModelAndView.
Постраничная проверка в мастерах формы
Как и для любого command-контроллера, данные в объект Команды, wizard-контроллера могут быть проверены с помощью объекта Validator. Однако, есть одно небольшое - но.
С другими command-контроллерами, объект Команды полностью формируется - сразу. Но с wizard-контроллерами, объект Команды заполняется поэтапно, по мере того как пользователь делает шаги по страницам мастера. Используя мастера формы, не имеет смысла выполнять проверку - одномоментно (т.е. "ВСЁ - сразу"), потому что, если проверку провести слишком рано, вы вероятно обнаружите при проверке проблемы, вызванные тем, что пользователь не завершил работу мастера. И наоборот, после нажатии кнопки "Готово", будет уже слишком поздно для проверки, потому что любые найденные ошибки могут охватить несколько страницы (и как далеко пользователю теперь нужно будет вернуться?).
Вместо одномоментной проверки объекта Команды , wizard-контроллеры проверяют объект Команду - постранично. Это делается каждый раз, когда происходит меж-страничный переход, путем вызова метода validatePage(). Реализация по умолчанию для метода validatePage() - "пустая" (то есть, нет проверки), но вы можете переопределить его, предложив "свою цену".
К примеру, на странице motoristDetailForm вы спросите у пользователя его адрес электронной почты. Это поле не является обязательным, но если оно будет заполнено, то его содержимое должно быть в допустимом формате для адреса электронной почты. Следующий пример метода validatePage() показывает, как проверить адрес электронной почты, когда пользователь переходы со страницы motoristDetailForm:
protected void validatePage(Object command, Errors errors,
int page) {
Motorist motorist = (Motorist) command;
MotoristValidator validator =
(MotoristValidator) getValidator();
if(page == 0) {
validator.validateEmail(motorist.getEmail(), errors);
}
}
Когда пользователь переходит со страницы motoristDetailForm (индекс = 0), будет вызван метод validatePage() с 0, передаваемым в page аргумент. Первая делом validatePage() получают ссылку на объект Команду - Motorist и ссылку на объект MotoristValidator. Поскольку не нужно делать проверку электронной почты из любой другой страницы, validatePage() проверяет, что пользователь переходит со страницы 0.
На данный момент, вы можете выполнять проверку электронной почты непосредственно в методе validatePage(). Тем не менее, типичный мастер будет иметь несколько полей, которые должны быть проверены. Таким образом, validatePage() может стать довольно громоздким. Мы рекомендуем вам делегировать ответственность за филигранную проверку уровня отдельных полей в отдельные методы объекта Validator вашего Контроллера , как мы уже сделали здесь с вызовом validateEmail() метода в MotoristValidator.
Все это означает, что вы должны будете установить свойство validator при настройке Контроллера:
<bean id="registerMotoristController"
class="com.roadrantz.mvc.MotoristRegistrationController">
<property name="rantService" ref="rantService" />
<property name="pages">
<list>
<value>motoristDetailForm</value>
<value>motoristVehicleForm</value>
<value>motoristConfirmation</value>
<value>redirect:home.htm</value>
</list>
</property>
<property name="validator">
<bean class="com.roadrantz.mvc.MotoristValidator" />
</property>
</bean>
Важно знать, что в отличие от других command-контроллеров , wizard-контроллеры никогда не вызывают стандартной метод validate() их объекта Validator. Это потому, что метод validate() одномоментно проверяет весь объект Команды, и потому понятно, что по причинам изложенным выше для мастера это - бессмысленно.
Контроллеры, которые вы видели до сих пор являются частью одной иерархии, уходящей своими корнями к интерфейсу Controller . Хотя Контроллеры постепенно становятся все более сложными (и более мощными) по мере продвижения вниз по иерархии, все Контроллеры, реализующие интерфейс Controller чем-то напоминают друг-друга. Но прежде чем мы закончим наше обсуждение Контроллеров , давайте еще посмотрим на иной Контроллер, который очень отличается от прежних - "разовый"-контроллер.
Работа с "разовыми"-контроллерами
Последним Контроллером, который вы можете найти для себя полезным является - т.н "разовый"-контроллер. Несмотря на сомнительное название, "разовые"-контроллеры могут быть весьма полезны и просты в использовании. "Разовые"-контроллеры значительно проще, чем другие Контроллеры, о чем свидетельствует интерфейс ThrowawayController:
public interface ThrowawayController {
ModelAndView execute() throws Exception;
}
Для создания собственного "разового"-контроллера, все, что вы должны сделать, это реализовать этот интерфейс и поместить в логику программы в метод execute() . Довольно просто, не так ли?
Но, стойте. Как параметры, передаются в Контроллер? Выполняемым методам других Контроллеров даются HttpServletRequest и объекты Команд , из которых можно вытянуть все параметры запроса. Если-же метод execute() не принимает аргументов, то как может ваш Контроллер обрабатывать ввод данных пользователем?
Вы могли заметить на рисунке 13.5, что интерфейс ThrowawayController даже вне иерархии интерфейса Controller. Это потому, что "разовые"-контроллеры сильно отличаются от других Контроллеров . Вместо того, чтобы получать параметры через HttpServletRequest или объект Команды, "разовые"-контроллеры сами выступают в качестве своих собственных объектов Команды. Если вы когда-либо работали с WebWork, это может показаться вполне естественным, поскольку WebWork-"действия" ведут себя аналогичным образом.
Из требований к RoadRantz мы знаем, что нам нужно отобразить список тирад за определенный месяц, день и год. Мы могли бы реализовать это с помощью command-контроллера, как мы это делали с RantsForVehicleController (листинг 13.3). К сожалению, у нас не существует готового доменного объекта, который бы уже содержал месяц, день и год. Это означает, что мы должны были бы создать специальный класс Команд для хранения этих данных. Было бы совсем не трудно создать такой POJO, но возможно, есть путь и получше.
Вместо реализации RantsForDayController как command-контроллера, давайте выполним его его в виде ThrowawayController-а, как показано в листинге 13.8.
Листинг 13.8 "разовый"-контроллер, который выдает список тирад на данный день:
package com.roadrantz.mvc;
import java.util.Date;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.throwaway.ThrowawayController;
import com.roadrantz.service.RantService;
public class RantsForDayController implements ThrowawayController {
private Date day;
public ModelAndView execute() throws Exception {
List<Rant> dayRants = rantService.getRantsForDay(day); //Gets list
//of rants
return new ModelAndView("dayRants", "rants", dayRants);//for day
}
public void setDay(Date day) { //Binds day to
this.day = day; //request
}
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
}
Перед тем как RantsForDayController обрабатывает запрос, Spring будет вызывать метод setDay(), передавая в него значение параметра day из запроса. Затем в методе execute() , RantsForDayController просто передаст day, в rantService.getRantsForDay(), чтобы получить список тирад в этот день. Одна вещь, которая остается такой же, как и других Контроллерах то, что метод execute() должен возвращать объект ModelAndView, после своего завершения.
Так же, как с любым другим Контроллером , вы должны объявить "разовый"-контроллер в файле конфигурации контекста DispatcherServlet. Есть лишь одна небольшая разница, которую вы можете увидеть в этой конфигурации RantsForDayController:
<bean id="rantsForDayController"
class="com.roadrantz.mvc.RantsForDayController"
scope="prototype">
<property name="rantService" ref="rantService" />
</bean>
Обратите внимание, что scope-атрибут был установлен на prototype. Это то, за что "разовые"-контроллеры и получили свое название. По умолчанию все бины являются - singleton , и поэтому, пока вы не установите scope=" prototype", бин RantsForDayController будет повторно использоваться между запросами. Это значит, его свойства (которые должны отражать значения параметра запроса) также могут быть использован повторно. Настройка scope=" prototype" говорит Spring выбросить Контроллер после того, как он был использован, и создавать "свежий" экземпляр его, для каждого нового запроса.
Есть еще одна вещь, которую следует сделать прежде, чем мы сможем использовать наши "разовые"-контроллеры. DispatcherServlet знает, как направить запросы к Контроллерам благодаря использованию адаптера-обработчика. Концепция адаптеров-обработчиков это нечто, о чем вы обычно не должны волноваться, потому что DispatcherServlet использует адаптер-обработчика по умолчанию , который отправляет запросы к Контроллерам в иерархии интерфейса Controller.
Но из-за того что ThrowawayController находится вне иерархии Controller, то DispatcherServlet не знает, как ему общаться с ThrowawayController. Чтобы заставить его работать, вы должны сообщить DispatcherServlet-у о необходимости использовать другой адаптер-обработчика. В частности, необходимо настроить ThrowawayControllerHandlerAdapter следующим образом:
<bean id="throwawayHandler"
class="org.springframework.web.servlet.mvc.
throwaway.ThrowawayControllerHandlerAdapter"/>
Объявляя этот бин - так, вы говорите DispatcherServlet, заменить его адаптер-обработчика по умолчанию на ThrowawayControllerHandlerAdapter.
Это нормально, если ваше приложение не содержит ничего, кроме "разовых"-контроллеров. Но приложение RoadRantz будет использовать как "разовые" так и обычные ("регулярные") контроллеры рядом друг с другом в одном приложении.
Следовательно, Вы все еще нуждаетесь, чтобы DispatcherServlet мог использовать также и "регулярный" адаптер-обработчика - SimpleControllerHandlerAdapter. Потому, вам необходимо объявить в вашей конфигурации и SimpleControllerHandlerAdapter, сделав это - следующим образом:
<bean id="simpleHandler"
class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
Объявление как обработчик адаптеров позволяет смешивать оба типа контроллеров в одном приложении.
Независимо от того, какая функциональность вашими Контроллерами выполняется, в конечном итоге, им всем придется вернуть некоторые результаты - пользователю. Страницы результатов генерируются и отображаются - Видами, которые будут выбраны по их логическое именам, задаваемым при создании объектов ModelAndView. Однако должен существовать механизм для отображения логических имен в фактические Виды , которые и будут показывать - ответ. Мы увидим, его в главе 14, когда мы обратим наше внимание на арбитров-видов Spring.
Но для начала, обратили ли вы внимание, что все Контроллеры Spring MVC объявлены, генерирующими - исключения? Вполне ведь возможно, что все может пойти наперекосяк, когда Контроллер обрабатывает запрос. Если исключение выброшено из Контроллера, то что пользователь видит? Давайте выясним, как контролировать поведение ошибочных контроллеров с арбитром исключения.
Обработка исключений
Наклейка для бампера - гласит: "Ошибка это не опция. Она входит в обязательную поставку с программным обеспечением" Если отбросить юмор то это формулировка универсальной истины. Не все всегда идет как задумано, и даже - в программном обеспечении. Когда ошибка происходит (а это неизбежно произойдет), вы ведь наверное хотели бы, чтобы пользователи вашего приложения, увидел не загадочную трассировку стека, а - понятное дружественным сообщение? Как же можно изящно сообщать об ошибках в программе вашим пользователям?
Когда исключение генерируется - Контроллером, то Арбитр Исключений SimpleMappingExceptionResolver приходит к нам на помощь. Используйте следующее определение <bean> , чтобы настроить SimpleMappingExceptionResolver для элегантной обработки любых java.lang.Exceptions выброшенных из Контроллеров Spring MVC:
<bean id="exceptionResolver"
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.Exception">friendlyError</prop>
</props>
</property>
</bean>
Свойство ExceptionMappings принимает карту java.util.Properties, которая содержит отображение полных имен классов исключений - в логические имена Видов. В приведенном случае базовый класс исключений, отображается на Вид с логическим именм friendlyError. Так, что в случае возникновения какой-либо ошибки, пользователям не придется видеть загадочные трассировки стека в своем браузере.
Когда Контроллер генерирует исключение, Арбитр Исключений SimpleMappingExceptionResolver преобразует его в логический friendlyError, который, в свою очередь, будет преобразован в фактически Вид с использованием соответственно настроенных Арбитров Вида. Если InternalResourceViewResolver будет настроен, тогда возможно, пользователю будет отправлена страница, заданная в /WEB-INF/jsp/friendlyError.jsp.
Резюме
Spring Framework поставляется с мощным и гибким веб-фреймворком, который сам построен на основе принципов Spring слабой связи, внедрения зависимостей и расширяемости.
Spring предлагает множество обработчиков-маппинга, которые помогают выбрать соответствующий Контроллер для обработки поступившего запроса. У вас также есть возможность выбора, среди нескольких методов определения соответствия URL-адресов - Контроллерам Эти методы основываются на:
- имени бина Контроллера
- имени класса Контроллера
- простом отображения URL-к-Контроллеру
- метаданных в исходном коде.
Для обработки запроса, Spring предоставляет широкий выбор классов Контроллеров разной сложности, начиная от очень простого интерфейса Controller, и заканчивая очень мощными wizard-контроллерами. Наличие нескольких слоев между ними, позволяет Вам всегда выбрать Контроллер с необходимым количеством функций(и не более сложный, чем требуется). Это позиционирует Spring отдельно от других веб-платформ MVC, таких как Struts или WebWork, где Ваш выбор ограничен только одним или двумя Action-классами.
В общем случае, Spring MVC поддерживает слабую связь между тем, как выбирается Контроллер чтобы обработать запрос и как выбирается Вид чтобы отобразить результаты запроса. Это мощная концепция для создания веб-слоя наиболее подходящего для Вашего приложения.
В этой главе Вы приняли участие в беглом туре, посвященном тому как Spring MVC обрабатывает запросы. По пути Вы также увидели, как устроена большая часть веб-слоя приложений RoadRantz.
Независимо от того, какая функциональность обеспечивается Контроллером, Вас в конечном счете более всего будет интересовать то, что должен получить и увидеть пользователь. Итак, в следующей главе, мы опираясь на Spring MVC, будем создавать слой Вида для приложения RoadRantz. В дополнение к JSP, Вы узнаете, как использовать в качестве альтернативы - языки шаблонов, такие как Velocity и FreeMarker. И Вы также узнаете, как динамически генерировать не-HTML-контент, в частности: Excel таблицы, PDF документы, и RSS-каналы.
------------------------
ТРИО теплый пол отзыв
Заработок на сокращении ссылок
Earnings on reducing links
Код PHP на HTML сайты
Категория: Книги по Java
Комментарии |