Skip to content

Введение в spring web mvc

wizardjedi edited this page May 11, 2013 · 83 revisions

Создание базового web-приложения

Создадим в среде 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-приложения.

Browser window

Структура каталогов для web-приложения в maven

В 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)
  • Рендеринг шаблона по имени с передачей в него параметров модели, полученных от контроллера

MVC

Схема работы сервлетов на сервере сервлетов (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.

Создание view

Наши шаблоны для вывода будут располагаться по адресу 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>

Browser window for minimal view and controller

Рассмотрим создание 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") - указывает что метод будет вызван при запросе URL user/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 влияет на содержимое, а также видна работа с параметрами.

Browser window without parameters

Browser window with parameters

Обработка форм

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";
}

Simple form

Populated simple form

Рассмотрим методы более подробно:

@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>

Создание полноценной формы с использованием пространства имён form:

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>

Browser window with form

Browser window with populated form

Browser window with invalid populated form

Рассмотрим тэги подробнее

  • <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>

bootstraped form

bootstraped form

bootstraped form

Ресурсы располагаются в файловой системе по адресу 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>

Validation

На рисунке показан снимок экрана с сообщениями об ошибках в случае неверного заполнения формы.

Использование apache tiles

Кроме вывода по шаблону в 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/.

Web browser with tiles

На странице появились 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;
	}
}

Будем реализовывать форму как представленная на рисунке ниже:

Advanced form screenshot

Для вывода элементов формы воспользуемся пространством имён 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.

Advanced populated form screenshot

TODO! form:select, form:checkboxes

Аутентификация и авторизация с помощью Spring Security

В 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, то увидим форму ввода логина и пароля. Перейти к приложению можно будет только тогда, когда будут введены правильные логин и пароль.

Login form screenshot Login failed form screenshot

Создание собственной формы логина и интеграция с Apache tiles

Рассмотрим форму для ввода логина и пароля, которую генерирует 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 - для пароля

Это значения по умолчанию и их можно заменить.

Создадим форму как на рисунке ниже:

Custom login form

Для начала реализуем форму в файле /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>

Соберём и запустим приложение.

failed login attempt

Форма входа после неудачного ввода логина и пароля.

success login attempt

Результирующий вид страницы после входа в систему. В header'е страницы выводится имя текущего пользователя

Работа с LDAP и более подробная работа с Spring security рассмотрены в соответствующем разделе Spring security.

Создание динамических tiles

В рассмотренном механизме работы с 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/* и наконец */*.

Работы с tiles с использованием AJAX

TODO!

Использование AJAX и Apache Tiles

TODO!

Создание динамических форм

TODO!

Локализация и интернационализация в Spring MVC

В состав spring входят инструменты для поддержки локализация и интернационализации: фильтры для смены локали и тэги для вывода строк из Message Source.

Добавим в проект поддержку русского и английского языка. Строки для перевода будут хранится в файлах типа messages_ru.properties и message_en.properties.

Создадим файлы messages_ru.properties и message_en.peoprties по адресу /src/main/resources/ в проекте.

/src/main/resources/messages_ru.peoprties

welcome=Приветственное сообщение.

В коде страниц будем ссылаться на строки с помощью тэга <:message />.

<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
...
<spring:message code="welcome" />

Теперь добавим загрузку данных в Message Source.

appServlet-servlet.xml

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="basenames">
		<list>
			<value>messages</value>
		</list>
	</property>
</bean>

Теперь необходимо добавить resolver, который будет определять локаль по cookie или устанаваливает локаль по умолчанию.

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
	<property name="defaultLocale" value="ru" />
	<property name="cookieName" value="locale" />
</bean>

Далее необходимо добавить interceptor, который будет изменять локаль по данным из строки запроса. Определим параметр lang, который будет отвечать за изменение локали. При запросе с параметром lang=ru будет установлена русская локаль, lang=en устанавливает английскую локаль.

Добавим interceptor в раздел interceptors.

<mvc:interceptors>
	<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
		<property name="paramName" value="lang" />
	</bean>
</mvc:interceptors>

Поддержка тем

Поддержка тем реализуется по аналогии с изменением локали. Приведём фрагмент настройки interceptor'ов и т.д. для поддержки тем.

<mvc:interceptors>
	<bean id="themeChangeInterceptor" class="org.springframework.web.servlet.theme.ThemeChangeInterceptor">
		<property name="paramName" value="theme" />
	</bean>
</mvc:interceptors>

<bean id="themeResolver" class="org.springframework.web.servlet.theme.CookieThemeResolver">
	<property name="defaultThemeName" value="default" />
	<property name="cookieName" value="theme" />
</bean>

<bean class="org.springframework.ui.context.support.ResourceBundleThemeSource" id="themeSource">
	<property name="basenamePrefix" value="themes/" />
</bean>

Доступ к свойствам темы осуществляется следующим образом:

<spring:theme code="some_variable" />

Параметры темы должны быть описаны в файлах theme_name.properties в директории /src/main/resources/themes/.

Запрет на редактирование отдельных элементов формы

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, которое не было перехвачено

Обработка исключений

Spring MVC позволяет перехватывать исключения и отображать страницу с соответствующей ошибкой.

Например, реализуем исключения ResourceNotFoundException с отображением соответствующей страницы.

Объявим исключение и добавим аннотацию, соответствующую коду 404.

@ResponseStatus(value= HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
	public NotFoundException(String message){
		super(message);
	}
}

В методе контроллера добавим некоторую проверку и генерацию исключения:

@RequestMapping(value = "/some-page",method=RequestMethod.GET)
public String somePage(Model ui)
{
	if (!this.getResource()) {
		throw new NotFoundException("Not found");
	} else {
		...
		return "resources/view";
	}
}

Теперь в дескрипторе развёртываня добавим описание ошибки 404. web.xml

<error-page>
	<error-code>404</error-code>
	<location>/WEB-INF/views/error404.jsp</location>
</error-page>

По аналогии с другими ошибками (403 и 500) добавим файл /WEB-INF/views/error404.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="error404" />

и файл со страницей 404.

/WEB-INF/views/errorPages/error404.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>
			File not found
		</div>
	</div>
</div>

Создание собственной библиотеки тэгов TLD (Tag library definition)

В ряде случае необходимо реализовать общую обработку на стороне JSP. Например, формирование таблиц из коллекций или форматирование даты.

Можно реализовать класс helper'а и вызывать его с использованием средств JSP, а можно определить собственный тэг, который будет производить обработку.

В JSP существует механизм расширения с использованием tld(tag library definition). Фактически реализуются классы, которые расширяют поведение JSP-парсера.

Для создания собственной библиотеки тэгов необходимо реализовать:

  • класс, который реализует поведение и расширяет объект javax.servlet.TagSupport
  • описание библиотеки тэгов (xml-файл описания .tld)

Реализуем тэг для вывода коллекций в таблицу с раскраской чётных и нечётных строк.

Taglib screenshot

Определим описание 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;
	}
}

Рассмотрим описание класса тэга более подробно. Класс наследуется от класса TagSupport, который реализует интерфейс Tag. Данный класс реализует основные методы для поддержки написания тэгов.

Основным свойством данного класса является свойство pageContext, которое позволяет получить доступ к контексту страницы. Данный объект позволяет, например, получить JspWriter, с помощью которого осуществляется вывод на страницу, или получить объект сервлета, запроса или ответа.

В процессе своей работы JSP-парсер вызывает методы для классов тэгов, которые настраивают контекст страницы, устанавливают свойства и отвечают за жизненный цикл тэга.

Рассмотрим основные методы:

  • void setPageContext(PageContext pageContext) - установить контекст страницы
  • void setParent(Tag t) - установить родительский тэг
  • int doStartTag() - обработчик открывающего тега
  • int doEndTag() - обработчик закрывающего тега
  • int doAfterBody() - обработчик вызываемый после обработки тела тэга

Обработчики жизненного цикла возвращают числовые константы, которые влияют на поведение JSP-парсера.

Например,

  • EVAL_BODY - обработать тело тэга
  • EVAL_BODY_AGAIN - обработать тело тэга ещё раз
  • SKIP_BODY - пропустить тело тэга
  • EVAL_PAGE - продолжить обработку страницы
  • SKIP_PAGE - завершить обработку страницы

Таким образом, наш тэг получает все настройки через сеттеры, тело тэга не используется, поэтому мы реализуем обработчик doStartTag() и вернём SKIP_BODY (тело тэга является не значащим), а внутри методы выведем таблицу в виде HTML с помощью JspWriter'а.

Добавим на страницу вывод таблицы с коллекцией themes.

list.jspx

<a1s:table str="${themes}" />

Скомпилируем и соберём проект. Результирующий вид страницы с внедрённым тэгом:

Taglib screenshot

Flash attributes

В разработке web-приложений часто используется шаблон POST-REDIRECT-GET, который исключает повторную отправку данных, если пользователь нажал F5 после отправки формы.

Данный шаблон характерен для операций редактирования и добавления.

После того, как было сделано некоторые изменения, хорошо бы вывести пользователю уведомление об успешности его действий, но на предыдущем шаге был сделан redirect и вывод уведомления усложняется.

В данной случае нам на помощь приходят flash message, которые действуют следующим образом:

  • При сохранении записать сообщение в сессию
  • При запросе страницы показать сообщение из сессии
  • Уничтожить данные в сессии, для исключения повторного отображения сообщения

В Spring MVC за данную функциональность отвечают flash attributes.

Рассмотрим метод работы с flash attributes.

public String someAction(RedirectAttributes redirectAttributes) {

	redirectAttributes.addFlashAttribute("controller.message", "Operation performed successfully...");

	return "redirect:...";
}

А в jsp-странице достаточно осуществить вывод соответствующего объекта:

...
${controller.message}
<!-- Edit form -->
...