Skip to content

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

wizardjedi edited this page Nov 26, 2012 · 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.0.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

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

TODO!

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

TODO!

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

TODO!

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

TODO!

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

TODO!

Создание собственной библиотеки тэгов 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
    • tlib-version
    • short-name
  • tag
    • description
    • name - наименование тэга, которое будет использоваться после имени пространства имён (в нашем случае <a1s:*table* />)
    • tag-class - наименование класса, который реализует логику работы тэга
    • attribute - атрибут тэга и класса, который будет распознан и засечен в объект тэга
      • description - описание параметра
      • name - название параметра
      • required - обязательный или нет параметр?
      • rtexprvalue - необходимо ли проводить подстановку значений?

Далее в шаблоне jsp необходимо подключить библиотеку типов

list.jspx

<div
	...
	xmlns:a1s="http://www.a1-systems.com/tags"
 >

Реализуем класса тэга.

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

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

Taglib screenshot