-
Notifications
You must be signed in to change notification settings - Fork 56
Введение в spring web mvc
Создадим в среде NetBeans проект Maven Java приложения и добавим все необходимые зависимости для построения web-приложения.
Добавляем зависимости для Spring Framework в pom.xml
, вынеся версии в раздел свойств.
pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>4.10</junit.version>
<spring.version>3.1.3.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
В качестве сервера сервлетов будем использовать сервер Jetty, а точнее плагин для maven, который позволяет развёртывать приложение в сервере Jetty.
Добавим в pom.xml
описание загрузки плагина для jetty и произведём соответствующие настройки.
Рассмотрим настраиваемые свойства:
-
scanIntervalSeconds
- интервал в секундах через который Jetty проверяет директории на изменения для редеплоя приложения -
connectors
- список подключений -
port
- настраивает порт, на котором будет запущен jetty
pom.xml
<build>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>8.1.8.v20121106</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<stopKey>foo</stopKey>
<stopPort>9999</stopPort>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>9090</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
</configuration>
</plugin>
</plugins>
</build>
После подключения плагина стало возможным проводить развёртывание проекта с использованием команды mvn jetty:run
.
Попробуем запустить проект.
$ mvn jetty:run
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building pres_0_3 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> jetty-maven-plugin:8.1.8.v20121106:run (default-cli) @ pres_0_3 >>>
[INFO]
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ pres_0_3 ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/wiz/TRASH/myspringlearning/pres_0_3/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ pres_0_3 ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.5:testResources (default-testResources) @ pres_0_3 ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/wiz/TRASH/myspringlearning/pres_0_3/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ pres_0_3 ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] <<< jetty-maven-plugin:8.1.8.v20121106:run (default-cli) @ pres_0_3 <<<
[INFO]
[INFO] --- jetty-maven-plugin:8.1.8.v20121106:run (default-cli) @ pres_0_3 ---
[INFO] Configuring Jetty for project: pres_0_3
[INFO] webAppSourceDirectory not set. Defaulting to /home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] Classes = /home/wiz/TRASH/myspringlearning/pres_0_3/target/classes
[INFO] Context path = /
[INFO] Tmp directory = /home/wiz/TRASH/myspringlearning/pres_0_3/target/tmp
[INFO] Web defaults = org/eclipse/jetty/webapp/webdefault.xml
[INFO] Web overrides = none
[INFO] web.xml file = null
[INFO] Webapp directory = /home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp
2012-11-17 20:33:08.842:INFO:oejs.Server:jetty-8.1.8.v20121106
2012-11-17 20:33:09.901:INFO:oejpw.PlusConfiguration:No Transaction manager found - if your webapp requires one, please configure one.
Null identity service, trying login service: null
Finding identity service: null
2012-11-17 20:33:12.678:INFO:/:No Spring WebApplicationInitializer types detected on classpath
2012-11-17 20:33:13.749:INFO:oejsh.ContextHandler:started o.m.j.p.JettyWebAppContext{/,file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp},file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp
2012-11-17 20:33:13.749:INFO:oejsh.ContextHandler:started o.m.j.p.JettyWebAppContext{/,file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp},file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp
2012-11-17 20:33:13.750:INFO:oejsh.ContextHandler:started o.m.j.p.JettyWebAppContext{/,file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp},file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp
2012-11-17 20:33:14.063:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:9090
[INFO] Started Jetty Server
[INFO] Starting scanner at interval of 10 seconds.
^C2012-11-17 20:33:18.002:INFO:oejsl.ELContextCleaner:javax.el.BeanELResolver purged
2012-11-17 20:33:18.002:INFO:oejsh.ContextHandler:stopped o.m.j.p.JettyWebAppContext{/,file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp},file:/home/wiz/TRASH/myspringlearning/pres_0_3/src/main/webapp
2012-11-17 20:33:18.058:INFO:oejut.ShutdownThread:shutdown already commenced
[INFO] Jetty server exiting.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Теперь можно приступать к созданию директорий для web-приложения.
В Maven web-приложения располагаются в директории webapp
.
Всё содержимое web-приложения по требованиям Java должно располагаться в директории WEB-INF
(по аналогии с META-INF
).
Создаём директорию src/main/webapp
и src/main/webapp/WEB-INF
.
Теперь необходимо создать дескриптор развертывания для сервлетов (данный файл используется сервером сервлетов, в нашем случае Jetty).
Создаём src/main/webapp/WEB-INF/web.xml
web.xml
<?xml version="1.0" ?>
<web-app 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"
version="3.0">
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Данные файл описывает запускаемые сервлеты и мэппинг сервлетов на пути на web-сервере.
Рассмотрим секции подробнее:
-
servlet
- задаёт описание сервлета -
servlet-name
- название сервлета, которое будет использовано для мэппинга -
servlet-class
- задаёт класс сервлета (в нашем случае это будетorg.springframework.web.servlet.DispatcherServlet
) -
load-on-startup
- задаёт загрузку сервлета при старте сервера -
servlet-mapping
- описывает отображение частей пути в URL на сервлеты -
url-pattern
- шаблон URL, при отправке запроса на который будет задействован сервлет
В файле web.xml
мы описали, что будет использоваться сервлет диспетчер, рассмотрим работу и необходимость наличия сервлета диспетчера.
Сервлет диспетчер является точкой входа для всех запросов. Фактически данный сервлет реализует шаблон FrontController, то есть принимает запросы, обрабатывает и отдаёт необходимому контроллеру.
В данном случае в качестве серлета диспетчера используется сервлет ``org.springframework.web.servlet.DispatcherServlet`, который входит в состав Spring.
Действия выполняемые данным сервлетом:
- Приём запроса
- Обработка запроса соответствующими фильтрами
- Вызов соответствующих
listener'ов
- Определение класса контроллера
- Передача управления на метод класса контроллера
- Получение от контроллера объекта модели и имени шаблона
- Определение шаблона по имени (
ViewResolver
) - Рендеринг шаблона по имени с передачей в него параметров модели, полученных от контроллера
Схема работы сервлетов на сервере сервлетов (http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/mvc.html).
Далее нам необходимо создать файл с описанием контекта сервлета. Создаём src/main/webapp/WEB-INF/appServlet-servlet.xml
, в котором опишем настройки для web-приложения.
appServlet-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
">
<mvc:annotation-driven />
<context:component-scan base-package="a1s.learn.controller" />
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jspx" />
</bean>
</beans>
Рассмотрим элементы подробнее:
-
<mvc:annotation-driven />
- указывает, что для описания сущностей MVC будут использоваться аннотации -
<context:component-scan base-package="a1s.learn.controller" />
- указывает на пакеты, в которых будет происходить сканирование на наличие аннотаций - Бин
viewResolver
представляет собой объект классаorg.springframework.web.servlet.view.InternalResourceViewResolver
, который производит определение шаблона по имени.- Свойство
prefix
задаёт префикс для шаблонов (в нашем случае это путь) - Свойство
suffix
задаёт суффикс (в нашем случае расширение) для файла шаблона
- Свойство
В случае, если необходимы глобальные настройки для Spring context, которые сервлеты разделяют между собой (например, подключение к БД или настройка messageSource
) существует возможность определить корневой контекст. Для определения корневого контекста необходимо использовать listener org.springframework.web.context.ContextLoaderListener
, который осуществляет соответствующую загрузку, а также указать путь к файлу с корневым контекстом.
Добавим в web.xml следующие строки:
web.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
Рассмотрим подробнее:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root.xml</param-value>
</context-param>
Данный элемент настраивает параметр contextConfigLocation
в рамках контекста и задаёт путь к файлу с корневым контекстом как /WEB-INF/spring/root.xml
.
Элемент <listener />
в свою очередь настраивает слушателя для загрузки соответствующего контекста.
Создадим файл-заготовку для корневого контекса по адресу src/main/webapp/WEB-INF/spring/root.xml
.
root.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
">
<!-- Root context -->
</beans>
Создадим основной контроллер MainController.java
.
MainController.java
package a1s.learn.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/")
public class MainController {
@RequestMapping(method= RequestMethod.GET)
public String main(Model ui)
{
return "user/list";
}
}
Рассмотрим аннотации:
-
@Controller
- указывает, что этот класс является контроллером -
@RequestMapping("/")
- указывает, что этот контроль обрабатывает все URL от корня -
@RequestMapping(method= RequestMethod.GET)
- указывает, что метод обрабатывает методGET
Метод main()
принимает в качестве параметров объект Model ui
, который представляет модель.
В качестве результата контроллер возвращает строку user/list
, что означает, что необходимо произвести рендеринг по шаблону user/list
.
Наши шаблоны для вывода будут располагаться по адресу src/main/webapp/WEB-INF/views/
.
Создаём директорию src/main/webapp/WEB-INF/views/user
для шаблонов отображения данных о пользователе.
Создаём шаблон jspx src/main/webapp/WEB-INF/views/user/list.jspx
.
<?xml version="1.0" encoding="UTF-8"?>
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
version="2.0"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8"/>
<jsp:output omit-xml-declaration="yes" />
${contacts}
</div>
Рассмотрим создание view более подробно:
- так как используется jspx, то используются более строгие правила оформления в стиле XML и сам заголовок XML-файлов
<?xml version="1.0" encoding="UTF-8"?>
-
xmlns:jsp="http://java.sun.com/JSP/Page"
- указывает пространства имён JSP для jsp tld -
xmlns:spring="http://www.springframework.org/tags"
- указывает пространства имён spring для spring tld -
xmlns:c="http://java.sun.com/jsp/jstl/core"
- указывает пространства имён c для jstl core tld - tld - Tag Library Descriptors, библиотека тэгов
-
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8"/>
- указывает на тип страницы и кодировку -
<jsp:output omit-xml-declaration="yes" />
- указывает, что необходимо пропустить XML-описание при рендеринге -
${contacts}
указывает, что необходимо вывести элемент модели с ключомcontacts
Создадим простой контроллер, который производит редирект со страницы /
на страницу user/list
и заполняет свойство модели contacts
для вывода и принимает в качестве параметров некий идентификатор.
MainController.java
package a1s.learn.controller;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
@RequestMapping("/")
public class MainController {
@RequestMapping(value="user/list/id/{id}",method= RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public String userList(Model ui, @PathVariable("id") Long id)
{
ui.addAttribute("contacts", "Ce contacts"+id.toString());
return "user/list";
}
@RequestMapping(value="user/list/id/{id}", method= RequestMethod.GET,produces="text/json")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public String userListJson(Model ui, @PathVariable("id") Long id)
{
return "{id:"+id.toString()+"}";
}
@RequestMapping("/")
public String main(Model ui)
{
return "redirect:user/list";
}
}
Рассмотрим контроллер более подробно:
@RequestMapping("/")
public String main(Model ui)
{
return "redirect:user/list";
}
Данный метод аннотирован с помощью @RequestMapping("/")
, что означает, что используется URL '/'.
Метод возвращает строку redirect:user/list
, что приводит к переходу на страницу user/list
при
@RequestMapping(value="user/list/id/{id}", method= RequestMethod.GET,produces="text/json")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public String userListJson(Model ui, @PathVariable("id") Long id)
{
return "{id:"+id.toString()+"}";
}
- Аннотация
@ResponseBody
означает, что результатом работы метода(возвращаемым значением) является тело ответа. -
@ResponseStatus(HttpStatus.OK)
- будет возвращаться HTTP-статус 200 OK. -
@RequestMapping(value="user/list/id/{id}", method=RequestMethod.GET,produces="text/json")
- указывает что метод будет вызван при запросе URLuser/list/id/*/
, если метод запроса = GET, а также, что этот метод выдаёт содержимое типаtext/json
. - Параметр
produces
будет сравниваться с заголовкомAccept
. И если клиент не поддерживает такое содержимое, то данный метод вызван не будет. -
@PathVariable("id") Long id
указывает, что в метод необходимо передать параметрid
типаLong
, который необходимо взять из пути (шаблон URL и расположение параметраid
уже описание в@RequestMapping(value="user/list/id/{id}"...
)
@RequestMapping(value="user/list/id/{id}",method= RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public String userList(Model ui, @PathVariable("id") Long id)
{
ui.addAttribute("contacts", "Ce contacts"+id.toString());
return "user/list";
}
Данный метод - аналог предыдущего, но будет работать, если клиент не поддерживает text/json
и проводить рендеринг пошаблону.
Возьмём curl
для проверки работы контроллера.
$ curl -H 'Accept: text/html' -v http://127.0.0.1:9090/user/list/id/123
* About to connect() to 127.0.0.1 port 9090 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 9090 (#0)
> GET /user/list/id/123 HTTP/1.1
> User-Agent: curl/7.27.0
> Host: 127.0.0.1:9090
> Accept: text/html
>
* additional stuff not fine transfer.c:1037: 0 0
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 200 OK
< Content-Language: ru-RU
< Content-Type: text/html;charset=UTF-8
< Set-Cookie: JSESSIONID=110ioyauffrfjf7v3bir67y77;Path=/
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Content-Length: 52
< Server: Jetty(8.1.8.v20121106)
<
<div version="2.0">
Ce contacts123
* Connection #0 to host 127.0.0.1 left intact
</div>* Closing connection #0
$ curl -H 'Accept: text/json' -v http://127.0.0.1:9090/user/list/id/123
* About to connect() to 127.0.0.1 port 9090 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 9090 (#0)
> GET /user/list/id/123 HTTP/1.1
> User-Agent: curl/7.27.0
> Host: 127.0.0.1:9090
> Accept: text/json
>
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 200 OK
< Content-Length: 8
< Content-Type: text/json
< Server: Jetty(8.1.8.v20121106)
<
* Connection #0 to host 127.0.0.1 left intact
{id:123}* Closing connection #0
Из вывода видно как заголовок Accept
влияет на содержимое, а также видна работа с параметрами.
Spring при работе с формами производит установку свойств объектов классов предметной области, поэтому создадим объекты предметной области для иллюстрации работы с формами.
Создадим классы для пользователя и группы.
User.java
package a1s.learn;
public class User {
protected Long id;
protected Group group;
public Group getGroup() {
return group;
}
public void setGroup(Group group) {
this.group = group;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String toString() {
return "User{" + "id=" + id + ", group=" + group + ", name=" + name + '}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
protected String name;
}
Group.java
package a1s.learn;
public class Group {
protected String name;
@Override
public String toString() {
return "Group{" + "name=" + name + '}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Реализуем простую форму в jspx.
<?xml version="1.0" encoding="UTF-8"?>
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
version="2.0"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
${user}
<form action="/user/list/form" method="POST">
<table>
<tr><td>ID</td><td><input name="id" type="text" /></td></tr>
<tr><td>NAME</td><td><input name="name" type="text" /></td></tr>
<tr><td>GROUP</td><td><input name="group.name" type="text" /></td></tr>
<tr><td></td><td><input type="submit" /></td></tr>
</table>
</form>
</div>
Имена элементов указывают на свойства объектов:
-
id
- установить свойствоid
-
group.name
- установить свойствоname
у вложенного объектаgroup
-
groups[2].name
- установить свойствоname
у объекта коллекции(List<Group>
, например)groups
-
groups[MY_GROUP].name
- установить свойствоname
у объекта с ключомMY_GROUP
в Map (Map<Groups>
, например)groups
@RequestMapping(value="user/list", method= RequestMethod.GET)
public String userListGet(Model ui)
{
ui.addAttribute("user", new User());
return "user/list";
}
@RequestMapping(value="user/list/form", method= RequestMethod.POST)
public String userListPost(User u, BindingResult br, Model ui)
{
System.out.println(br.toString());
System.out.println(u.toString());
ui.addAttribute("user", u);
return "user/list";
}
Рассмотрим методы более подробно:
@RequestMapping(value="user/list", method= RequestMethod.GET)
public String userListGet(Model ui)
{
ui.addAttribute("user", new User());
return "user/list";
}
Данный метод вызывается в случае запроса по адресу user/list
с использованием метода GET
. Метод создаёт пустой объект User
сохраняет в модель ui
по ключу user
. Далее управление передаётся view user/list
.
@RequestMapping(value="user/list/form", method= RequestMethod.POST)
public String userListPost(User u, BindingResult br, Model ui)
{
System.out.println(br.toString());
System.out.println(u.toString());
ui.addAttribute("user", u);
return "user/list";
}
Данный метод вызывается в случае запроса по адресу user/list/form
методом POST
. В метод передаются следующие параметры:
-
User u
- объект пользователя. ПараметрыPOST
-запроса будут сконвертированы в объект пользователя. -
BindingResult br
- содержит результаты конвертации параметров запроса в объект пользователя -
Model ui
- объект модели
Данный метод выводит в консоль результат конвертации запроса, а также сам объект пользователя, после чего объект пользователя добавляется в модель.
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
Указывает, что необходимо удалить все пробельные символы, что приводит выводу подобному ниже.
<div version="2.0">User{id=1, group=Group{name=asdsda}, name=adasd}<form method="POST" action="/user/list/form"><table><tr><td>ID</td><td><input type="text" name="id" /></td></tr><tr><td>NAME</td><td><input type="text" name="name" /></td></tr><tr><td>GROUP</td><td><input type="text" name="group.name" /></td></tr><tr><td /><td><input type="submit" /></td></tr></table></form></div>
MainController.java
@RequestMapping(value="user/list/form", method= RequestMethod.POST)
public String userListPost(@ModelAttribute("user") User u, BindingResult br, Model ui)
{
System.out.println(br.toString());
System.out.println(u.toString());
return "user/list";
}
list.jspx
<?xml version="1.0" encoding="UTF-8"?>
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
${user}
<form:form modelAttribute="user" action="/user/list/form" id="userForm" method="post">
<div>
<form:errors path="*" cssClass="error" />
</div>
<table>
<tr>
<td>
<form:label path="id">
Идентификатор
</form:label>
</td>
<td>
<form:input path="id" />
</td>
</tr>
<tr>
<td colspan="2">
<form:errors path="id" cssClass="error" />
</td>
</tr>
<tr>
<td></td>
<td><form:input path="group.name" /></td>
</tr>
<tr>
<td></td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Send" />
</td>
</tr>
</table>
</form:form>
</div>
Рассмотрим тэги подробнее
-
<form:form modelAttribute="user" action="/user/list/form" id="userForm" method="post">
- определяет форму на странице-
modelAttribute
- определяет ключ модели, под которым сохранён объект, управляемый формой -
action
- задаёт url (атрибутaction
тэга<form />
), на который будет передана форма -
method
- задаёт метод отправки формы
-
-
<form:label path="id">
- определяет подпись к элементу-
path
- задаёт свойство объекта из модели, к которому относится подпись
-
-
<form:input path="id" />
- задаёт элемент ввода -
<form:errors path="id" cssClass="alert alert-error" />
- задаёт вывод ошибок к свойству объекта-
cssClass
- задаёт стили применяемые к элементу с ошибками
-
-
<form:errors path="*" cssClass="error" />
- выводит все ошибки ко всем элементам формы
appServlet-servlet.xml
<mvc:resources mapping="/resources/**" location="/web-resources/" />
Создаём папку src/main/webapp/web-resources/.
Скачиваем Twitter Bootsrap http://twitter.github.com/bootstrap/getting-started.html#download-bootstrap
и распаковываем в /src/main/webapp/web-resources/bootstrap
list.jspx
<?xml version="1.0" encoding="UTF-8"?>
<html
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
>
<head>
<link rel="stylesheet" href="/resources/main.css" />
<link href="/resources/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
<script src="http://code.jquery.com/jquery-latest.js"><!-- --></script>
<script src="/resources/bootstrap/js/bootstrap.min.js"><!-- --></script>
</head>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
<body>
${user}
<form:form modelAttribute="user" action="/user/list/form" id="userForm" method="post">
<form:errors path="*" cssClass="alert alert-error" />
<table>
<tr>
<td>
<form:label path="id">
Идентификатор
</form:label>
</td>
<td>
<form:input path="id" />
</td>
</tr>
<tr>
<td colspan="2">
<form:errors path="id" cssClass="alert alert-error" />
</td>
</tr>
<tr>
<td></td>
<td><form:input path="group.name" /></td>
</tr>
<tr>
<td></td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" class="btn btn-success" value="Send" />
</td>
</tr>
</table>
</form:form>
</body>
</html>
Ресурсы располагаются в файловой системе по адресу web-resources
и доступны по адресу /resources/
.
Важным момент является то, что JSP проводить оптимизацию тэгов и тэг в JSP вида <script src="..."></script>
будет преобразован к <script src="..." />
в html, что противоречит html в плане загрузки скриптов. По этой причине тэги для загрузки скриптов описаны как <script src="/resources/bootstrap/js/bootstrap.min.js"><!-- --></script>
, что на выходе даст: <script src="/resources/bootstrap/js/bootstrap.min.js"></script>
.
Важным аспектом разработки web-приложений является проверка достоверности и валидация данных. Spring framework имеет поддержку валидации JSR 303. Валидация по JSR 303 подразумевает аннотирование полей объекта с использованием аннотаций, которые определяют правила валидации.
Для начала необходимо добавить зависимость от поставщика валидации. В нашем случае этой библиотекой будет Hibernate validator.
pom.xml
<properties>
<hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
</properties>
<!-- hibernate validation -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate.validator.version}</version>
</dependency>
Заменим <mvc:annotation-driven />
на создание валидатора и использование валидатора в MVC.
appServlet-servlet.xml
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validator" />
<mvc:annotation-driven validator="validator" />
Добавим аннотации в User
User.java
package a1s.learn;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
public class User {
@Min(100)
@Max(200)
protected Long id;
protected Group group;
public Group getGroup() {
return group;
}
public void setGroup(Group group) {
this.group = group;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String toString() {
return "User{" + "id=" + id + ", group=" + group + ", name=" + name + '}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Size(min=4,max=6)
@NotEmpty
protected String name;
}
Рассмотрим аннотации подробнее:
-
@NotEmpty
указывает, что поле обязательно для заполнения -
@Size(min=4,max=6)
указывает, что значение поля может быть от 4 до 6 символов -
@Min(100)
указывает, что значение поле должно быть больше или равно 100 -
@Max(200)
указывает, что значение поле должно быть меньше или равно 200
Необходимо добавить аннотацию @Valid
в MainController
, чтобы Spring MVC автоматически проверяла объекты.
MainController.java
@RequestMapping(value="user/list/form", method= RequestMethod.POST)
public String userListPost(@Valid @ModelAttribute("user") User u, BindingResult br, Model ui)
{
System.out.println(br.toString());
System.out.println(u.toString());
return "user/list";
}
Добавим в файл формы вывод ошибок.
<?xml version="1.0" encoding="UTF-8"?>
<html
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
>
<head>
<link rel="stylesheet" href="/resources/main.css" />
<link href="/resources/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
<script src="http://code.jquery.com/jquery-latest.js"><!-- --></script>
<script src="/resources/bootstrap/js/bootstrap.min.js"><!-- --></script>
</head>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
<body>
${user}
<form:form modelAttribute="user" action="/user/list/form" id="userForm" method="post">
<form:errors path="*" cssClass="alert alert-error" />
<table>
<tr>
<td>
<form:label path="id">
Идентификатор
</form:label>
</td>
<td>
<form:input path="id" /><form:errors path="id" cssClass="alert alert-error" />
</td>
</tr>
<tr>
<td></td>
<td><form:input path="group.name" /><form:errors path="group.name" cssClass="alert alert-error" /></td>
</tr>
<tr>
<td></td>
<td><form:input path="name" /><form:errors path="name" cssClass="alert alert-error" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" class="btn btn-success" value="Send" />
</td>
</tr>
</table>
</form:form>
</body>
</html>
На рисунке показан снимок экрана с сообщениями об ошибках в случае неверного заполнения формы.
Кроме вывода по шаблону в web-приложениях может понадобиться группировка однотипных шаблонов (шаблон Composite). Для решения задач составления шаблонов в макет (layout) используется библиотека Apache tiles.
Apache tiles позволяет определить layout, в который будут вставлены jsp-страницы. Таким образом, достигается возможность группировки отдельных шаблонов в страницы.
Для начала необходимо добавить зависимость от библиотеки Apache tiles (tiles-core и tiles-jsp).
pom.xml
<properties>
<apache.tiles.version>2.2.2</apache.tiles.version>
</properties>
<!-- Apache tiles -->
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-core</artifactId>
<version>${apache.tiles.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-jsp</artifactId>
<version>${apache.tiles.version}</version>
</dependency>
Для функционирования Apache tiles необходимо наличие логгера поддерживающего slf4j. В данном случае таким логгером будет logback.
Добавим зависимости от logback и удалим зависимость от JCL для spring-core.
Зависимости для SLF4J.
<properties>
<slf4j.version>1.7.2</slf4j.version>
</properties>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
Добавим logback в зависимости:
<properties>
<logback.version>1.0.7</logback.version>
</properties>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${logback.version}</version>
</dependency>
Удалим зависимость от apache commons logging из spring-core.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
Для функционирования tiles необходим общий шаблон, который будет располагаться по адресу WEB-INF/layouts.xml
.
Создаём WEB-INF/layouts.xml
.
layouts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN"
"http://tiles.apache.org/dtds/tiles-config_2_0.dtd">
<tiles-definitions>
<!-- Default Main Template -->
<definition name="default" template="/WEB-INF/layouts/default.jspx">
<put-attribute name="header" value="/WEB-INF/layouts/header.jspx" />
<put-attribute name="footer" value="/WEB-INF/layouts/footer.jspx" />
</definition>
</tiles-definitions>
В данном описании описан 1 макет по имени default
, файл макета расположен по адресу /WEB-INF/layouts/default.jspx
. В макете 3 атрибута (header
, footer
и body
). header
и footer
всегда постоянные, а вот body
будет заменяться.
default.jsp
<html
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
xmlns:tiles="http://tiles.apache.org/tags-tiles"
>
<head>
<link rel="stylesheet" href="/resources/main.css" />
<link href="/resources/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
<script src="http://code.jquery.com/jquery-latest.js"><!-- --></script>
<script src="/resources/bootstrap/js/bootstrap.min.js"><!-- --></script>
</head>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
<body>
<tiles:insertAttribute name="header" ignore="true" />
<tiles:insertAttribute name="body" ignore="true" />
<tiles:insertAttribute name="footer" ignore="true" />
</body>
</html>
Макет - обычная jsp-страница со вставками атрибутов tiles с использованием пространства имён tiles
.
-
<tiles:insertAttribute name="header" ignore="true" />
- приводит к вставке значения атрибутаheader
. -
ignore="true"
- означает, что в случае отсутствия атрибута исключение создаваться не будет.
header.jspx
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
xmlns:tiles="http://tiles.apache.org/tags-tiles"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
Header
</div>
footer.jspx
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
xmlns:tiles="http://tiles.apache.org/tags-tiles"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
Footer
</div>
Spring имеет поддержку Apache tiles версии 2. Для включения поддержки tiles необходимо настроить view resolver, для этого заменим определение view resolver'а в appServlet-servlet.xml
.
Необходимо заменить код
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jspx" />
</bean>
на
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" id="tilesViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView" />
</bean>
<bean class="org.springframework.web.servlet.view.tiles2.TilesConfigurer" id="tilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/layouts.xml</value>
<value>/WEB-INF/views/**/views.xml</value>
</list>
</property>
</bean>
-
org.springframework.web.servlet.view.UrlBasedViewResolver
- маршрутизирует обработку view по URL -
org.springframework.web.servlet.view.tiles2.TilesView
- класс поддержки apache tiles как view-класса для SpringMVC -
org.springframework.web.servlet.view.tiles2.TilesConfigurer
- настраивает собственно Apache tiles
Поддержка Apache tiles требует, чтобы все макеты были описаны в xml. Использование макетов по умолчанию не предусмотрено.
Описание
<list>
<value>/WEB-INF/layouts.xml</value>
<value>/WEB-INF/views/**/views.xml</value>
</list>
указывает, что основной файл описания макета расположен по адресу /WEB-INF/layouts.xml
, кроме того необходимо проверять описание tiles во всех вложенных директориях шаблонов /WEB-INF/views/**/views.xml
.
То есть:
- в контроллере возвращается не наименование JSP страницы, а наименование макета
- для каждого макета должно быть описана схема внедрения атрибутов
Реализуем файл макета для tiles, в котором описано как и в какой атрибут будет рендерится наша jsp-страница с формой.
Создадим файл /WEB-INF/views/user/views.xml
.
/WEB-INF/views/user/views.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN"
"http://tiles.apache.org/dtds/tiles-config_2_0.dtd">
<tiles-definitions>
<!-- Default Main Template -->
<definition extends="default" name="user/list">
<put-attribute name="body" value="/WEB-INF/views/user/list.jspx" />
</definition>
</tiles-definitions>
Так как за страницу целиком теперь отвечает макет tiles, а не страница jsp, то из jsp-страницы можно удалить все указания загрузки ресурсов и т.д.
/WEB-INF/views/user/list.jspx
<?xml version="1.0" encoding="UTF-8"?>
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
${user}
<form:form modelAttribute="user" action="/user/list/form" id="userForm" method="post">
<form:errors path="*" cssClass="alert alert-error" />
<table>
<tr>
<td>
<form:label path="id">
Идентификатор
</form:label>
</td>
<td>
<form:input path="id" /><form:errors path="id" cssClass="alert alert-error" />
</td>
</tr>
<tr>
<td></td>
<td><form:input path="group.name" /><form:errors path="group.name" cssClass="alert alert-error" /></td>
</tr>
<tr>
<td></td>
<td><form:input path="name" /><form:errors path="name" cssClass="alert alert-error" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" class="btn btn-success" value="Send" />
</td>
</tr>
</table>
</form:form>
</div>
По умолчанию logback выводит все сообщения в консоль, поэтому для исключения большого количества сообщений необходимо провести настройку логгера.
Создадим файл logback.xml
по адресу src/main/resources/logback.xml
.
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{YYYY-MM-dd HH:mm:ss} %level %logger %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework" level="info" />
<!--logger name="org.springframework.beans" level="debug" /-->
<logger name="a1s" level="debug" />
<root level="warn">
<appender-ref ref="console" />
</root>
</configuration>
Конфигурация настраивает logback на вывод сообщений в консоль. Кроме того, в консоль будут выводиться сообщения с уровнем не ниже info
для Spring framework и debug
для a1s
.
Соберём приложение и перейдём по адресу http://127.0.0.1:9090/.
На странице появились Header
и Footer
.
Создадим более сложную форму с различными элементами управления:
-
input
- для ввода строковых значений -
textarea
- для ввода большого объёма текста -
select
- для выбора из множества -
checkbox
- для булева выбора -
radiobutton
- для выбора элемента из списка
Для использования всех элементов управления изменим объект пользователя и группы.
User.java
package a1s.learn;
public class User {
protected Long id;
protected String name;
protected Group group;
protected boolean active = true;
protected String comment;
protected String securityLevel = "minimal";
@Override
public String toString() {
return "User{" + "id=" + id + ", name=" + name + ", group=" + group + ", active=" + active + ", comment=" + comment + ", securityLevel=" + securityLevel + '}';
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Group getGroup() {
return group;
}
public void setGroup(Group group) {
this.group = group;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getSecurityLevel() {
return securityLevel;
}
public void setSecurityLevel(String securityLevel) {
this.securityLevel = securityLevel;
}
}
Group.java
package a1s.learn;
public class Group {
protected Long id;
protected String name;
public Group(Long id) {
this.id = id;
this.name= "Группа "+id;
}
public Group(Long id,String name) {
this.id = id;
this.name=name;
}
@Override
public String toString() {
return "Group{" + "id=" + id + ", name=" + name + '}';
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Будем реализовывать форму как представленная на рисунке ниже:
Для вывода элементов формы воспользуемся пространством имён form:
из spring-form.tld.
list.jspx
<?xml version="1.0" encoding="UTF-8"?>
<div
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" />
<jsp:output omit-xml-declaration="yes" />
${user}
<form:form modelAttribute="user" action="/user/list/form" id="userForm" method="post">
<table>
<tr>
<td>
Наименование
</td>
<td>
<form:input path="name" />
</td>
</tr>
<tr>
<td colspan="2">
<ul>
<form:radiobuttons delimiter="" element="li" path="group" items="${groups}" itemValue="id" itemLabel="name" />
</ul>
</td>
</tr>
<tr>
<td>
Активность
</td>
<td>
<form:checkbox path="active" /><form:label path="active">Активен?</form:label>
</td>
</tr>
<tr>
<td colspan="2">
<form:textarea path="comment" rows="15" cols="60" />
</td>
</tr>
<tr>
<td colspan="2">
<form:radiobutton path="securityLevel" id="maxSecurityLevel" value="maximum" /><form:label path="securityLevel" for="maxSecurityLevel">Максимальный</form:label><br />
<form:radiobutton path="securityLevel" id="minSecurityLevel" value="minimal" /><form:label path="securityLevel" for="minSecurityLevel">Минимальный</form:label>
</td>
</tr>
<tr>
<td>
</td>
<td>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Send" />
</td>
</tr>
</table>
</form:form>
<form:errors path="*" />
</div>
Рассмотрим тэги более подробнее:
-
<form:input path="name" />
- поле ввода имени -
<ul><form:radiobuttons delimiter="" element="li" path="group" items="${groups}" itemValue="id" itemLabel="name" /></ul>
формирует список из элементов<li />
(element="li"
) для свойстваgroup
(path="group"
). В качестве элемента управления будут использоваться радиокнопки. Элементами списка будет список объектов, который сохранён в модели по ключуgroups
(items="${groups}"
). Значением для радиокнопки будет выбрано свойствоid
(itemValue="id"
) объекта из списка, а подписью - свойствоname
(itemLabel="name"
). Кроме того, радиокнопки можно разделять между собой с помощью разделителя (delimiter=""
) в нашем случае такого разделения нет. -
<form:textarea path="comment" rows="15" cols="60" />
- поля для ввода текст (textarea
) размерами 15 строк и 60 столбцов -
<form:checkbox path="active" /><form:label path="active">Активен?</form:label>
выводит флажок для отображения активности пользователя -
<form:radiobutton path="securityLevel" id="maxSecurityLevel" value="maximum" /><form:label path="securityLevel" for="maxSecurityLevel">Максимальный</form:label><br /><form:radiobutton path="securityLevel" id="minSecurityLevel" value="minimal" /><form:label path="securityLevel" for="minSecurityLevel">Минимальный</form:label>
- формирует список из радиокнопок для строковых значений (minimal
иmaximum
).
Элементы управление заполняются автоматически.
В большинстве мы использовали простые типы, кроме элемента с группами, и Spring проведёт преобразование типов автоматически, так как в состав spring уже входят преобразователи для базовых типов.
Таким образом, необходимо реализовать преобразователь из значения типа String
в значение типа Group
, чтобы заполнение формы было корректным.
Можно воспользоваться реализацией глобального преобразователя и зарегистрировать его в conversion service, но для специфических случаев может понадобиться реализация преобразователя по месту.
Реализуем метод, который будет указывать как проводить преобразование для поля group
.
MainController.java
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Group.class, "group", new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
Group g = new Group(Long.valueOf(text));
setValue(g);
}
});
}
Аннотация @InitBinder
указывает, что данный метод проводить настройку биндинга и преобразования.
WebDataBinder binder
- собственно объект биндера.
Мы регистрируем специфичный преобразователь в объект класса Group
для поля group
с использованием расширения класса PropertyEditorSupport
(фактически создаём собственный propertyeditor). Наш property editor будет создавать объект класса Group
из переданного строкового значения и устанавливать объект как значение необходимо объекта.
Теперь необходимо реализовать заполнение groups
в модели. Для этого реализуем соответствующий метод и добавим заполнение модели в необходимые методы.
@RequestMapping(value="user/list", method= RequestMethod.GET)
public String userListGet(Model ui)
{
ui.addAttribute("user", new User());
ui.addAttribute("groups", this.getGroups());
return "user/list";
}
@RequestMapping(value="user/list/form", method= RequestMethod.POST)
public String userListPost(@Valid @ModelAttribute("user") User u, BindingResult br, Model ui)
{
System.out.println(br.toString());
System.out.println(u.toString());
ui.addAttribute("groups", this.getGroups());
return "user/list";
}
protected List<Group> getGroups()
{
List<Group> groups = new ArrayList<Group>();
groups.add(new Group(Long.valueOf(1),"Группа 1"));
groups.add(new Group(Long.valueOf(2),"Группа 2"));
groups.add(new Group(Long.valueOf(3),"Группа 3"));
return groups;
}
После этого можно собрать приложение и перейти по адресу http://127.0.0.1:9090/user/list.
TODO! form:select, form:checkboxes
В Spring framework есть отдельный проект для реализации авторизации и аутентификации - Spring security. Данный проект реализует общий функционал для обеспечения безопасности и разграничения прав доступа.
Для начала добавим зависимости в pom.xml.
pom.xml
<!-- spring security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.version}</version>
</dependency>
-
spring-security-taglibs
- библиотека тэгов для JSP -
spring-security-core
- общая функциональность -
spring-security-config
- классы для конфигурации Spring security -
spring-security-web
- классы поддержки авторизации в web
Spring security, в контексте web-приложений, реализует разграничение прав доступа на уровне URL, то есть определяет по правам может ли пользователь запрашивать данный URL. Данный механизм реализуется с помощью фильтров http-запросов.
Определим минимальную конфигурацию для Spring security.
В файле web.xml
опишем использование фильтра.
web.xml
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Раздел <filter />
- указывает на загружаемый фильтр. В нашем случае это фильтр, который делегирует управление в инфраструктуру Spring (то есть вызов из инфраструктуры servlet делегируется spring фильтрам с поддержкой IoC).
Раздел <filter-mapping />
указывает к каким URL будет применяться данный фильтр. В нашем случае ко всем.
Далее необходимо настроить сам Spring security, для этого нам необходимо добавить настройки spring security в контекст web-приложения (файл WEB-INF/spring/root.xml
), так как эти настройки глобальные и должны быть применены ко всему web-приложению.
Добавим используемое пространство имён и настройки.
WEB-INF/spring/root.xml
<beans ...
xmlns:security="http://www.springframework.org/schema/security"
...
xsi:schemaLocation="...
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd
...
">
WEB-INF/spring/root.xml
<security:http auto-config='true'>
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user1" password="user1" authorities="ROLE_USER, ROLE_ADMIN" />
<security:user name="user2" password="user2" authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
Рассмотрим настройки подробнее:
<security:http auto-config='true'>
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>
Раздел <securty:http />
настраивает безопасность для http. В нашем случае используется автоматическая конфигурация (auto-config='true'
), которая настраивает использование форм для авторизации.
Раздел <security:intercept-url />
настраивает к каким URL с какими правами пользователи имеют доступ.
В нашем случае только зарегистрированные пользователи имеют доступ ко всему web-приложению.
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user1" password="user1" authorities="ROLE_USER, ROLE_ADMIN" />
<security:user name="user2" password="user2" authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
Данный раздел настраивает менеджер аутентификации. В нашем случае пользователи и их роли прописаны прямо в XML-файле конфигурации, о можно использовать JDBC или LDAP в качестве провайдера аутентификации.
Мы настроили аутентификацию с использованием форм. Форму логина Spring security создаёт сам.
Если теперь собрать приложение и обратиться по адресу http://127.0.0.1:9090, то увидим форму ввода логина и пароля. Перейти к приложению можно будет только тогда, когда будут введены правильные логин и пароль.
Рассмотрим форму для ввода логина и пароля, которую генерирует Spring security по умолчанию.
Во-первых, при входе по любом адресу нас перебрасывает на страницу по адресу /spring_security_login
.
Код страницы формы.
<html><head><title>Login Page</title></head><body onload='document.f.j_username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/j_spring_security_check' method='POST'>
<table>
<tr><td>User:</td><td><input type='text' name='j_username' value=''></td></tr>
<tr><td>Password:</td><td><input type='password' name='j_password'/></td></tr>
<tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
</table>
</form></body></html>
По умолчанию url для обработки формы является /j_spring_security_check
. Обрабатываемые параметры:
-
j_username
- для имени пользователя -
j_password
- для пароля
Это значения по умолчанию и их можно заменить.
Создадим форму как на рисунке ниже:
Для начала реализуем форму в файле /WEB-INF/views/login.jspx
.
login.jspx
<div
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form"
xmlns:spring="http://www.springframework.org/tags"
>
<jsp:directive.page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"/>
<jsp:output omit-xml-declaration="yes"/>
<c:url value="try_login" var="login_form_url" />
<c:if test="${not empty error}">
<div class="alert alert-error" style="border:2px solid #e9322d;background: #eed3d7;">
${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
</div>
</c:if>
<form action="${login_form_url}" method="POST">
<table>
<tr>
<td>
Name (login)
</td>
<td>
<input type="text" name="username" />
</td>
</tr>
<tr>
<td>
Password
</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Login" />
</td>
</tr>
</table>
</form>
</div>
Рассмотрим важные элементы:
-
<c:url value="try_login" var="login_form_url" />
- преобразовывает значениеtry_login
в ссылку и сохраняет в переменнойlogin_form_url
-
${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
- хранит сообщение от Spring security о результате авторизации - название полей формы были заменены на
username
иpassword
соответственно
Теперь необходимо реализовать контроллер поддержки авторизации, который и будет показывать соответствующие формы.
LoginController.java
package a1s.learn.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LoginController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(ModelMap model) {
return "login";
}
@RequestMapping(value = "/login", params = {"failed"}, method = RequestMethod.GET)
public String loginerror(ModelMap model) {
model.addAttribute("error", "true");
return "login";
}
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(ModelMap model) {
return "login";
}
}
Замечания по URL:
-
/login
- используется для отображения формы ввода логина и пароля -
/logout
- используется для выхода из системы -
/login?failed
- используется для отображения ошибки входа в систему
Так как мы использовали Apache tiles как шаблонизатор, то необходимо добавить соответствующие шаблоны к описанию.
Добавим в layout.xml
следующие строки:
layout.xml
<!-- Login Template -->
<definition name="login" extends="default">
<put-attribute name="body" value="/WEB-INF/views/login.jspx" />
</definition>
то есть расширяем шаблон по умолчанию путём добавления атрибута body
, в который будет рендериться страница login.jspx
.
Осталось только указать Spring security, что используется нестандартная форма авторизации и заменены параметры, кроме того необходимо разрешить доступ к страницам /login
неавторизованным пользователям, иначе попытка логина приведёт к циклическому редиректу, кроме того разрешим доступ к ресурсам неавторизованным пользователям.
root.xml
<security:http security="none" pattern="/resources/**" />
<security:http security="none" pattern="/login*" />
<security:http security="none" pattern="/logout*" />
<security:http auto-config='true' use-expressions="true">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
<security:form-login login-page="/login" default-target-url="/user/list" login-processing-url="/try_login"
authentication-failure-url="/login?failed" password-parameter="password" username-parameter="username" />
<security:logout logout-success-url="/login" logout-url="/logout" />
</security:http>
Рассмотрим настройки подробнее:
-
<security:http security="none" pattern="/resources/**" />
- указывает на URL, который не обрабатывается системой безопасности -
<security:http use-expressions="true">
разрешает использование выражений (это понадобится нам для вывода логина текущего пользователя в header'е страницы), но этот параметр запрещает использование простых выражений для описания полномочийaccess="ROLE_USER"
, поэтому выражение будет заменено наaccess="hasRole('ROLE_USER')"
-
<security:form-login />
- настраивает форму логина-
login-page="/login"
- URL для формы логина -
default-target-url="/user/list"
- страница по умолчанию для перехода после успешного логина -
login-processing-url="/try_login"
- URL для обработки данных авторизации -
authentication-failure-url="/login?failed"
- URL страницы, если авторизация прошла неуспешно -
password-parameter="password"
- название поля для ввода пароля (см. JSP страницу login.jspx) -
sername-parameter="username"
- название поля для ввода имени пользователя (см. JSP страницу login.jspx)
-
-
<security:logout logout-success-url="/login" logout-url="/logout" />
- страница для выхода из системы и страница для перехода после логаута
Осталось добавить в header.jspx вывод текущего пользователя.
Для этого добавим использование пространства имён sec
.
<div ...
xmlns:sec="http://www.springframework.org/security/tags"
>
и выведем имя пользователя, если пользователь авторизован.
<sec:authorize access="isAuthenticated()">
<sec:authentication property="principal.username"/>
</sec:authorize>
Соберём и запустим приложение.
Форма входа после неудачного ввода логина и пароля.
Результирующий вид страницы после входа в систему. В header'е страницы выводится имя текущего пользователя
Работа с LDAP и более подробная работа с Spring security рассмотрены в соответствующем разделе Spring security.
В рассмотренном механизме работы с tiles есть недостаток связанный с тем, что для каждого шаблона, который будет использоваться в коде, необходимо описывать собственный шаблон.
Apache Tiles поддерживает механизм wildcard в именах шаблонов, что позволяет использовать лишь подстановку нужного атрибута, а остальные атрибуты взять из базового шаблона.
Wildcard'ы описываются с помощью *
. Wildcard'ы являются позиционными и на них можно ссылаться помощью шаблона вида {0}
(для полного имени шаблона) или {1}
(для ссылки на конкретный wildcard).
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition name="default" template="/WEB-INF/views/default.jsp">
<put-attribute name="header" value="/WEB-INF/views/header.jsp" />
<put-attribute name="footer" value="/WEB-INF/views/footer.jsp" />
</definition>
<definition name="*/*" extends="default">
<put-attribute name="body" value="/WEB-INF/views/{0}.jsp" />
</definition>
<definition name="asd/*" extends="default">
<put-attribute name="body" value="/WEB-INF/views/asd/{1}.jsp" />
</definition>
<definition name="error*" extends="default">
<put-attribute name="body" value="/WEB-INF/views/errorPages/error{1}.jsp" />
</definition>
</tiles-definitions>
Шаблон default
описывает основную страницу с "шапкой" и "подвалом". Остальные шаблоны ссылаются на шаблон по умолчанию, но заменяют атрибут body
.
Рассмотрим шаблон error*
, который используется для отображения страниц с ошибками.
<definition name="error*" extends="default">
<put-attribute name="body" value="/WEB-INF/views/errorPages/error{1}.jsp" />
</definition>
Допустим пользователю надо отобразить ошибку 500, то есть запрашивается шаблон error500
, тогда псевдошаблон, который описывает атрибуты будет выглядеть так.
<definition name="error500"template="/WEB-INF/views/default.jsp">
Выбор шаблона происходит "интеллектуально", то есть более специфичные шаблоны имеют приоритет над более общими. То есть, если запрашивается шаблон user/list
, то сначала будет идти поиск шаблона user/list
, потом user/*
и наконец */*
.
TODO!
TODO!
TODO!
TODO!
TODO!
Важным механизмом работы web-приложения являются страницы стандартных ошибок (500 - Internal server error, 403 - Forbidden/Access denied, 404 - file not found). Для задания обработчиков для таких страниц необходимо добавить описание того, где искать эти обработчики в дескрпторе развёртывания (web.xml).
Опишем обработчики ошибок для ошибок 500 и 403. web.xml
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/views/error403.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/views/error500.jsp</location>
</error-page>
Jsp-страница ошибки выглядит как обычная страница за исключением того, что установлен атрибут isErrorPage="true"
.
Рассмотрим пример интеграции Apache Tiles со страницами ошибок.
Для интуграции с tiles необходимо создать страницы ошибок, например, /WEB-INF/views/error500.jsp
следующего содержания.
/WEB-INF/views/error500.jsp
<%@page contentType="text/html" pageEncoding="UTF-8" isErrorPage="true" trimDirectiveWhitespaces="true"%>
<%@page isELIgnored="false" %>
<%@taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<tiles:insertDefinition name="error500" />
Данная страница ссылается на шаблон error500
из tiles.
Шаблон tiles будет выглядеть так.
layout.xml
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition name="default" template="/WEB-INF/views/default.jsp">
<put-attribute name="header" value="/WEB-INF/views/header.jsp" />
<put-attribute name="footer" value="/WEB-INF/views/footer.jsp" />
</definition>
<definition name="error*" extends="default">
<put-attribute name="body" value="/WEB-INF/views/errorPages/error{1}.jsp" />
</definition>
</tiles-definitions>
Шаблон error*
будет обрабатывать шаблон error500
, а в качестве атрибута body
будет подставлена страница /WEB-INF/views/errorPages/error500.jsp
.
/WEB-INF/views/errorPages/error500.jsp
<%@page contentType="text/html" pageEncoding="UTF-8" isErrorPage="true" trimDirectiveWhitespaces="true"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<div class="container">
<div class="wrapper" id="content">
<div class="inner">
<img src="/images/facepalm.png" />
<spring:message code="server.internal_error_msg" />
</div>
</div>
</div>
Аналогично поступим с error403
.
Вызов данных обработчиков осуществляется в следующих случаях:
-
error500
- произошла исключение и оно не перехвачено -
error403
- подключён Spring Security и сгенерировано исключениеAccessDeniedException
, которое не было перехвачено
TODO!
В ряде случае необходимо реализовать общую обработку на стороне JSP. Например, формирование таблиц из коллекций или форматирование даты.
Можно реализовать класс helper'а и вызывать его с использованием средств JSP, а можно определить собственный тэг, который будет производить обработку.
В JSP существует механизм расширения с использованием tld(tag library definition). Фактически реализуются классы, которые расширяют поведение JSP-парсера.
Для создания собственной библиотеки тэгов необходимо реализовать:
- класс, который реализует поведение и расширяет объект
javax.servlet.TagSupport
- описание библиотеки тэгов (xml-файл описания .tld)
Реализуем тэг для вывода коллекций в таблицу с раскраской чётных и нечётных строк.
Определим описание tld-библиотеки в файле /WEB-INF/a1stable.tld
a1stable.tld
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<description>a1s Tag Library</description>
<tlib-version>3.0</tlib-version>
<short-name>a1s</short-name>
<uri>http://www.a1-systems.com/tags</uri>
<tag>
<description>
Render table
</description>
<name>table</name>
<tag-class>a1s.learn.Table</tag-class>
<attribute>
<description>Items to render</description>
<name>str</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
</taglib>
-
taglib
-
description
- описание библиотеки -
uri
- URI пространства имён библиотеки (будет использоваться в JSP странице) -
tlib-version
- версия библиотеки тэгов (версия формата) -
short-name
- короткое название, может использоваться как предполагаемое имя префикса
-
-
tag
-
description
- описание тэга и функциональности -
name
- наименование тэга, которое будет использоваться после имени пространства имён (в нашем случае<a1s:*table* />
) -
tag-class
- наименование класса, который реализует логику работы тэга -
attribute
- атрибут тэга и класса, который будет распознан и засечен в объект тэга (через setter)-
description
- описание параметра -
name
- название параметра -
required
- обязательный или нет параметр? -
rtexprvalue
- необходимо ли проводить подстановку значений? (позволяет заменять${object}
на непосредственный объект)
-
-
Далее в шаблоне jsp необходимо подключить библиотеку типов
list.jspx
<div
...
xmlns:a1s="http://www.a1-systems.com/tags"
>
Для реализации класса тэга необходимо добавить jsp-api в зависимости проекта. Так как используем jetty, то воспользуемся jsp-api для Jetty.
pom.xml
<properties>
<jsp.api.version>7.0.0pre2</jsp.api.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-api-2.1</artifactId>
<version>${jsp.api.version}</version>
</dependency>
</dependencies>
Реализуем класса тэга.
Table.java
package a1s.learn;
import java.io.IOException;
import java.util.List;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.TagSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Table extends TagSupport {
protected static final Logger log = LoggerFactory.getLogger(Table.class);
private List<Object> str;
public List<Object> getStr() {
return str;
}
public void setStr(List<Object> str) {
this.str = str;
}
@Override
public int doStartTag() throws JspException {
log.debug("Start tag");
try {
//Get the writer object for output.
JspWriter out = pageContext.getOut();
int i=1;
out.print("<table border='1'>");
for (Object item:str) {
out.print("<tr style='background:#"+(i % 2 == 0 ? "ссс" : "fff")+"'>");
out.print("<td>"+i+"</td>");
out.print("<td>"+item.toString()+"</td>");
out.print("</tr>");
i++;
}
out.print("</table>");
} catch (IOException e) {
log.error(e.getStackTrace().toString());
}
return SKIP_BODY;
}
}
TODO!
Добавим на страницу вывод таблицы с коллекцией themes
.
list.jspx
<a1s:table str="${themes}" />
Скомпилируем и соберём проект. Результирующий вид страницы с внедрённым тэгом:
Todo!