Skip to content

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

Sayapin Alexander edited this page Feb 24, 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

TODO!

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

TODO!

Создание страниц с ошибками

Важным механизмом работы web-приложения являются страницы стандартных ошибок (500 - Internal server error, 403 - Forbidden/Access denied, 404 - file not found). Для задания обработчиков для таких страниц необходимо добавить описание того, где искать эти обработчики в дескрпторе развёртывания (web.xml).

Опишем обработчики ошибок для ошибок 500 и 403. web.xml

<error-page>
	<error-code>403</error-code>
	<location>/WEB-INF/views/error403.jsp</location>
</error-page>
<error-page>
	<error-code>500</error-code>
	<location>/WEB-INF/views/error500.jsp</location>
</error-page>

Jsp-страница ошибки выглядит как обычная страница за исключением того, что установлен атрибут isErrorPage="true".

Рассмотрим пример интеграции Apache Tiles со страницами ошибок. Для интуграции с tiles необходимо создать страницы ошибок, например, /WEB-INF/views/error500.jsp следующего содержания.

/WEB-INF/views/error500.jsp

<%@page contentType="text/html" pageEncoding="UTF-8" isErrorPage="true" trimDirectiveWhitespaces="true"%>
<%@page isELIgnored="false" %>
<%@taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<tiles:insertDefinition name="error500" />

Данная страница ссылается на шаблон error500 из tiles. Шаблон tiles будет выглядеть так. layout.xml

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
	<definition name="default" template="/WEB-INF/views/default.jsp">
		<put-attribute name="header" value="/WEB-INF/views/header.jsp" />
		<put-attribute name="footer" value="/WEB-INF/views/footer.jsp" />
	</definition>
	<definition name="error*" extends="default">
		<put-attribute name="body" value="/WEB-INF/views/errorPages/error{1}.jsp" />
	</definition>
</tiles-definitions>

Шаблон error* будет обрабатывать шаблон error500, а в качестве атрибута body будет подставлена страница /WEB-INF/views/errorPages/error500.jsp.

/WEB-INF/views/errorPages/error500.jsp

<%@page contentType="text/html" pageEncoding="UTF-8" isErrorPage="true" trimDirectiveWhitespaces="true"%>

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

<div class="container">
	<div class="wrapper" id="content">
		<div class="inner">
			<img src="/images/facepalm.png" />
			<spring:message code="server.internal_error_msg" />
		</div>
	</div>
</div>

Аналогично поступим с error403.

Вызов данных обработчиков осуществляется в следующих случаях:

  • error500 - произошла исключение и оно не перехвачено
  • error403 - подключён Spring Security и сгенерировано исключение AccessDeniedException, которое не было перехвачено

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

TODO!

Создание собственной библиотеки тэгов 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;
	}
}

TODO!

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

list.jspx

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

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

Taglib screenshot

Flash attributes

Todo!