I want to use a form without an object. How to display errors?
Controller:
@PostMapping(value="/submit")
public String doSubmit(@RequestParam("myParameter") String param, BindingResult bindingResult) {
bindingResult.rejectValue("myParameter", "Oh nooo!!!!");
return "html";
}
HTML:
<form th:action = "@{/submit}" method="post">
<label for="myParameter" class="form-label">My Parameter</label>
<input name="myParameter" class="form-control" type="text"/>
<span class="text-danger" th:if="${#fields.hasErrors('myParameter')}" th:errors="myParameter">Error</span>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
th:if="${#fields.hasErrors('myParameter')}" th:errors="myParameter"
throws errors, cause «myParameter» is not known.
How to display the error, without using th:object
in <form>
asked Jul 7, 2022 at 13:03
ThanthlaThanthla
4861 gold badge8 silver badges28 bronze badges
1
I did it with the help of this question Thymeleaf: display global errors by error code
@PostMapping(value="/submit")
public String doSubmit(@RequestParam("myParameter") String param, BindingResult bindingResult) {
bindingResult.reject("myParameter", "myParameter");
return "html";
}
Important: myParameter
must not be a key of a message source!
<form th:action="@{/submit}" method="post">
<label for="myParameter" class="form-label">My Parameter</label>
<input name="myParameter" class="form-control" type="text"/>
<span class="text-danger" th:if="${#lists.contains(#fields.globalErrors(), "myParameter")}" th:text="An Error happened">Error</span>
<button type="submit" class="btn btn-primary">Submit</button>
Important: Use "
to indicate the string. th:text? may contain a message key like
th:text=»#{error.code1}»`.
answered Jul 8, 2022 at 7:49
ThanthlaThanthla
4861 gold badge8 silver badges28 bronze badges
Preface
This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC.
Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two separate libraries called thymeleaf-spring3
and thymeleaf-spring4
. These libraries are packaged in separate .jar
files (thymeleaf-spring3-{version}.jar
and thymeleaf-spring4-{version}.jar
) and need to be added to your classpath in order to use Thymeleaf’s Spring integrations in your application.
The code samples and example application in this tutorial make use of Spring 4.x and its corresponding Thymeleaf integrations, but the contents of this text are valid also for Spring 3.x. If your application uses Spring 3.x, all you have to do is replace the org.thymeleaf.spring4
package with org.thymeleaf.spring3
in the code samples.
1 Integrating Thymeleaf with Spring
Thymeleaf offers a set of Spring integrations that allow you to use it as a full-featured substitute for JSP in Spring MVC applications.
These integrations will allow you to:
- Make the mapped methods in your Spring MVC
@Controller
objects forward to templates managed by Thymeleaf, exactly like you do with JSPs. - Use Spring Expression Language (Spring EL) instead of OGNL in your templates.
- Create forms in your templates that are completely integrated with your form-backing beans and result bindings, including the use of property editors, conversion services and validation error handling.
- Display internationalization messages from messages files managed by Spring (through the usual
MessageSource
objects).
Note that in order to fully understand this tutorial, you should have first gone through the “Using Thymeleaf” tutorial, which explains the Standard Dialect in depth.
2 The SpringStandard Dialect
In order to achieve an easier and better integration, Thymeleaf provides a dialect which specifically implements all the needed features for it to work correctly with Spring.
This specific dialect is based on the Thymeleaf Standard Dialect and is implemented in a class called org.thymeleaf.spring4.dialect.SpringStandardDialect
, which in fact extends from org.thymeleaf.standard.StandardDialect
.
Besides all the features already present in the Standard Dialect – and therefore inherited –, the SpringStandard Dialect introduces the following specific features:
- Use Spring Expression Language (Spring EL) as a variable expression language, instead of OGNL. Consequently, all
${...}
and*{...}
expressions will be evaluated by Spring’s Expression Language engine. - Access any beans in your application context using SpringEL’s syntax:
${@myBean.doSomething()}
- New attributes for form processing:
th:field
,th:errors
andth:errorclass
, besides a new implementation ofth:object
that allows it to be used for form command selection. - An expression object and method,
#themes.code(...)
, which is equivalent to thespring:theme
JSP custom tag. - An expression object and method,
#mvc.uri(...)
, which is equivalent to thespring:mvcUrl(...)
JSP custom function (only in Spring 4.1+). - New DTDs for validation, including these new attributes, as well as new corresponding DOCTYPE translation rules.
Note that you shouldn’t use this dialect directly in a normal TemplateEngine object as a part of its configuration. Instead, you should instance a new template engine class that performs all the required configuration steps: org.thymeleaf.spring4.SpringTemplateEngine
.
An example bean configuration:
<bean id="templateResolver"
class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML5" />
</bean>
<bean id="templateEngine"
class="org.thymeleaf.spring4.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
3 Views and View Resolvers
3.1 Views and View Resolvers in Spring MVC
There are two interfaces in Spring MVC that conform the core of its templating system:
org.springframework.web.servlet.View
org.springframework.web.servlet.ViewResolver
Views model pages in our applications and allow us to modify and predefine their behaviour by defining them as beans. Views are in charge of rendering the actual HTML interface, usually by the execution of some template engine like JSP (or Thymeleaf).
ViewResolvers are the objects in charge of obtaining View objects for a specific operation and locale. Typically, controllers ask ViewResolvers to forward to a view with a specific name (a String returned by the controller method), and then all the view resolvers in the application execute in ordered chain until one of them is able to resolve that view, in which case a View object is returned and control is passed to it for the renderization of HTML.
Note that not all pages in our applications have to be defined as Views, but only those which behaviour we wish to be non-standard or configured in a specific way (for example, by wiring some special beans to it. If a ViewResolver is asked a view that has no corresponding bean —which is the common case—, a new View object is created ad hoc and returned.
A typical configuration for a JSP+JSTL ViewResolver in a Spring MVC application looks like this:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsps/" />
<property name="suffix" value=".jsp" />
<property name="order" value="2" />
<property name="viewNames" value="*jsp" />
</bean>
A quick look at its properties is enough to know about how it’s configured:
viewClass
establishes the class of the View instances. This is needed for a JSP resolver, but it will not be needed at all when we’re working with Thymeleaf.prefix
andsuffix
work in a similar way to the attributes of the same names in Thymeleaf’s TemplateResolver objects.order
establishes the order in which the ViewResolver will be queried in the chain.viewNames
allows the definition (with wildcards) of the view names that will be resolved by this ViewResolver.
3.2 Views and View Resolvers in Thymeleaf
Thymeleaf offers implementations for the two interfaces mentioned above:
org.thymeleaf.spring4.view.ThymeleafView
org.thymeleaf.spring4.view.ThymeleafViewResolver
These two classes will be in charge of processing Thymeleaf templates as a result of the execution of controllers.
Configuration of the Thymeleaf View Resolver is very similar to that of JSP’s:
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<property name="order" value="1" />
<property name="viewNames" value="*.html,*.xhtml" />
</bean>
The templateEngine
parameter is, of course, the SpringTemplateEngine
object we defined in the previous chapter. The other two (order
and viewNames
) are both optional, and have the same meaning as in the JSP ViewResolver we saw before.
Note that we do not need prefix
or suffix
parameters, because these are already specified at the Template Resolver (which in turn is passed to the Template Engine).
And what if we wanted to define a View
bean and add some static variables to it? Easy:
<bean name="main" class="org.thymeleaf.spring4.view.ThymeleafView">
<property name="staticVariables">
<map>
<entry key="footer" value="Some company: <b>ACME</b>" />
</map>
</property>
</bean>
4 Template Resolution
4.1 Spring-based Template Resolution
When used with Spring, Thymeleaf provides additional implementations of ITemplateResolver
and an associated IResourceResolver
, fully integrated with Spring’s resource resolution mechanism. These are:
org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver
for resolving templates.org.thymeleaf.spring4.resourceresolver.SpringResourceResourceResolver
, mostly for internal use.
This template resolver will allow applications to resolve templates using the standard Spring resource resolution syntax. It can be configured like:
<bean id="templateResolver"
class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML5" />
</bean>
And this will allow you to use view names like:
@RequestMapping("/doit")
public String doIt() {
...
return "classpath:resources/templates/doit";
}
Note that this Spring-based resource resolver will never be used by default. It will just be an option available for applications to configure in addition to the other template resolver implementations offered by the Thymeleaf core.
5 Spring Thyme Seed Starter Manager
The source code for the examples shown in this and future chapters of this guide can be found in the Spring Thyme Seed Starter Manager GitHub repository.
5.1 The Concept
At Thymeleaf we’re huge fans of thyme, and every spring we prepare our seed starting kits with good soil and our favourite seeds, place them under the Spanish sun and patiently wait for our new plants to grow.
But this year we got fed up with sticking labels to the seed starter containers for knowing which seed was in each cell of the container, so we decided to prepare an application using Spring MVC and Thymeleaf to help us catalogue our starters: The Spring Thyme SeedStarter Manager.
In a similar way to the Good Thymes Virtual Grocery application we developed in the Using Thymeleaf tutorial, the STSM will allow us to exemplify the most important aspects of the integration of Thymeleaf as a template engine for Spring MVC.
5.2 Business Layer
We will need a very simple business layer for our application. First of all, let’s have a look at our model entities:
A couple of very simple service classes will provide the required business methods. Like:
@Service
public class SeedStarterService {
@Autowired
private SeedStarterRepository seedstarterRepository;
public List<SeedStarter> findAll() {
return this.seedstarterRepository.findAll();
}
public void add(final SeedStarter seedStarter) {
this.seedstarterRepository.add(seedStarter);
}
}
And:
@Service
public class VarietyService {
@Autowired
private VarietyRepository varietyRepository;
public List<Variety> findAll() {
return this.varietyRepository.findAll();
}
public Variety findById(final Integer id) {
return this.varietyRepository.findById(id);
}
}
5.3 Spring MVC configuration
Next we need to set up the Spring MVC configuration for the application, which will include not only the standard Spring MVC artifacts like resource handling or annotation scanning, but also the creation of the Template Engine and View Resolver instances.
<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- **************************************************************** -->
<!-- RESOURCE FOLDERS CONFIGURATION -->
<!-- Dispatcher configuration for serving static resources -->
<!-- **************************************************************** -->
<mvc:resources location="/images/" mapping="/images/**" />
<mvc:resources location="/css/" mapping="/css/**" />
<!-- **************************************************************** -->
<!-- SPRING ANNOTATION PROCESSING -->
<!-- **************************************************************** -->
<mvc:annotation-driven conversion-service="conversionService" />
<context:component-scan base-package="thymeleafexamples.stsm" />
<!-- **************************************************************** -->
<!-- MESSAGE EXTERNALIZATION/INTERNATIONALIZATION -->
<!-- Standard Spring MessageSource implementation -->
<!-- **************************************************************** -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="Messages" />
</bean>
<!-- **************************************************************** -->
<!-- CONVERSION SERVICE -->
<!-- Standard Spring formatting-enabled implementation -->
<!-- **************************************************************** -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
<bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
</set>
</property>
</bean>
<!-- **************************************************************** -->
<!-- THYMELEAF-SPECIFIC ARTIFACTS -->
<!-- TemplateResolver <- TemplateEngine <- ViewResolver -->
<!-- **************************************************************** -->
<bean id="templateResolver"
class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML5" />
</bean>
<bean id="templateEngine"
class="org.thymeleaf.spring4.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
</bean>
</beans>
Important: Note that we have selected HTML5 as a template mode.
5.4 The Controller
Of course, we will also need a controller for our application. As the STSM will only contain one web page with a list of seed starters and a form for adding new ones, we will write only one controller class for all the server interactions:
@Controller
public class SeedStarterMngController {
@Autowired
private VarietyService varietyService;
@Autowired
private SeedStarterService seedStarterService;
...
}
Now let’s see what we can add to this controller class.
Model Attributes
First we will add some model attributes that we will need in the page:
@ModelAttribute("allTypes")
public List<Type> populateTypes() {
return Arrays.asList(Type.ALL);
}
@ModelAttribute("allFeatures")
public List<Feature> populateFeatures() {
return Arrays.asList(Feature.ALL);
}
@ModelAttribute("allVarieties")
public List<Variety> populateVarieties() {
return this.varietyService.findAll();
}
@ModelAttribute("allSeedStarters")
public List<SeedStarter> populateSeedStarters() {
return this.seedStarterService.findAll();
}
Mapped methods
And now the most important part of a controller, the mapped methods: one for showing the form page, and other for processing the addition of new Seed Starter objects.
@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter) {
seedStarter.setDatePlanted(Calendar.getInstance().getTime());
return "seedstartermng";
}
@RequestMapping(value="/seedstartermng", params={"save"})
public String saveSeedstarter(
final SeedStarter seedStarter, final BindingResult bindingResult, final ModelMap model) {
if (bindingResult.hasErrors()) {
return "seedstartermng";
}
this.seedStarterService.add(seedStarter);
model.clear();
return "redirect:/seedstartermng";
}
5.5 Configuring a Conversion Service
In order to allow easy formatting of Date
and also Variety
objects in our view layer, we registered a Spring ConversionService
implementation at the application context. See it again:
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
...
<mvc:annotation-driven conversion-service="conversionService" />
...
<!-- **************************************************************** -->
<!-- CONVERSION SERVICE -->
<!-- Standard Spring formatting-enabled implementation -->
<!-- **************************************************************** -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
<bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
</set>
</property>
</bean>
...
</beans>
That conversion service allowed us to register two Spring formatters, implementations of the org.springframework.format.Formatter
interface. For more information on how the Spring conversion infrastructure works, see the docs at spring.io.
Let’s have a look at the DateFormatter
, which formats dates according to a format string present at the date.format
message key of our Messages.properties
:
public class DateFormatter implements Formatter<Date> {
@Autowired
private MessageSource messageSource;
public DateFormatter() {
super();
}
public Date parse(final String text, final Locale locale) throws ParseException {
final SimpleDateFormat dateFormat = createDateFormat(locale);
return dateFormat.parse(text);
}
public String print(final Date object, final Locale locale) {
final SimpleDateFormat dateFormat = createDateFormat(locale);
return dateFormat.format(object);
}
private SimpleDateFormat createDateFormat(final Locale locale) {
final String format = this.messageSource.getMessage("date.format", null, locale);
final SimpleDateFormat dateFormat = new SimpleDateFormat(format);
dateFormat.setLenient(false);
return dateFormat;
}
}
The VarietyFormatter
automatically converts between our Variety
entities and the way we want to use them in our forms (basically, by their id
field values):
public class VarietyFormatter implements Formatter<Variety> {
@Autowired
private VarietyService varietyService;
public VarietyFormatter() {
super();
}
public Variety parse(final String text, final Locale locale) throws ParseException {
final Integer varietyId = Integer.valueOf(text);
return this.varietyService.findById(varietyId);
}
public String print(final Variety object, final Locale locale) {
return (object != null ? object.getId().toString() : "");
}
}
We will learn more on how these formatters affect the way our data is displayed later on.
6 Listing Seed Starter Data
The first thing that our /WEB-INF/templates/seedstartermng.html
page will show is a listing with the seed starters currently stored. For this we will need some externalized messages and also some expression evaluation on model attributes. Like this:
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
<h2 th:text="#{title.list}">List of Seed Starters</h2>
<table>
<thead>
<tr>
<th th:text="#{seedstarter.datePlanted}">Date Planted</th>
<th th:text="#{seedstarter.covered}">Covered</th>
<th th:text="#{seedstarter.type}">Type</th>
<th th:text="#{seedstarter.features}">Features</th>
<th th:text="#{seedstarter.rows}">Rows</th>
</tr>
</thead>
<tbody>
<tr th:each="sb : ${allSeedStarters}">
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
<td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>
<td th:text="#{${'seedstarter.type.' + sb.type}}">Wireframe</td>
<td th:text="${#strings.arrayJoin(
#messages.arrayMsg(
#strings.arrayPrepend(sb.features,'seedstarter.feature.')),
', ')}">Electric Heating, Turf</td>
<td>
<table>
<tbody>
<tr th:each="row,rowStat : ${sb.rows}">
<td th:text="${rowStat.count}">1</td>
<td th:text="${row.variety.name}">Thymus Thymi</td>
<td th:text="${row.seedsPerCell}">12</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
Lots to see here. Let’s have a look at each fragment separately.
First of all, this section will only be shown if there are any seed starters. We achieve that with a th:unless attribute and the #lists.isEmpty(...)
function.
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
Note that all utility objects like #lists
are available in Spring EL expressions just as they were in OGNL expressions in the Standard Dialect.
The next thing to see is a lot of internationalized (externalized) texts, like:
<h2 th:text="#{title.list}">List of Seed Starters</h2>
<table>
<thead>
<tr>
<th th:text="#{seedstarter.datePlanted}">Date Planted</th>
<th th:text="#{seedstarter.covered}">Covered</th>
<th th:text="#{seedstarter.type}">Type</th>
<th th:text="#{seedstarter.features}">Features</th>
<th th:text="#{seedstarter.rows}">Rows</th>
...
This being a Spring MVC application, we already defined a MessageSource
bean in our spring XML configuration (MessageSource
objects are the standard way of managing externalized texts in Spring MVC):
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="Messages" />
</bean>
…and that basename property indicates that we will have files like Messages_es.properties
or Messages_en.properties
in our classpath. Let’s have a look at the Spanish version:
title.list=Lista de semilleros
date.format=dd/MM/yyyy
bool.true=sí
bool.false=no
seedstarter.datePlanted=Fecha de plantación
seedstarter.covered=Cubierto
seedstarter.type=Tipo
seedstarter.features=Características
seedstarter.rows=Filas
seedstarter.type.WOOD=Madera
seedstarter.type.PLASTIC=Plástico
seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros
seedstarter.feature.FERTILIZER=Fertilizante
seedstarter.feature.PH_CORRECTOR=Corrector de PH
In the first column of the table listing we will show the date when the seed starter was prepared. But we will show it formatted in the way we defined in our DateFormatter
. In order to do that we will use the double-bracket syntax, which will automatically apply the Spring Conversion Service.
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
Next is showing whether the seed starter container is covered or not, by transforming the value of the boolean covered bean property into an internationalized “yes” or “no” with a conditional expression:
<td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>
Now we have to show the type of seed starter container. Type is a java enum with two values (WOOD
and PLASTIC
), and that’s why we defined two properties in our Messages
file called seedstarter.type.WOOD
and seedstarter.type.PLASTIC
.
But in order to obtain the internationalized names of the types, we will need to add the seedstarter.type.
prefix to the enum value by means of an expression, which result we will then use as the message key:
<td th:text="#{${'seedstarter.type.' + sb.type}}">Wireframe</td>
The most difficult part of this listing is the features column. In it we want to display all the features of our container —that come in the form of an array of Feature
enums—, separated by commas. Like “Electric Heating, Turf”.
Note that this is particularly difficult because these enum values also need to be externalized, as we did with Types. The flow is then:
- Prepend the corresponding prefix to all the elements of the
features
array. - Obtain the externalized messages corresponding to all the keys from step 1.
- Join all the messages obtained in step 2, using a comma as a delimiter.
For achieving this, we create the following code:
<td th:text="${#strings.arrayJoin(
#messages.arrayMsg(
#strings.arrayPrepend(sb.features,'seedstarter.feature.')),
', ')}">Electric Heating, Turf</td>
The last column of our listing will be quite simple, in fact. Even if it has a nested table for showing the contents of each row in the container:
<td>
<table>
<tbody>
<tr th:each="row,rowStat : ${sb.rows}">
<td th:text="${rowStat.count}">1</td>
<td th:text="${row.variety.name}">Thymus Thymi</td>
<td th:text="${row.seedsPerCell}">12</td>
</tr>
</tbody>
</table>
</td>
7 Creating a Form
7.1 Handling the command object
Command object is the name Spring MVC gives to form-backing beans, this is, to objects that model a form’s fields and provide getter and setter methods that will be used by the framework for establishing and obtaining the values input by the user at the browser side.
Thymeleaf requires you to specify the command object by using a th:object
attribute in your <form>
tag:
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
...
</form>
This is consistent with other uses of th:object,
but in fact this specific scenario adds some limitations in order to correctly integrate with Spring MVC’s infrastructure:
- Values for
th:object
attributes in form tags must be variable expressions (${...}
) specifying only the name of a model attribute, without property navigation. This means that an expression like${seedStarter}
is valid, but${seedStarter.data}
would not be. - Once inside the
<form>
tag, no otherth:object
attribute can be specified. This is consistent with the fact that HTML forms cannot be nested.
7.2 Inputs
Let’s see now how to add an input to our form:
<input type="text" th:field="*{datePlanted}" />
As you can see, we are introducing a new attribute here: th:field.
This is a very important feature for Spring MVC integration because it does all the heavy work of binding your input with a property in the form-backing bean. You can see it as an equivalent of the path attribute in a tag from Spring MVC’s JSP tag library.
The th:field
attribute behaves differently depending on whether it is attached to an <input>
, <select>
or <textarea>
tag (and also depending on the specific type of <input>
tag). In this case (input[type=text]
), the above line of code is similar to:
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
…but in fact it is a little bit more than that, because th:field
will also apply the registered Spring Conversion Service, including the DateFormatter
we saw before (even if the field expression is not double-bracketed). Thanks to this, the date will be shown correctly formatted.
Values for th:field
attributes must be selection expressions (*{...}
), which makes sense given the fact that they will be evaluated on the form-backing bean and not on the context variables (or model attributes in Spring MVC jargon).
Contrary to the ones in th:object
, these expressions can include property navigation (in fact any expression allowed for the path attribute of a <form:input>
JSP tag will be allowed here).
Note that th:field
also understands the new types of <input>
element introduced by HTML5 like <input type="datetime" ... />
, <input type="color" ... />
, etc., effectively adding complete HTML5 support to Spring MVC.
7.3 Checkbox fields
th:field
also allows us to define checkbox inputs. Let’s see an example from our HTML page:
<div>
<label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">Covered</label>
<input type="checkbox" th:field="*{covered}" />
</div>
Note there’s some fine stuff here besides the checkbox itself, like an externalized label and also the use of the #ids.next('covered')
function for obtaining the value that will be applied to the id attribute of the checkbox input.
Why do we need this dynamic generation of an id attribute for this field? Because checkboxes are potentially multi-valued, and thus their id values will always be suffixed a sequence number (by internally using the #ids.seq(...)
function) in order to ensure that each of the checkbox inputs for the same property has a different id value.
We can see this more easily if we look at such a multi-valued checkbox field:
<ul>
<li th:each="feat : ${allFeatures}">
<input type="checkbox" th:field="*{features}" th:value="${feat}" />
<label th:for="${#ids.prev('features')}"
th:text="#{${'seedstarter.feature.' + feat}}">Heating</label>
</li>
</ul>
Note that we’ve added a th:value
attribute this time, because the features field is not a boolean like covered was, but instead is an array of values.
Let’s see the HTML output generated by this code:
<ul>
<li>
<input id="features1" name="features" type="checkbox" value="SEEDSTARTER_SPECIFIC_SUBSTRATE" />
<input name="_features" type="hidden" value="on" />
<label for="features1">Seed starter-specific substrate</label>
</li>
<li>
<input id="features2" name="features" type="checkbox" value="FERTILIZER" />
<input name="_features" type="hidden" value="on" />
<label for="features2">Fertilizer used</label>
</li>
<li>
<input id="features3" name="features" type="checkbox" value="PH_CORRECTOR" />
<input name="_features" type="hidden" value="on" />
<label for="features3">PH Corrector used</label>
</li>
</ul>
We can see here how a sequence suffix is added to each input’s id attribute, and how the #ids.prev(...)
function allows us to retrieve the last sequence value generated for a specific input id.
Don’t worry about those hidden inputs with
name="_features"
: they are automatically added in order to avoid problems with browsers not sending unchecked checkbox values to the server upon form submission.
Also note that if our features property contained some selected values in our form-backing bean, th:field
would have taken care of that and would have added a checked="checked"
attribute to the corresponding input tags.
7.4 Radio Button fields
Radio button fields are specified in a similar way to non-boolean (multi-valued) checkboxes —except that they are not multivalued, of course:
<ul>
<li th:each="ty : ${allTypes}">
<input type="radio" th:field="*{type}" th:value="${ty}" />
<label th:for="${#ids.prev('type')}" th:text="#{${'seedstarter.type.' + ty}}">Wireframe</label>
</li>
</ul>
7.5 Dropdown/List selectors
Select fields have two parts: the <select>
tag and its nested <option>
tags. When creating this kind of field, only the <select>
tag has to include a th:field
attribute, but the th:value
attributes in the nested <option>
tags will be very important because they will provide the means of knowing which is the currently selected option (in a similar way to non-boolean checkboxes and radio buttons).
Let’s re-build the type field as a dropdown select:
<select th:field="*{type}">
<option th:each="type : ${allTypes}"
th:value="${type}"
th:text="#{${'seedstarter.type.' + type}}">Wireframe</option>
</select>
At this point, understanding this piece of code is quite easy. Just notice how attribute precedence allows us to set the th:each
attribute in the <option>
tag itself.
7.6 Dynamic fields
Thanks to the advanced form-field binding capabilities in Spring MVC, we can use complex Spring EL expressions to bind dynamic form fields to our form-backing bean. This will allow us to create new Row objects in our SeedStarter
bean, and to add those rows’ fields to our form at user request.
In order to do this, we will need a couple of new mapped methods in our controller, which will add or remove a row from our SeedStarter
depending on the existence of specific request parameters:
@RequestMapping(value="/seedstartermng", params={"addRow"})
public String addRow(final SeedStarter seedStarter, final BindingResult bindingResult) {
seedStarter.getRows().add(new Row());
return "seedstartermng";
}
@RequestMapping(value="/seedstartermng", params={"removeRow"})
public String removeRow(
final SeedStarter seedStarter, final BindingResult bindingResult,
final HttpServletRequest req) {
final Integer rowId = Integer.valueOf(req.getParameter("removeRow"));
seedStarter.getRows().remove(rowId.intValue());
return "seedstartermng";
}
And now we can add a dynamic table to our form:
<table>
<thead>
<tr>
<th th:text="#{seedstarter.rows.head.rownum}">Row</th>
<th th:text="#{seedstarter.rows.head.variety}">Variety</th>
<th th:text="#{seedstarter.rows.head.seedsPerCell}">Seeds per cell</th>
<th>
<button type="submit" name="addRow" th:text="#{seedstarter.row.add}">Add row</button>
</th>
</tr>
</thead>
<tbody>
<tr th:each="row,rowStat : *{rows}">
<td th:text="${rowStat.count}">1</td>
<td>
<select th:field="*{rows[__${rowStat.index}__].variety}">
<option th:each="var : ${allVarieties}"
th:value="${var.id}"
th:text="${var.name}">Thymus Thymi</option>
</select>
</td>
<td>
<input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}" />
</td>
<td>
<button type="submit" name="removeRow"
th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button>
</td>
</tr>
</tbody>
</table>
Quite a lot of things to see here, but not much we should not understand by now… except for one strange
thing:
<select th:field="*{rows[__${rowStat.index}__].variety}">
...
</select>
If you recall from the “Using Thymeleaf” tutorial, that __${...}__
syntax is a preprocessing expression, which is an inner expression that is evaluated before actually evaluating the whole expression. But why that way of specifying the row index? Wouldn’t it be enough with:
<select th:field="*{rows[rowStat.index].variety}">
...
</select>
…well, actually, no. The problem is that Spring EL does not evaluate variables inside array index brackets, so when executing the above expression we would obtain an error telling us that rows[rowStat.index]
(instead of rows[0]
, rows[1]
, etc) is not a valid position in the rows collection. That’s why preprocessing is needed here.
Let’s have a look at a fragment of the resulting HTML after pressing “Add Row” a couple of times:
<tbody>
<tr>
<td>1</td>
<td>
<select id="rows0.variety" name="rows[0].variety">
<option selected="selected" value="1">Thymus vulgaris</option>
<option value="2">Thymus x citriodorus</option>
<option value="3">Thymus herba-barona</option>
<option value="4">Thymus pseudolaginosus</option>
<option value="5">Thymus serpyllum</option>
</select>
</td>
<td>
<input id="rows0.seedsPerCell" name="rows[0].seedsPerCell" type="text" value="" />
</td>
<td>
<button name="removeRow" type="submit" value="0">Remove row</button>
</td>
</tr>
<tr>
<td>2</td>
<td>
<select id="rows1.variety" name="rows[1].variety">
<option selected="selected" value="1">Thymus vulgaris</option>
<option value="2">Thymus x citriodorus</option>
<option value="3">Thymus herba-barona</option>
<option value="4">Thymus pseudolaginosus</option>
<option value="5">Thymus serpyllum</option>
</select>
</td>
<td>
<input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" />
</td>
<td>
<button name="removeRow" type="submit" value="1">Remove row</button>
</td>
</tr>
</tbody>
8 Validation and Error Messages
Most of our forms will need to show validation messages in order to inform the user of the errors he/she has made.
Thymeleaf offers some tools for this: a couple of functions in the #fields
object, the th:errors
and the th:errorclass
attributes.
8.1 Field errors
Let’s see how we could set a specific CSS class to a field if it has an error:
<input type="text" th:field="*{datePlanted}"
th:class="${#fields.hasErrors('datePlanted')}? fieldError" />
As you can see, the #fields.hasErrors(...)
function receives the field expression as a parameter (datePlanted
), and returns a boolean telling whether any validation errors exist for that field.
We could also obtain all the errors for that field and iterate them:
<ul>
<li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" />
</ul>
Instead of iterating, we could have also used th:errors
, a specialized attribute which builds a list with all the errors for the specified selector, separated by <br />
:
<input type="text" th:field="*{datePlanted}" />
<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
Simplifying error-based CSS styling: th:errorclass
The example we saw above, setting a CSS class to a form input if that field has errors, is so common that Thymeleaf offers a specific attribute for doing exacly that: th:errorclass
.
Applied to a form field tag (input, select, textarea…), it will read the name of the field to be examined from any existing name
or th:field
attributes in the same tag, and then append the specified CSS class to the tag if such field has any associated errors:
<input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" />
If datePlanted
has errors, this will render as:
<input type="text" id="datePlanted" name="datePlanted" value="2013-01-01" class="small fieldError" />
8.2 All errors
And what if we want to show all the errors in the form? We just need to query the #fields.hasErrors(...)
and #fields.errors(...)
methods with the '*'
or 'all'
constants (which are equivalent):
<ul th:if="${#fields.hasErrors('*')}">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}">Input is incorrect</li>
</ul>
As in the examples above, we could obtain all the errors and iterate them…
<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${err}" />
</ul>
…as well as build a <br />
-separated list:
<p th:if="${#fields.hasErrors('all')}" th:errors="*{all}">Incorrect date</p>
Finally. Note #fields.hasErrors('*')
is equivalent to #fields.hasAnyErrors()
and #fields.errors('*')
is equivalent to #fields.allErrors()
. Use whichever syntax you prefer:
<div th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p>
</div>
8.3 Global errors
There is a third type of error in a Spring form: global errors. These are errors that are not associated with any specific fields in the form, but still exist.
Thymeleaf offers the global
constant for accessing these errors:
<ul th:if="${#fields.hasErrors('global')}">
<li th:each="err : ${#fields.errors('global')}" th:text="${err}">Input is incorrect</li>
</ul>
<p th:if="${#fields.hasErrors('global')}" th:errors="*{global}">Incorrect date</p>
…as well as equivalent #fields.hasGlobalErrors()
and #fields.globalErrors()
convenience methods:
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p>
</div>
8.4 Displaying errors outside forms
Form validation errors can also be displayed outside forms by using variable (${...}
) instead of selection (*{...}
) expressions and prefixing the name of the form-backing bean:
<div th:errors="${myForm}">...</div>
<div th:errors="${myForm.date}">...</div>
<div th:errors="${myForm.*}">...</div>
<div th:if="${#fields.hasErrors('${myForm}')}">...</div>
<div th:if="${#fields.hasErrors('${myForm.date}')}">...</div>
<div th:if="${#fields.hasErrors('${myForm.*}')}">...</div>
<form th:object="${myForm}">
...
</form>
8.5 Rich error objects
Thymeleaf offers the possibility to obtain form error information in the form of beans (instead of mere strings), with the fieldName
(String), message
(String) and global
(boolean) attributes.
These errors can be obtained by means of the #fields.detailedErrors()
utility method:
<ul>
<li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr">
<span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> |
<span th:text="${e.message}">The error message</span>
</li>
</ul>
9 It’s still a Prototype!
Our application is ready now. But let’s have a second look at the .html
page we created…
One of the nicest consequences of working with Thymeleaf is that after all this functionality we have added to our HTML, we can still use it as a prototype (we say it is a Natural Template). Let’s open seedstartermng.html
directly in our browser without executing our application:
There it is! It’s not a working application, it’s not real data… but it is a perfectly valid prototype made up of perfectly displayable HTML code. Try to do that with JSP!
10 The Conversion Service
10.1 Configuration
As explained before, Thymeleaf can make use of a Conversion Service registered at the Application Context. Let’s see again what it looks like:
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
...
<mvc:annotation-driven conversion-service="conversionService" />
...
<!-- **************************************************************** -->
<!-- CONVERSION SERVICE -->
<!-- Standard Spring formatting-enabled implementation -->
<!-- **************************************************************** -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
<bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
</set>
</property>
</bean>
...
</beans>
10.1 Double-bracket syntax
The Conversion Service can be easily applied in order to convert/format any object into String. This is done by means of the double-bracket syntax:
- For variable expressions:
${{...}}
- For selection expressions:
*{{...}}
So, for example, given an Integer-to-String converter that adds commas as a thousands separator, this:
<p th:text="${val}">...</p>
<p th:text="${{val}}">...</p>
…should result in:
<p>1234567890</p>
<p>1,234,567,890</p>
10.2 Use in forms
We saw before that every th:field
attribute will always apply the conversion service, so this:
<input type="text" th:field="*{datePlanted}" />
…is actually equivalent to:
<input type="text" th:field="*{{datePlanted}}" />
Note that this is the only scenario in which the Conversion Service is applied in expressions using single-bracket syntax.
10.3 #conversions
utility object
The #conversions
expression utility object allows the manual execution of the Conversion Service wherever needed:
<p th:text="${'Val: ' + #conversions.convert(val,'String')}">...</p>
Syntax for this utility object:
#conversions.convert(Object,Class)
: converts the object to the specified class.#conversions.convert(Object,String)
: same as above, but specifying the target class as a String (note thejava.lang.
package can be ommitted).
11 Rendering Template Fragments
Thymeleaf offers the possibility to render only part of a template as the result of its execution: a fragment.
This can be a useful componentization tool. For example, it can be used at controllers that execute on AJAX calls, which might return markup fragments of a page that is already loaded at the browser (for updating a select, enabling/disabling buttons…).
Fragmentary rendering can be achieved by using Thymeleaf’s fragment specs: objects implementing the org.thymeleaf.fragment.IFragmentSpec
interface.
The most common of these implementations is org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec
, which allows specifying a fragment using a DOM Selector exactly like the ones used at th:include
or th:replace
.
11.1 Specifying fragments in view beans
View beans are beans of the org.thymeleaf.spring4.view.ThymeleafView
class declared at the application context. They allow the specification of fragments like this:
<bean name="content-part" class="org.thymeleaf.spring4.view.ThymeleafView">
<property name="templateName" value="index" />
<property name="fragmentSpec">
<bean class="org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec"
c:selectorExpression="content" />
</property>
</bean>
Given the above bean definition, if our controller returns content-part
(the name of the above bean)…
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "content-part";
}
…thymeleaf will return only the content
fragment of the index
template – which location will probably be something like /WEB-INF/templates/index.html
, once prefix and suffix are applied:
<!DOCTYPE html>
<html>
...
<body>
...
<div th:fragment="content">
Only this will be rendered!!
</div>
...
</body>
</html>
Note also that, thanks to the power of Thymeleaf DOM Selectors, we could select a fragment in a template without needing any th:fragment
attributes at all. Let’s use the id
attribute, for example:
<bean name="content-part" class="org.thymeleaf.spring4.view.ThymeleafView">
<property name="fragmentSpec">
<bean class="org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec"
c:selectorExpression="#content" />
</property>
<property name="templateName" value="index" />
</bean>
…which will perfectly select:
<!DOCTYPE html>
<html>
...
<body>
...
<div id="content">
Only this will be rendered!!
</div>
...
</body>
</html>
11.2 Specifying fragments in controller return values
Instead of declaring view beans, fragments can be specified from the controllers themselves by using the same syntax as in th:include
or th:replace
attributes:
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "index :: content";
}
Of course, again the full power of DOM Selectors is available, so we could select our fragment based on standard HTML attributes, like id="content"
:
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "index :: #content";
}
And we can also use parameters, like:
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "index :: #content ('myvalue')";
}
12 Advanced Integration Features
12.1 Integration with RequestDataValueProcessor
Thymeleaf now seamlessly integrates with Spring’s RequestDataValueProcessor
interface. This interface allows the interception of link URLs, form URLs and form field values before they are written to the markup result, as well as transparently adding hidden form fields that enable security features like e.g. protection agains CSRF (Cross-Site Request Forgery).
An implementation of RequestDataValueProcessor
can be easily configured at the Application Context:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
...
<bean name="requestDataValueProcessor"
class="net.example.requestdata.processor.MyRequestDataValueProcessor" />
</beans>
…and Thymeleaf uses it this way:
-
th:href
andth:src
callRequestDataValueProcessor.processUrl(...)
before rendering the URL. -
th:action
callsRequestDataValueProcessor.processAction(...)
before rendering the form’saction
attribute, and additionally it detects when this attribute is being applied on a<form>
tag —which should be the only place, anyway—, and in such case callsRequestDataValueProcessor.getExtraHiddenFields(...)
and adds the returned hidden fields just before the closing</form>
tag. -
th:value
callsRequestDataValueProcessor.processFormFieldValue(...)
for rendering the value it refers to, unless there is ath:field
present in the same tag (in which caseth:field
will take care). -
th:field
callsRequestDataValueProcessor.processFormFieldValue(...)
for rendering the value of the field it applies to (or the tag body if it is a<textarea>
).
Note this feature will only be available for Spring versions 3.1 and newer.
12.1 Building URIs to controllers
Since version 4.1, Spring allows the possibility to build links to annotated controllers directly from views, without the need to know the URIs these controllers are mapped to.
In Thymeleaf, this can be achieved by means of the #mvc.url(...)
expression method, which allows the specification of controller methods by the capital letters of the controller class they are in, followed by the name of the method itself. This is equivalent to JSP’s spring:mvcUrl(...)
custom function.
For example, for:
public class ExampleController {
@RequestMapping("/data")
public String getData(Model model) { ... return "template" }
@RequestMapping("/data")
public String getDataParam(@RequestParam String type) { ... return "template" }
}
The following code will create a link to it:
<a th:href="${(#mvc.url('EC#getData')).build()}">Get Data Param</a>
<a th:href="${(#mvc.url('EC#getDataParam').arg(0,'internal')).build()}">Get Data Param</a>
You can read more about this mechanism at http://docs.spring.io/spring-framework/docs/4.1.2.RELEASE/spring-framework-reference/html/mvc.html#mvc-links-to-controllers-from-views
13 Spring WebFlow integration
13.1 Basic configuration
The thymeleaf-spring4
integration package includes integration with Spring WebFlow 2.3.x.
WebFlow includes some AJAX capabilities for rendering fragments of the displayed page when specific events (transitions) are triggered, and in order to enable Thymeleaf to attend these AJAX requests, we will have to use a different ViewResolver
implementation, configured like this:
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring4.view.AjaxThymeleafViewResolver">
<property name="viewClass" value="org.thymeleaf.spring4.view.FlowAjaxThymeleafView" />
<property name="templateEngine" ref="templateEngine" />
</bean>
…and then this ViewResolver
can be configured at your WebFlow ViewFactoryCreator
like:
<bean id="mvcViewFactoryCreator"
class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
<property name="viewResolvers" ref="thymeleafViewResolver"/>
</bean>
From here on, you can specify Thymeleaf templates in your view-state’s:
<view-state id="detail" view="bookingDetail">
...
</view-state>
In the above example, bookingDetail
is a Thymeleaf template specified in the usual way, understandable by any of the Template Resolvers configured at the TemplateEngine
.
13.2 Ajax fragments
WebFlow allows the specification of fragments to be rendered via AJAX with <render>
tags, like this:
<view-state id="detail" view="bookingDetail">
<transition on="updateData">
<render fragments="hoteldata"/>
</transition>
</view-state>
These fragments (hoteldata
, in this case) can be a comma-separated list of fragments specified at the markup with th:fragment
:
<div id="data" th:fragment="hoteldata">
This is a content to be changed
</div>
Always remember that the specified fragments must have an id
attribute, so that the Spring JavaScript libraries running on the browser are capable of substituting the markup.
<render>
tags can also be specified using DOM selectors:
<view-state id="detail" view="bookingDetail">
<transition on="updateData">
<render fragments="[//div[@id='data']]"/>
</transition>
</view-state>
…and this will mean no th:fragment
is needed:
<div id="data">
This is a content to be changed
</div>
As for the code that triggers the updateData
transition, it looks like:
<script type="text/javascript" th:src="@{/resources/dojo/dojo.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring-Dojo.js}"></script>
...
<form id="triggerform" method="post" action="">
<input type="submit" id="doUpdate" name="_eventId_updateData" value="Update now!" />
</form>
<script type="text/javascript">
Spring.addDecoration(
new Spring.AjaxEventDecoration({formId:'triggerform',elementId:'doUpdate',event:'onclick'}));
</script>
Этот урок освещает процесс создания простого web-приложения с ресурсами, которые защищены Spring Security.
Что вы создадите
Вы создадите Spring MVC приложение, которое обеспечивает защиту страницы входа фиксированным списком пользователей.
Что вам потребуется
- Примерно 15 минут свободного времени
- Любимый текстовый редактор или IDE
- JDK 6 и выше
-
Gradle 1.11+ или
Maven 3.0+ - Вы также можете импортировать код этого урока, а также просматривать web-страницы прямо из
Spring Tool Suite (STS),
собственно как и работать дальше из него.
Как проходить этот урок
Как и большинство уроков по Spring, вы можете начать с
нуля и выполнять каждый шаг, либо пропустить базовые шаги, которые вам уже знакомы. В любом случае,
вы в конечном итоге получите рабочий код.
Чтобы начать с нуля, перейдите в Настройка проекта.
Чтобы пропустить базовые шаги, выполните следующее:
- Загрузите и
распакуйте архив с кодом этого урока, либо кнонируйте из репозитория с помощью
Git:
git clone https://github.com/spring-guides/gs-securing-web.git
- Перейдите в каталог
gs-securing-web/initial
- Забегая вперед, настройте Spring Security
Когда вы закончите, можете сравнить получившийся результат с образцом в gs-securing-web/complete
.
Настройка проекта
Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки,
которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с
Gradle и
Maven. Если вы не знакомы ни с одним из них, ознакомьтесь
с соответсвующими уроками Сборка Java-проекта с использованием Gradle
или Сборка Java-проекта с использованием Maven.
Создание структуры каталогов
В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру,
командой mkdir -p src/main/java/hello
для *nix систем:
└── src └── main └── java └── hello
Создание файла сборки Gradle
Ниже представлен начальный файл сборки Gradle.
Файл pom.xml находится здесь.
Если вы используете Spring Tool Suite (STS),
то можете импортировать урок прямо из него.
Если вы посмотрите на pom.xml
, вы найдете, что указана версия для maven-compiler-plugin.
В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы,
которая по умолчанию имеет старую(до Java 5) версию этого плагина.
build.gradle
buildscript {
repositories {
maven { url "http://repo.spring.io/libs-release" }
mavenLocal()
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-securing-web'
version = '0.1.0'
}
repositories {
mavenLocal()
mavenCentral()
maven { url "http://repo.spring.io/libs-release" }
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
testCompile("junit:junit")
}
task wrapper(type: Wrapper) {
gradleVersion = '1.11'
}
Spring Boot gradle plugin
предоставляет множество удобных возможностей:
-
Он собирает все jar’ы в classpath и собирает единое, исполняемое «über-jar», что
делает более удобным выполнение и доставку вашего сервиса - Он ищет
public static void main()
метод, как признак исполняемого класса -
Он предоставляет встроенное разрешение зависимостей, с определенными номерами версий для соответсвующих
Spring Boot зависимостей.
Вы можете переопределить на любые версии, какие захотите, но он будет по умолчанию для Boot
выбранным набором версий
Создание незащищенного web приложения
До того, как применить защиту к вашему приложению, вам необходимо само приложение.
Шаги в этой главе освещают процесс создания очень простого web-приложения. Затем в следующей главе
вы защитите его с помощью Spring Security.
Приложение включает два простых представления: домашнюю страницу и «Hello World» страницу. Домашняя
страница предствлена ниже как Thymeleaf шаблон:
src/main/resources/templates/home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
Как вы можете увидеть, это простое приложение включает ссылку на страницу «/hello»,
которая представлена ниже также как Thymeleaf шаблон.
src/main/resources/templates/hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
Приложение основано на Spring MVC. Т.о. вам необходимо настроить Spring MVC и контроллеры
представлений для отображения этих шаблонов. Ниже конфигурационный класс для настройки Spring
MVC в приложении.
src/main/java/hello/MvcConfig.java
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
Метод addViewControllers()
(переопределение метода с таким же названием в
WebMvcConfigurerAdapter
), добавляющий четыре контроллера. Двое из них настроены
на представление с именем «home»(home.html
), другой настроен на «hello».
Четвертый контроллер настроен на представление с названием «login». Вы создадите это представление
в следующей главе.
На данном этапе вы можете перейти к созданию приложения исполняемым
и запустить приложени без входа в систему.
Фундамент простого web-приложения создано, теперь вы можете добавить защиту к нему.
Настройка Spring Security
Предположим, что вы хотите предотвратить неавторизованный доступ к просмотру представления
«/hello». Сейчас, если пользователь нажмет на ссылку на домашней странице, он увидит приветствие
без каких либо помех к нему. Вам необходимо добавить барьер, который заставляет пользователя
войти в систему до того, как он увидит страницу.
Для этого вы настраиваете Spring Security в приложении. Если Spring Security в classpath’е,
Spring Boot автоматически обеспечивает защиту для всех HTTP путей
с «базовой» аутентификацией. Но вы сможете в дальнейшем изменить под себя настройки безопасности.
Первое, что вам нужно сделать, это добавить Spring Security в classpath.
C Gradle это достигается одной строчкой в блоке dependencies
:
build.gradle
dependencies {
...
compile("org.springframework.boot:spring-boot-starter-security")
...
}
C Maven — добавлением дополнительного блока в <dependencies>
:
pom.xml
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
Ниже приведена настройка безопасности, которая гарантирует, что только авторизованные
пользователи могут увидеть секретное приветствие:
src/main/java/hello/WebSecurityConfig.java
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated();
http
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Configuration
protected static class AuthenticationConfiguration extends
GlobalAuthenticationConfigurerAdapter {
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
}
Класс WebSecurityConfig
содержит аннотацию @EnableWebMvcSecurity
для включения поддержки безопасности Spring Security и Spring MVC интеграцию. Он также
расширяет WebSecurityConfigurerAdapter
и переопределяет пару методов для
установки некоторых настроек безопасности.
Метод configure(HttpSecurity)
определяет, какие URL пути должны быть защищены,
а какие нет. В частности, «/» и «/home» настроены без требования к авторизации. Ко всем
остальным путям должна быть произведена аутентификация.
Когда пользователь успешно войдет в систему, он будет перенаправлен на предыдущую запрашиваемую
страницу, требующую авторизацию. Здесь вы определили собственную «/login» страницу в loginPage()
и каждый имеет доступ к ней.
Что касается метода configure(AuthenticationManagerBuilder)
, то он создает
в памяти хранилище пользователей с единственным пользователем. Этому пользователю дано
имя «user», пароль «password» и роль «ROLE».
Теперь нам нужно создать страницу входа. Т.к. уже есть сопоставление контроллера с представлением
«login», то вам нужно только создать это представления входа:
src/main/resources/templates/login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
Как видите, этот Thymeleaf шаблон просто представляет собой форму, которая собирает значения
имени пользователя и пароля и отправляет их к «/login». Рассмотрим, как настроен Spring Security
фильтр, который перхватывает запрос и идентифицирует пользователя. Если пользователь не прошел
проверку, то он будет перенаправлен на страницу «/login?error» и наша страница отобразит соответсвующее
сообщение об ошибке. При удачной авторизации, приложение отправит к «/login?logout» и наша
страница отобразит соответственное сообщение об успехе.
Последнее, что нам нужно, это предоставить пользователю способ для отображения текущего имени и
выхода из системы. Обновим hello.html
, чтобы сказать привет текущему пользователю и
добавить в форму «Sign Out», как показано ниже:
src/main/resources/templates/hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
Мы отображаем имя пользователя с использованием интеграции Spring Security с
HttpServletRequest#getRemoteUser()
. По кнопке «Sign Out» форма отправляет
POST запрос к «/logout». При успешном выходе пользователь будет перенаправлен к
«/login?logout».
Создание приложения исполняемым
Несмотря на то, что пакет этого сервиса может быть в составе web-приложения и
WAR файлов,
более простой подход, продемонстрированный ниже создает отдельное самостоятельное приложение.
Вы упаковываете все в единый, исполняемый JAR-файл, который запускается через хорошо знакомый
старый main()
Java-метод. Попутно, вы используете поддержку Spring для встроенного
Tomcat
контейнера сервлетов как HTTP среду выполнения вместо развертывания на сторонний экземпляр.
src/main/java/hello/Application.java
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
main()
метод передает управление вспомогательному классу
SpringApplication,
где Application.class
— аргумент его run()
метода. Это сообщает Spring
о чтении метаданных аннотации из Application
и управлении ею как компонента в
Spring application context.
Аннотация @ComponentScan
говорит Spring’у рекурсивно искать в пакете hello
и его потомках классы, помеченные прямо или косвенно Spring аннотацией
@Component.
Эта директива гарантирует, что Spring найдет и зарегистрирует WebConfig
и WebSecurityConfig
,
потому что он отмечен @Configuration
, который, в свою очередь, является своего рода
@Component
аннотацией. По факту, эти конфигурационные классы также используются для настройки Spring.
Аннотация @EnableAutoConfiguration
переключает на приемлемое по умолчанию поведение, основанное на содержании вашего classpath. К примеру,
т.к. приложение зависит от встраиваемой версии Tomcat (tomcat-embed-core.jar), Tomcat сервер установлен
и сконфигурирован на приемлемое по умолчанию поведение от вашего имени. И т.к. приложение также
зависит от Spring MVC (spring-webmvc.jar), Spring MVC
DispatcherServlet
сконфигурирован и зарегестрирован для вас — web.xml
не нужен! Автоконфигурация полезный и
гибкий механизм. Подробную информацию смотрите в
API документации.
Сборка исполняемого JAR
Вы можете собрать единый исполняемый JAR-файл, который содержит все необходимые зависимости,
классы и ресурсы. Это делает его легким в загрузке, версионировании и развертывании сервиса как
приложения на протяжении всего периода разработки, на различных средах и так далее.
./gradlew build
Затем вы можете запустить JAR-файл:
java -jar build/libs/gs-securing-web-0.1.0.jar
Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run
,
либо вы можете собрать приложение с mvn clean package
и запустить JAR примерно так:
java -jar target/gs-securing-web-0.1.0.jar
Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:
./gradlew clean build && java -jar build/libs/gs-securing-web-0.1.0.jar
Если вы используете Maven, то можете запустить ваш сервис таким образом:
mvn clean package && java -jar target/gs-securing-web-0.1.0.jar
.
Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:
./gradlew bootRun
С mvn — mvn spring-boot:run
.
... app starts up ...
как только приложение запустится, откройте в вашем браузере адрес
http://localhost:8080.
Вы должны увидеть домашнюю страницу:
Когда вы нажмете на ссылку, то она попытается открыть вам сстраницу приветствия
/hello
. Но т.к. эта страница защищена и вы ещё не вошли в систему,
вы увидите страницу входа:
Если вы перешли на сюда с главы по незащищенной версии, то вы не увидите этой
страницы входа. Вы смело можете вернуться обратно и дописать остальную часть
кода, связанную с безопасностью.
На странице входа войдите под тестовым пользователем, введя «user» и «password»
в соответствующие поля. Как только вы отправите форму, вы авторизуетесь и увидите
страницу приветствия:
Если вы нажмете на кнопку «Sign Out», ваша авторизация отменится и вы вернетесь к
странице входа с сообщением о том, что вы вышли из системы.
Итог
Поздравляем! Вы только что разработали простое web-приложение, которое защищено Spring Security.
С оригинальным текстом урока вы можете ознакомиться на
spring.io.
comments powered by
Error in user YAML: (<unknown>): found character that cannot start any token while scanning for the next token at line 3 column 10
--- title: 'Tutorial: Thymeleaf + Spring' author: The Thymeleaf Team version: @documentVersion@ thymeleafVersion: @projectVersion@ ---
Preface
This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC.
Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two
separate libraries called thymeleaf-spring3
and thymeleaf-spring4
. These libraries are packaged in separate
.jar
files (thymeleaf-spring3-{version}.jar
and thymeleaf-spring4-{version}.jar
) and need to be added to your
classpath in order to use Thymeleaf’s Spring integrations in your application.
The code samples and example application in this tutorial make use of Spring 4.x and its corresponding
Thymeleaf integrations, but the contents of this text are valid also for Spring 3.x. If your application uses
Spring 3.x, all you have to do is replace the org.thymeleaf.spring4
package with org.thymeleaf.spring3
in the
code samples.
1 Integrating Thymeleaf with Spring
Thymeleaf offers a set of Spring integrations that allow you to use it as a
fully-featured substitute for JSP in Spring MVC applications.
These integrations will allow you to:
- Make the mapped methods in your Spring MVC
@Controller
objects forward to
templates managed by Thymeleaf, exactly like you do with JSPs. - Use Spring Expression Language (Spring EL) instead of OGNL in your
templates. - Create forms in your templates that are completely integrated with your
form-backing beans and result bindings, including the use of property editors, conversion services and validation error handling. - Display internationalization messages from message files managed by Spring
(through the usualMessageSource
objects). - Resolve your templates using Spring’s own resource resolution mechanisms.
Note that in order to fully understand this tutorial, you should have first gone
through the «Using Thymeleaf» tutorial, which explains the Standard Dialect in depth.
2 The SpringStandard Dialect
In order to achieve an easier and better integration, Thymeleaf provides a
dialect which specifically implements all the needed features for it to work
correctly with Spring.
This specific dialect is based on the Thymeleaf Standard Dialect and is
implemented in a class called org.thymeleaf.spring4.dialect.SpringStandardDialect
,
which in fact extends from org.thymeleaf.standard.StandardDialect
.
Besides all the features already present in the Standard Dialect —and therefore
inherited—, the SpringStandard Dialect introduces the following specific
features:
- Use Spring Expression Language (Spring EL or SpEL) as a variable expression
language, instead of OGNL. Consequently, all${...}
and*{...}
expressions will be evaluated by Spring’s Expression Language engine. Note also
that support for the Spring EL compiler is available (Spring 4.2.4+). - Access any beans in your application context using SpringEL’s syntax:
${@myBean.doSomething()}
- New attributes for form processing:
th:field
,th:errors
andth:errorclass
, besides a new
implementation ofth:object
that allows it to be used for form command
selection. - An expression object and method,
#themes.code(...)
, which is equivalent
to thespring:theme
JSP custom tag. - An expression object and method,
#mvc.uri(...)
, which is equivalent to
thespring:mvcUrl(...)
JSP custom function (only in Spring 4.1+).
Note that most of the times you shouldn’t be using this dialect directly in a normal
TemplateEngine
object as a part of its configuration. Unless you have very specific
Spring integration needs, you should instead be creating an instance of a new template
engine class that performs all the required configuration steps automatically:
org.thymeleaf.spring4.SpringTemplateEngine
.
An example bean configuration:
@Bean public SpringResourceTemplateResolver templateResolver(){ // SpringResourceTemplateResolver automatically integrates with Spring's own // resource resolution infrastructure, which is highly recommended. SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); templateResolver.setApplicationContext(this.applicationContext); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); // HTML is the default value, added here for the sake of clarity. templateResolver.setTemplateMode(TemplateMode.HTML); // Template cache is true by default. Set to false if you want // templates to be automatically updated when modified. templateResolver.setCacheable(true); return templateResolver; } @Bean public SpringTemplateEngine templateEngine(){ // SpringTemplateEngine automatically applies SpringStandardDialect and // enables Spring's own MessageSource message resolution mechanisms. SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); // Enabling the SpringEL compiler with Spring 4.2.4 or newer can // speed up execution in most scenarios, but might be incompatible // with specific cases when expressions in one template are reused // across different data types, so this flag is "false" by default // for safer backwards compatibility. templateEngine.setEnableSpringELCompiler(true); return templateEngine; }
Or, using Spring’s XML-based configuration:
<!-- SpringResourceTemplateResolver automatically integrates with Spring's own --> <!-- resource resolution infrastructure, which is highly recommended. --> <bean id="templateResolver" class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver"> <property name="prefix" value="/WEB-INF/templates/" /> <property name="suffix" value=".html" /> <!-- HTML is the default value, added here for the sake of clarity. --> <property name="templateMode" value="HTML" /> <!-- Template cache is true by default. Set to false if you want --> <!-- templates to be automatically updated when modified. --> <property name="cacheable" value="true" /> </bean> <!-- SpringTemplateEngine automatically applies SpringStandardDialect and --> <!-- enables Spring's own MessageSource message resolution mechanisms. --> <bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine"> <property name="templateResolver" ref="templateResolver" /> <!-- Enabling the SpringEL compiler with Spring 4.2.4 or newer can speed up --> <!-- execution in most scenarios, but might be incompatible with specific --> <!-- cases when expressions in one template are reused across different data --> <!-- ypes, so this flag is "false" by default for safer backwards --> <!-- compatibility. --> <property name="enableSpringELCompiler" value="true" /> </bean>
3 Views and View Resolvers
3.1 Views and View Resolvers in Spring MVC
There are two interfaces in Spring MVC that conform the core of its templating
system:
org.springframework.web.servlet.View
org.springframework.web.servlet.ViewResolver
Views model pages in our applications and allow us to modify and predefine their
behaviour by defining them as beans. Views are in charge of rendering the actual
HTML interface, usually by the execution of some template engine like Thymeleaf.
ViewResolvers are the objects in charge of obtaining View objects for a specific
operation and locale. Typically, controllers ask ViewResolvers to forward to a
view with a specific name (a String returned by the controller method), and then
all the view resolvers in the application execute in ordered chain until one of
them is able to resolve that view, in which case a View object is returned and
control is passed to it for the renderization of HTML.
Note that not all pages in our applications have to be defined as Views, but
only those which behaviour we wish to be non-standard or configured in a
specific way (for example, by wiring some special beans to it). If a
ViewResolver is asked a view that has no corresponding bean —which is the
common case—, a new View object is created ad hoc and returned.
A typical configuration for a JSP+JSTL ViewResolver in a Spring MVC application
from the past looked like this:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/WEB-INF/jsps/" /> <property name="suffix" value=".jsp" /> <property name="order" value="2" /> <property name="viewNames" value="*jsp" /> </bean>
A quick look at its properties is enough to know about how it was configured:
viewClass
establishes the class of the View instances. This is needed for a
JSP resolver, but it will not be needed at all when we’re working with Thymeleaf.prefix
andsuffix
work in a similar way to the attributes of the same
names in Thymeleaf’s TemplateResolver objects.order
establishes the order in which the ViewResolver will be queried in
the chain.viewNames
allows the definition (with wildcards) of the view names that
will be resolved by this ViewResolver.
3.2 Views and View Resolvers in Thymeleaf
Thymeleaf offers implementations for the two interfaces mentioned above:
org.thymeleaf.spring4.view.ThymeleafView
org.thymeleaf.spring4.view.ThymeleafViewResolver
These two classes will be in charge of processing Thymeleaf templates as a
result of the execution of controllers.
Configuration of the Thymeleaf View Resolver is very similar to that of JSP:
@Bean public ThymeleafViewResolver viewResolver(){ ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine()); // NOTE 'order' and 'viewNames' are optional viewResolver.setOrder(1); viewResolver.setViewNames(new String[] {".html", ".xhtml"}); return viewResolver; }
…or in XML:
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <!-- NOTE 'order' and 'viewNames' are optional --> <property name="order" value="1" /> <property name="viewNames" value="*.html,*.xhtml" /> </bean>
The templateEngine
parameter is, of course, the SpringTemplateEngine
object
we defined in the previous chapter. The other two (order
and viewNames
) are
both optional, and have the same meaning as in the JSP ViewResolver we saw
before.
Note that we do not need prefix
or suffix
parameters, because these are
already specified at the Template Resolver (which in turn is passed to the
Template Engine).
And what if we wanted to define a View
bean and add some static variables to
it? Easy, just define a prototype bean for it:
@Bean @Scope("prototype") public ThymeleafView mainView() { ThymeleafView view = new ThymeleafView("main"); // templateName = 'main' view.setStaticVariables( Collections.singletonMap("footer", "The ACME Fruit Company")); return view; }
By doing this, you will be able to execute specifically this view bean
selecting it by bean name (mainView
, in this case).
4 Spring Thyme Seed Starter Manager
The source code for the examples shown in this and future chapters of this guide
can be found in the Spring Thyme Seed Starter Manager GitHub repository.
4.1 The Concept
At Thymeleaf we’re huge fans of thyme, and every spring we prepare our seed
starting kits with good soil and our favourite seeds, place them under the
Spanish sun and patiently wait for our new plants to grow.
But this year we got fed up with sticking labels to the seed starter containers
for knowing which seed was in each cell of the container, so we decided to
prepare an application using Spring MVC and Thymeleaf to help us catalogue our
starters: The Spring Thyme SeedStarter Manager.
In a similar way to the Good Thymes Virtual Grocery application we developed in
the Using Thymeleaf tutorial, the STSM will allow us to exemplify the most
important aspects of the integration of Thymeleaf as a template engine for
Spring MVC.
4.2 Business Layer
We will need a very simple business layer for our application. First of all,
let’s have a look at our model entities:
A couple of very simple service classes will provide the required business
methods. Like:
@Service public class SeedStarterService { @Autowired private SeedStarterRepository seedstarterRepository; public List<SeedStarter> findAll() { return this.seedstarterRepository.findAll(); } public void add(final SeedStarter seedStarter) { this.seedstarterRepository.add(seedStarter); } }
And:
@Service public class VarietyService { @Autowired private VarietyRepository varietyRepository; public List<Variety> findAll() { return this.varietyRepository.findAll(); } public Variety findById(final Integer id) { return this.varietyRepository.findById(id); } }
4.3 Spring MVC configuration
Next we need to set up the Spring MVC configuration for the application, which
will include not only the standard Spring MVC artifacts like resource handling
or annotation scanning, but also the creation of the Template Engine and View
Resolver instances.
@Configuration @EnableWebMvc @ComponentScan public class SpringWebConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware { private ApplicationContext applicationContext; public SpringWebConfig() { super(); } public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /* ******************************************************************* */ /* GENERAL CONFIGURATION ARTIFACTS */ /* Static Resources, i18n Messages, Formatters (Conversion Service) */ /* ******************************************************************* */ @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { super.addResourceHandlers(registry); registry.addResourceHandler("/images/**").addResourceLocations("/images/"); registry.addResourceHandler("/css/**").addResourceLocations("/css/"); registry.addResourceHandler("/js/**").addResourceLocations("/js/"); } @Bean public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("Messages"); return messageSource; } @Override public void addFormatters(final FormatterRegistry registry) { super.addFormatters(registry); registry.addFormatter(varietyFormatter()); registry.addFormatter(dateFormatter()); } @Bean public VarietyFormatter varietyFormatter() { return new VarietyFormatter(); } @Bean public DateFormatter dateFormatter() { return new DateFormatter(); } /* **************************************************************** */ /* THYMELEAF-SPECIFIC ARTIFACTS */ /* TemplateResolver <- TemplateEngine <- ViewResolver */ /* **************************************************************** */ @Bean public SpringResourceTemplateResolver templateResolver(){ // SpringResourceTemplateResolver automatically integrates with Spring's own // resource resolution infrastructure, which is highly recommended. SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); templateResolver.setApplicationContext(this.applicationContext); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); // HTML is the default value, added here for the sake of clarity. templateResolver.setTemplateMode(TemplateMode.HTML); // Template cache is true by default. Set to false if you want // templates to be automatically updated when modified. templateResolver.setCacheable(true); return templateResolver; } @Bean public SpringTemplateEngine templateEngine(){ // SpringTemplateEngine automatically applies SpringStandardDialect and // enables Spring's own MessageSource message resolution mechanisms. SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); // Enabling the SpringEL compiler with Spring 4.2.4 or newer can // speed up execution in most scenarios, but might be incompatible // with specific cases when expressions in one template are reused // across different data types, so this flag is "false" by default // for safer backwards compatibility. templateEngine.setEnableSpringELCompiler(true); return templateEngine; } @Bean public ThymeleafViewResolver viewResolver(){ ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine()); return viewResolver; } }
4.4 The Controller
Of course, we will also need a controller for our application. As the STSM will
only contain one web page with a list of seed starters and a form for adding new
ones, we will write only one controller class for all the server interactions:
@Controller public class SeedStarterMngController { @Autowired private VarietyService varietyService; @Autowired private SeedStarterService seedStarterService; ... }
Now let’s see what we can add to this controller class.
Model Attributes
First we will add some model attributes that we will need in the page:
@ModelAttribute("allTypes") public List<Type> populateTypes() { return Arrays.asList(Type.ALL); } @ModelAttribute("allFeatures") public List<Feature> populateFeatures() { return Arrays.asList(Feature.ALL); } @ModelAttribute("allVarieties") public List<Variety> populateVarieties() { return this.varietyService.findAll(); } @ModelAttribute("allSeedStarters") public List<SeedStarter> populateSeedStarters() { return this.seedStarterService.findAll(); }
Mapped methods
And now the most important part of a controller, the mapped methods: one for
showing the form page, and another one for processing the addition of new SeedStarter
objects.
@RequestMapping({"/","/seedstartermng"}) public String showSeedstarters(final SeedStarter seedStarter) { seedStarter.setDatePlanted(Calendar.getInstance().getTime()); return "seedstartermng"; } @RequestMapping(value="/seedstartermng", params={"save"}) public String saveSeedstarter( final SeedStarter seedStarter, final BindingResult bindingResult, final ModelMap model) { if (bindingResult.hasErrors()) { return "seedstartermng"; } this.seedStarterService.add(seedStarter); model.clear(); return "redirect:/seedstartermng"; }
4.5 Configuring a Conversion Service
In order to allow easy formatting of Date
and also Variety
objects in our view layer, we
configured our application so that a Spring ConversionService
object was created and
initialized (by the WebMvcConfigurerAdapter
we extend) with a couple of formatter
objects we will need. See it again:
@Override public void addFormatters(final FormatterRegistry registry) { super.addFormatters(registry); registry.addFormatter(varietyFormatter()); registry.addFormatter(dateFormatter()); } @Bean public VarietyFormatter varietyFormatter() { return new VarietyFormatter(); } @Bean public DateFormatter dateFormatter() { return new DateFormatter(); }
Spring formatters are implementations of the org.springframework.format.Formatter
interface. For more information on how the Spring conversion infrastructure works,
see the docs at spring.io.
Let’s have a look at the DateFormatter
, which formats dates according to a format
string present at the date.format
message key of our Messages.properties
:
public class DateFormatter implements Formatter<Date> { @Autowired private MessageSource messageSource; public DateFormatter() { super(); } public Date parse(final String text, final Locale locale) throws ParseException { final SimpleDateFormat dateFormat = createDateFormat(locale); return dateFormat.parse(text); } public String print(final Date object, final Locale locale) { final SimpleDateFormat dateFormat = createDateFormat(locale); return dateFormat.format(object); } private SimpleDateFormat createDateFormat(final Locale locale) { final String format = this.messageSource.getMessage("date.format", null, locale); final SimpleDateFormat dateFormat = new SimpleDateFormat(format); dateFormat.setLenient(false); return dateFormat; } }
The VarietyFormatter
automatically converts between our Variety
entities and the way we want to use them in our forms (basically, by their id
field values):
public class VarietyFormatter implements Formatter<Variety> { @Autowired private VarietyService varietyService; public VarietyFormatter() { super(); } public Variety parse(final String text, final Locale locale) throws ParseException { final Integer varietyId = Integer.valueOf(text); return this.varietyService.findById(varietyId); } public String print(final Variety object, final Locale locale) { return (object != null ? object.getId().toString() : ""); } }
We will learn more on how these formatters affect the way our data is displayed later on.
5 Listing Seed Starter Data
The first thing that our /WEB-INF/templates/seedstartermng.html
page will show
is a listing with the seed starters currently stored. For this we will need some
externalized messages and also some expression evaluation on model attributes.
Like this:
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}"> <h2 th:text="#{title.list}">List of Seed Starters</h2> <table> <thead> <tr> <th th:text="#{seedstarter.datePlanted}">Date Planted</th> <th th:text="#{seedstarter.covered}">Covered</th> <th th:text="#{seedstarter.type}">Type</th> <th th:text="#{seedstarter.features}">Features</th> <th th:text="#{seedstarter.rows}">Rows</th> </tr> </thead> <tbody> <tr th:each="sb : ${allSeedStarters}"> <td th:text="${{sb.datePlanted}}">13/01/2011</td> <td th:text="#{|bool.${sb.covered}|}">yes</td> <td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td> <td th:text="${#strings.arrayJoin( #messages.arrayMsg( #strings.arrayPrepend(sb.features,'seedstarter.feature.')), ', ')}">Electric Heating, Turf</td> <td> <table> <tbody> <tr th:each="row,rowStat : ${sb.rows}"> <td th:text="${rowStat.count}">1</td> <td th:text="${row.variety.name}">Thymus Thymi</td> <td th:text="${row.seedsPerCell}">12</td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div>
Lots to see here. Let’s have a look at each fragment separately.
First of all, this section will only be shown if there are any seed starters. We
achieve that with a th:unless attribute and the #lists.isEmpty(...)
function.
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
Note that all utility objects like #lists
are available in Spring EL
expressions just as they were in OGNL expressions in the Standard Dialect.
The next thing to see is a lot of internationalized (externalized) texts, like:
<h2 th:text="#{title.list}">List of Seed Starters</h2> <table> <thead> <tr> <th th:text="#{seedstarter.datePlanted}">Date Planted</th> <th th:text="#{seedstarter.covered}">Covered</th> <th th:text="#{seedstarter.type}">Type</th> <th th:text="#{seedstarter.features}">Features</th> <th th:text="#{seedstarter.rows}">Rows</th> ...
This being a Spring MVC application, we already defined a MessageSource
bean
in our Spring configuration (MessageSource
objects are the standard way of
managing externalized texts in Spring MVC):
@Bean public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("Messages"); return messageSource; }
…and that basename
property indicates that we will have files like Messages_es.properties
or Messages_en.properties
in our classpath. Let’s have a look at the Spanish
version:
title.list=Lista de semilleros date.format=dd/MM/yyyy bool.true=sí bool.false=no seedstarter.datePlanted=Fecha de plantación seedstarter.covered=Cubierto seedstarter.type=Tipo seedstarter.features=Características seedstarter.rows=Filas seedstarter.type.WOOD=Madera seedstarter.type.PLASTIC=Plástico seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros seedstarter.feature.FERTILIZER=Fertilizante seedstarter.feature.PH_CORRECTOR=Corrector de PH
In the first column of the table listing we will show the date when the seed
starter was prepared. But we will show it formatted in the way we defined
in our DateFormatter
. In order to do that we will use the double-brace
syntax (${{...}}
), which will automatically apply the Spring Conversion Service,
including the DateFormatter
we registered at configuration.
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
Next is showing whether the seed starter container is covered or not, by
transforming the value of the boolean covered bean property into an
internationalized «yes» or «no» with a literal substitution expression:
<td th:text="#{|bool.${sb.covered}|}">yes</td>
Now we have to show the type of seed starter container. Type is a java enum with
two values (WOOD
and PLASTIC
), and that’s why we defined two properties in
our Messages
file called seedstarter.type.WOOD
and seedstarter.type.PLASTIC
.
But in order to obtain the internationalized names of the types, we will need to
add the seedstarter.type.
prefix to the enum value by means of an expression,
which result we will then use as the message key:
<td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td>
The most difficult part of this listing is the features column. In it we want
to display all the features of our container —that come in the form of an array
of Feature
enums—, separated by commas. Like «Electric Heating, Turf».
Note that this is particularly difficult because these enum values also need to
be externalized, as we did with Types. The flow is then:
- Prepend the corresponding prefix to all the elements of the
features
array. - Obtain the externalized messages corresponding to all the keys from step 1.
- Join all the messages obtained in step 2, using a comma as a delimiter.
For achieving this, we create the following code:
<td th:text="${#strings.arrayJoin( #messages.arrayMsg( #strings.arrayPrepend(sb.features,'seedstarter.feature.')), ', ')}">Electric Heating, Turf</td>
The last column of our listing will be quite simple, in fact. Even if it has a
nested table for showing the contents of each row in the container:
<td> <table> <tbody> <tr th:each="row,rowStat : ${sb.rows}"> <td th:text="${rowStat.count}">1</td> <td th:text="${row.variety.name}">Thymus Thymi</td> <td th:text="${row.seedsPerCell}">12</td> </tr> </tbody> </table> </td>
6 Creating a Form
6.1 Handling the command object
Command object is the name Spring MVC gives to form-backing beans, this is, to
objects that model a form’s fields and provide getter and setter methods that
will be used by the framework for establishing and obtaining the values input by
the user at the browser side.
Thymeleaf requires you to specify the command object by using a th:object
attribute in your <form>
tag:
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post"> ... </form>
This is consistent with other uses of th:object,
but in fact this
specific scenario adds some limitations in order to correctly integrate with
Spring MVC’s infrastructure:
- Values for
th:object
attributes in form tags must be variable expressions (${...}
)
specifying only the name of a model attribute, without property navigation.
This means that an expression like${seedStarter}
is valid, but${seedStarter.data}
would not be. - Once inside the
<form>
tag, no otherth:object
attribute can be specified.
This is consistent with the fact that HTML forms cannot be nested.
6.2 Inputs
Let’s see now how to add an input to our form:
<input type="text" th:field="*{datePlanted}" />
As you can see, we are introducing a new attribute here: th:field.
This is a
very important feature for Spring MVC integration because it does all the heavy
work of binding your input with a property in the form-backing bean. You can see
it as an equivalent of the path attribute in a form:input tag from Spring
MVC’s JSP tag library.
The th:field
attribute behaves differently depending on whether it is attached
to an <input>
, <select>
or <textarea>
tag (and also depending on the
specific type of <input>
tag). In this case (input[type=text]
), the above
line of code is similar to:
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
…but in fact it is a little bit more than that, because th:field
will also apply the registered Spring Conversion Service, including the DateFormatter
we saw before (even if the field expression is not double-bracketed). Thanks to this, the date will be shown correctly formatted.
Values for th:field
attributes must be selection expressions (*{...}
), which
makes sense given the fact that they will be evaluated on the form-backing bean
and not on the context variables (or model attributes in Spring MVC jargon).
Contrary to the ones in th:object
, these expressions can include property
navigation (in fact any expression allowed for the path attribute of a <form:input>
JSP tag will be allowed here).
Note that th:field
also understands the new types of <input>
element
introduced by HTML5 like <input type="datetime" ... />
, <input type="color" ... />
,
etc., effectively adding complete HTML5 support to Spring MVC.
6.3 Checkbox fields
th:field
also allows us to define checkbox inputs. Let’s see an example from
our HTML page:
<div> <label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">Covered</label> <input type="checkbox" th:field="*{covered}" /> </div>
Note there’s some fine stuff here besides the checkbox itself, like an
externalized label and also the use of the #ids.next('covered')
function for
obtaining the value that will be applied to the id attribute of the checkbox
input.
Why do we need this dynamic generation of an id attribute for this field?
Because checkboxes are potentially multi-valued, and thus their id values will
always be suffixed a sequence number (by internally using the #ids.seq(...)
function) in order to ensure that each of the checkbox inputs for the same
property has a different id value.
We can see this more easily if we look at such a multi-valued checkbox field:
<ul> <li th:each="feat : ${allFeatures}"> <input type="checkbox" th:field="*{features}" th:value="${feat}" /> <label th:for="${#ids.prev('features')}" th:text="#{${'seedstarter.feature.' + feat}}">Heating</label> </li> </ul>
Note that we’ve added a th:value
attribute this time, because the features
field is not a boolean like covered was, but instead is an array of values.
Let’s see the HTML output generated by this code:
<ul> <li> <input id="features1" name="features" type="checkbox" value="SEEDSTARTER_SPECIFIC_SUBSTRATE" /> <input name="_features" type="hidden" value="on" /> <label for="features1">Seed starter-specific substrate</label> </li> <li> <input id="features2" name="features" type="checkbox" value="FERTILIZER" /> <input name="_features" type="hidden" value="on" /> <label for="features2">Fertilizer used</label> </li> <li> <input id="features3" name="features" type="checkbox" value="PH_CORRECTOR" /> <input name="_features" type="hidden" value="on" /> <label for="features3">PH Corrector used</label> </li> </ul>
We can see here how a sequence suffix is added to each input’s id attribute, and
how the #ids.prev(...)
function allows us to retrieve the last sequence value
generated for a specific input id.
Don’t worry about those hidden inputs with
name="_features"
: they are
automatically added in order to avoid problems with browsers not sending
unchecked checkbox values to the server upon form submission.
Also note that if our features property contained some selected values in our
form-backing bean, th:field
would have taken care of that and would have added
a checked="checked"
attribute to the corresponding input tags.
6.4 Radio Button fields
Radio button fields are specified in a similar way to non-boolean (multi-valued)
checkboxes —except that they are not multivalued, of course:
<ul> <li th:each="ty : ${allTypes}"> <input type="radio" th:field="*{type}" th:value="${ty}" /> <label th:for="${#ids.prev('type')}" th:text="#{${'seedstarter.type.' + ty}}">Wireframe</label> </li> </ul>
6.5 Dropdown/List selectors
Select fields have two parts: the <select>
tag and its nested <option>
tags.
When creating this kind of field, only the <select>
tag has to include a th:field
attribute, but the th:value
attributes in the nested <option>
tags will be
very important because they will provide the means of knowing which is the
currently selected option (in a similar way to non-boolean checkboxes and radio
buttons).
Let’s re-build the type field as a dropdown select:
<select th:field="*{type}"> <option th:each="type : ${allTypes}" th:value="${type}" th:text="#{${'seedstarter.type.' + type}}">Wireframe</option> </select>
At this point, understanding this piece of code is quite easy. Just notice how
attribute precedence allows us to set the th:each
attribute in the <option>
tag itself.
6.6 Dynamic fields
Thanks to the advanced form-field binding capabilities in Spring MVC, we can use
complex Spring EL expressions to bind dynamic form fields to our form-backing
bean. This will allow us to create new Row objects in our SeedStarter
bean,
and to add those rows’ fields to our form at user request.
In order to do this, we will need a couple of new mapped methods in our
controller, which will add or remove a row from our SeedStarter
depending on
the existence of specific request parameters:
@RequestMapping(value="/seedstartermng", params={"addRow"}) public String addRow(final SeedStarter seedStarter, final BindingResult bindingResult) { seedStarter.getRows().add(new Row()); return "seedstartermng"; } @RequestMapping(value="/seedstartermng", params={"removeRow"}) public String removeRow( final SeedStarter seedStarter, final BindingResult bindingResult, final HttpServletRequest req) { final Integer rowId = Integer.valueOf(req.getParameter("removeRow")); seedStarter.getRows().remove(rowId.intValue()); return "seedstartermng"; }
And now we can add a dynamic table to our form:
<table> <thead> <tr> <th th:text="#{seedstarter.rows.head.rownum}">Row</th> <th th:text="#{seedstarter.rows.head.variety}">Variety</th> <th th:text="#{seedstarter.rows.head.seedsPerCell}">Seeds per cell</th> <th> <button type="submit" name="addRow" th:text="#{seedstarter.row.add}">Add row</button> </th> </tr> </thead> <tbody> <tr th:each="row,rowStat : *{rows}"> <td th:text="${rowStat.count}">1</td> <td> <select th:field="*{rows[__${rowStat.index}__].variety}"> <option th:each="var : ${allVarieties}" th:value="${var.id}" th:text="${var.name}">Thymus Thymi</option> </select> </td> <td> <input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}" /> </td> <td> <button type="submit" name="removeRow" th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button> </td> </tr> </tbody> </table>
Quite a lot of things to see here, but not much we should not understand by now…
except for one strange
thing:
<select th:field="*{rows[__${rowStat.index}__].variety}"> ... </select>
If you recall from the «Using Thymeleaf» tutorial, that __${...}__
syntax is
a preprocessing expression, which is an inner expression that is evaluated
before actually evaluating the whole expression. But why that way of specifying
the row index? Wouldn’t it be enough with:
<select th:field="*{rows[rowStat.index].variety}"> ... </select>
…well, actually, no. The problem is that Spring EL does not evaluate variables
inside array index brackets, so when executing the above expression we would
obtain an error telling us that rows[rowStat.index]
(instead of rows[0]
,
rows[1]
, etc) is not a valid position in the rows collection. That’s why
preprocessing is needed here.
Let’s have a look at a fragment of the resulting HTML after pressing «Add Row»
a couple of times:
<tbody> <tr> <td>1</td> <td> <select id="rows0.variety" name="rows[0].variety"> <option selected="selected" value="1">Thymus vulgaris</option> <option value="2">Thymus x citriodorus</option> <option value="3">Thymus herba-barona</option> <option value="4">Thymus pseudolaginosus</option> <option value="5">Thymus serpyllum</option> </select> </td> <td> <input id="rows0.seedsPerCell" name="rows[0].seedsPerCell" type="text" value="" /> </td> <td> <button name="removeRow" type="submit" value="0">Remove row</button> </td> </tr> <tr> <td>2</td> <td> <select id="rows1.variety" name="rows[1].variety"> <option selected="selected" value="1">Thymus vulgaris</option> <option value="2">Thymus x citriodorus</option> <option value="3">Thymus herba-barona</option> <option value="4">Thymus pseudolaginosus</option> <option value="5">Thymus serpyllum</option> </select> </td> <td> <input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" /> </td> <td> <button name="removeRow" type="submit" value="1">Remove row</button> </td> </tr> </tbody>
7 Validation and Error Messages
Most of our forms will need to show validation messages in order to inform the
user of the errors he/she has made.
Thymeleaf offers some tools for this: a couple of functions in the #fields
object, the th:errors
and the th:errorclass
attributes.
7.1 Field errors
Let’s see how we could set a specific CSS class to a field if it has an error:
<input type="text" th:field="*{datePlanted}" th:class="${#fields.hasErrors('datePlanted')}? fieldError" />
As you can see, the #fields.hasErrors(...)
function receives the field
expression as a parameter (datePlanted
), and returns a boolean telling whether any validation
errors exist for that field.
We could also obtain all the errors for that field and iterate them:
<ul> <li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" /> </ul>
Instead of iterating, we could have also used th:errors
, a specialized attribute which builds a list with all the errors for the specified selector, separated by <br />
:
<input type="text" th:field="*{datePlanted}" /> <p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
Simplifying error-based CSS styling: th:errorclass
The example we saw above, setting a CSS class to a form input if that field has errors, is so common that Thymeleaf offers a specific attribute for doing exacly that: th:errorclass
.
Applied to a form field tag (input, select, textarea…), it will read the name of the field to be examined from any existing name
or th:field
attributes in the same tag, and then append the specified CSS class to the tag if such field has any associated errors:
<input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" />
If datePlanted
has errors, this will render as:
<input type="text" id="datePlanted" name="datePlanted" value="2013-01-01" class="small fieldError" />
7.2 All errors
And what if we want to show all the errors in the form? We just need to query the #fields.hasErrors(...)
and #fields.errors(...)
methods with the '*'
or 'all'
constants (which are equivalent):
<ul th:if="${#fields.hasErrors('*')}"> <li th:each="err : ${#fields.errors('*')}" th:text="${err}">Input is incorrect</li> </ul>
As in the examples above, we could obtain all the errors and iterate them…
<ul> <li th:each="err : ${#fields.errors('*')}" th:text="${err}" /> </ul>
…as well as build a <br />
-separated list:
<p th:if="${#fields.hasErrors('all')}" th:errors="*{all}">Incorrect date</p>
Finally note that #fields.hasErrors('*')
is equivalent to #fields.hasAnyErrors()
and #fields.errors('*')
is equivalent to #fields.allErrors()
. Use whichever syntax you prefer:
<div th:if="${#fields.hasAnyErrors()}"> <p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p> </div>
7.3 Global errors
There is a third type of error in a Spring form: global errors. These are errors that are not associated with any specific fields in the form, but still exist.
Thymeleaf offers the global
constant for accessing these errors:
<ul th:if="${#fields.hasErrors('global')}"> <li th:each="err : ${#fields.errors('global')}" th:text="${err}">Input is incorrect</li> </ul>
<p th:if="${#fields.hasErrors('global')}" th:errors="*{global}">Incorrect date</p>
…as well as equivalent #fields.hasGlobalErrors()
and #fields.globalErrors()
convenience methods:
<div th:if="${#fields.hasGlobalErrors()}"> <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p> </div>
7.4 Displaying errors outside forms
Form validation errors can also be displayed outside forms by using variable (${...}
) instead of selection (*{...}
) expressions and prefixing the name of the form-backing bean:
<div th:errors="${myForm}">...</div> <div th:errors="${myForm.date}">...</div> <div th:errors="${myForm.*}">...</div> <div th:if="${#fields.hasErrors('${myForm}')}">...</div> <div th:if="${#fields.hasErrors('${myForm.date}')}">...</div> <div th:if="${#fields.hasErrors('${myForm.*}')}">...</div> <form th:object="${myForm}"> ... </form>
7.5 Rich error objects
Thymeleaf offers the possibility to obtain form error information in the form of beans (instead of mere strings), with the fieldName
(String), message
(String) and global
(boolean) attributes.
These errors can be obtained by means of the #fields.detailedErrors()
utility method:
<ul> <li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr"> <span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> | <span th:text="${e.message}">The error message</span> </li> </ul>
8 It’s still a Prototype!
Our application is ready now. But let’s have a second look at the .html
page
we created…
One of the nicest consequences of working with Thymeleaf is that after all this
functionality we have added to our HTML, we can still use it as a prototype (we say it is a Natural Template). Let’s open seedstartermng.html
directly in our browser without executing our application:
There it is! It’s not a working application, it’s not real data… but it is a
perfectly valid prototype made up of perfectly displayable HTML code.
9 The Conversion Service
9.1 Configuration
As explained before, Thymeleaf can make use of a Conversion Service registered at the Application Context. Our
application configuration class, by extending Spring’s own WebMvcConfigurerAdapter
helper, will automatically
register such conversion service, which we can configure by adding the formatters that we need. Let’s see
again what it looks like:
@Override public void addFormatters(final FormatterRegistry registry) { super.addFormatters(registry); registry.addFormatter(varietyFormatter()); registry.addFormatter(dateFormatter()); } @Bean public VarietyFormatter varietyFormatter() { return new VarietyFormatter(); } @Bean public DateFormatter dateFormatter() { return new DateFormatter(); }
9.1 Double-brace syntax
The Conversion Service can be easily applied in order to convert/format any object into String. This
is done by means of the double-brace expression syntax:
- For variable expressions:
${{...}}
- For selection expressions:
*{{...}}
So, for example, given an Integer-to-String converter that adds commas as a thousands separator, this:
<p th:text="${val}">...</p> <p th:text="${{val}}">...</p>
…should result in:
<p>1234567890</p> <p>1,234,567,890</p>
9.2 Use in forms
We saw before that every th:field
attribute will always apply the conversion service, so this:
<input type="text" th:field="*{datePlanted}" />
…is actually equivalent to:
<input type="text" th:field="*{{datePlanted}}" />
Note that, per requirement of Spring, this is the only scenario in which the Conversion Service
is applied in expressions using single-brace syntax.
9.3 #conversions
utility object
The #conversions
expression utility object allows the manual execution of the Conversion Service wherever needed:
<p th:text="${'Val: ' + #conversions.convert(val,'String')}">...</p>
Syntax for this utility object:
#conversions.convert(Object,Class)
: converts the object to the specified class.#conversions.convert(Object,String)
: same as above, but specifying the target class as a String (note thejava.lang.
package can be ommitted).
10 Rendering Template Fragments
Thymeleaf offers the possibility to render only part of a template as the result of its execution: a fragment.
This can be a useful componentization tool. For example, it can be used at controllers that execute on AJAX calls, which might return markup fragments of a page that is already loaded at the browser (for updating a select, enabling/disabling buttons…).
Fragmentary rendering can be achieved by using Thymeleaf’s fragment specs: objects implementing the org.thymeleaf.fragment.IFragmentSpec
interface.
The most common of these implementations is org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec
, which allows specifying a fragment using a DOM Selector exactly like the ones used at th:include
or th:replace
.
10.1 Specifying fragments in view beans
View beans are beans of the org.thymeleaf.spring4.view.ThymeleafView
class declared at the application
context (@Bean
declarations if you are using Java configuration). They allow the specification of fragments like this:
@Bean(name="content-part") @Scope("prototype") public ThymeleafView someViewBean() { ThymeleafView view = new ThymeleafView("index"); // templateName = 'index' view.setMarkupSelector("content"); return view; }
Given the above bean definition, if our controller returns content-part
(the name of the above bean)…
@RequestMapping("/showContentPart") public String showContentPart() { ... return "content-part"; }
…thymeleaf will return only the content
fragment of the index
template — which location will
probably be something like /WEB-INF/templates/index.html
, once prefix and suffix are applied. So
the result will be completely equivalent to specifying index :: content
:
<!DOCTYPE html> <html> ... <body> ... <div th:fragment="content"> Only this div will be rendered! </div> ... </body> </html>
Note also that, thanks to the power of Thymeleaf Markup Selectors, we could select a fragment in a
template without needing any th:fragment
attributes at all. Let’s use the id
attribute, for example:
@Bean(name="content-part") @Scope("prototype") public ThymeleafView someViewBean() { ThymeleafView view = new ThymeleafView("index"); // templateName = 'index' view.setMarkupSelector("#content"); return view; }
…which will perfectly select:
<!DOCTYPE html> <html> ... <body> ... <div id="content"> Only this div will be rendered! </div> ... </body> </html>
10.2 Specifying fragments in controller return values
Instead of declaring view beans, fragments can be specified from the controllers themselves by using the
syntax of fragment expressions. Just like in th:insert
or th:replace
attributes:
@RequestMapping("/showContentPart") public String showContentPart() { ... return "index :: content"; }
Of course, again the full power of DOM Selectors is available, so we could select our fragment based on
standard HTML attributes, like id="content"
:
@RequestMapping("/showContentPart") public String showContentPart() { ... return "index :: #content"; }
And we can also use parameters, like:
@RequestMapping("/showContentPart") public String showContentPart() { ... return "index :: #content ('myvalue')"; }
11 Advanced Integration Features
11.1 Integration with RequestDataValueProcessor
Thymeleaf seamlessly integrates with Spring’s RequestDataValueProcessor
interface. This interface allows the interception of link URLs, form URLs and form field values before they are written to the markup result, as well as transparently adding hidden form fields that enable security features like e.g. protection agains CSRF (Cross-Site Request Forgery).
An implementation of RequestDataValueProcessor
can be easily configured at the Application Context. It needs to implement
the org.springframework.web.servlet.support.RequestDataValueProcessor
interface and have
requestDataValueProcessor
as a bean name:
@Bean public RequestDataValueProcessor requestDataValueProcessor() { return new MyRequestDataValueProcessor(); }
…and Thymeleaf will use it this way:
-
th:href
andth:src
callRequestDataValueProcessor.processUrl(...)
before rendering the URL. -
th:action
callsRequestDataValueProcessor.processAction(...)
before rendering the form’saction
attribute, and additionally it detects when this attribute is being applied on a<form>
tag —which should be the only place, anyway—, and in such case callsRequestDataValueProcessor.getExtraHiddenFields(...)
and adds the returned hidden fields just before the closing</form>
tag. -
th:value
callsRequestDataValueProcessor.processFormFieldValue(...)
for rendering the value it refers to, unless there is ath:field
present in the same tag (in which caseth:field
will take care). -
th:field
callsRequestDataValueProcessor.processFormFieldValue(...)
for rendering the value of the field it applies to (or the tag body if it is a<textarea>
).
Note there are very few scenarios in which you would need to explicitly implement
RequestDataValueProcessor
in your application. In most cases, this will be used automatically by security libraries you transparently use,
like e.g. Spring Security’s CSRF support.
11.1 Building URIs to controllers
Since version 4.1, Spring allows the possibility to build links to annotated controllers directly from views, without the
need to know the URIs these controllers are mapped to.
In Thymeleaf, this can be achieved by means of the #mvc.url(...)
expression object method, which allows the
specification of controller methods by the capital letters of the controller class they are in, followed by
the name of the method itself. This is equivalent to JSP’s spring:mvcUrl(...)
custom function.
For example, for:
public class ExampleController { @RequestMapping("/data") public String getData(Model model) { ... return "template" } @RequestMapping("/data") public String getDataParam(@RequestParam String type) { ... return "template" } }
The following code will create a link to it:
<a th:href="${(#mvc.url('EC#getData')).build()}">Get Data Param</a> <a th:href="${(#mvc.url('EC#getDataParam').arg(0,'internal')).build()}">Get Data Param</a>
You can read more about this mechanism
at http://docs.spring.io/spring-framework/docs/4.1.2.RELEASE/spring-framework-reference/html/mvc.html#mvc-links-to-controllers-from-views
12 Spring WebFlow integration
12.1 Basic configuration
The Thymeleaf + Spring integration packages include integration with Spring
WebFlow.
Note: Spring WebFlow 2.5+ is required when Thymeleaf is used with Spring 5, while only
versions older than WebFlow 2.5 are allowed when Thymeleaf is used with earlier
versions of Spring.
WebFlow includes some AJAX capabilities for rendering fragments of the displayed
page when specific events (transitions) are triggered, and in order to enable
Thymeleaf to attend these AJAX requests, we will have to use a different ViewResolver
implementation, configured like this (for Spring WebFlow 2.5+):
@Bean public FlowDefinitionRegistry flowRegistry() { // NOTE: Additional configuration might be needed in your app return getFlowDefinitionRegistryBuilder() .addFlowLocation("...") .setFlowBuilderServices(flowBuilderServices()) .build(); } @Bean public FlowExecutor flowExecutor() { // NOTE: Additional configuration might be needed in your app return getFlowExecutorBuilder(flowRegistry()).build(); } @Bean public FlowBuilderServices flowBuilderServices() { // NOTE: Additional configuration might be needed in your app return getFlowBuilderServicesBuilder() .setViewFactoryCreator(viewFactoryCreator()) .build(); } @Bean public ViewFactoryCreator viewFactoryCreator() { MvcViewFactoryCreator factoryCreator = new MvcViewFactoryCreator(); factoryCreator.setViewResolvers( Collections.singletonList(thymeleafViewResolver())); factoryCreator.setUseSpringBeanBinding(true); return factoryCreator; } @Bean public ViewResolver thymeleafViewResolver() { AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver(); // We need to set a special ThymeleafView implementation: FlowAjaxThymeleafView viewResolver.setViewClass(FlowAjaxThymeleafView.class); viewResolver.setTemplateEngine(templateEngine()); return viewResolver; }
Note the above is not a complete configuration: you will still need to configure your handlers, etc. Refer to the
Spring WebFlow documentation for that.
From here on, you can specify Thymeleaf templates in your view-state’s:
<view-state id="detail" view="bookingDetail"> ... </view-state>
In the above example, bookingDetail
is a Thymeleaf template specified in the
usual way, understandable by any of the Template Resolvers configured at the TemplateEngine
.
12.2 AJAX fragments in Spring WebFlow
Note that what is explained here is just the way to create AJAX fragments to be used
with Spring WebFlow. If you are not using WebFlow, creating a Spring MVC controller that
responds to an AJAX request and returns a chunk of HTML is as straightforward as creating
any other template-returning controller, with the only exception that you would probably
be returning a fragment like"main :: admin"
from your controller method.
WebFlow allows the specification of fragments to be rendered via AJAX with <render>
tags, like this:
<view-state id="detail" view="bookingDetail"> <transition on="updateData"> <render fragments="hoteldata"/> </transition> </view-state>
These fragments (hoteldata
, in this case) can be a comma-separated list of
fragments specified at the markup with th:fragment
:
<div id="data" th:fragment="hoteldata"> This is a content to be changed </div>
Always remember that the specified fragments must have an id
attribute, so
that the Spring JavaScript libraries running on the browser are capable of
substituting the markup.
<render>
tags can also be specified using DOM selectors:
<view-state id="detail" view="bookingDetail"> <transition on="updateData"> <render fragments="[//div[@id='data']]"/> </transition> </view-state>
…and this will mean no th:fragment
is needed:
<div id="data"> This is a content to be changed </div>
As for the code that triggers the updateData
transition, it looks like:
<script type="text/javascript" th:src="@{/resources/dojo/dojo.js}"></script> <script type="text/javascript" th:src="@{/resources/spring/Spring.js}"></script> <script type="text/javascript" th:src="@{/resources/spring/Spring-Dojo.js}"></script> ... <form id="triggerform" method="post" action=""> <input type="submit" id="doUpdate" name="_eventId_updateData" value="Update now!" /> </form> <script type="text/javascript"> Spring.addDecoration( new Spring.AjaxEventDecoration({formId:'triggerform',elementId:'doUpdate',event:'onclick'})); </script>
В этой статье рассмотрим, как заменить автоматически генерируемую форму ввода имени и пароля на свою собственную на Thymeleaf.
Метод formLogin() генерирует как саму форму логина, так и конечную точку обработки имени и пароля, приходящих с формы. Конечную точку, обрабатывающую по POST-запросу имя и пароль, переписывать не нужно, а вот саму html-форму, выдающуюся по методу GET /login обычно требуется заменить.
Настройка HttpSecurity
Чтобы это сделать, при настройке HttpSecurity нужно указать не просто formLogin(), как мы делали раньше, а еще .loginPage(«/login») с адресом шаблона:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .formLogin().permitAll() .loginPage("/login") .loginProcessingUrl("/perform-login") .usernameParameter("user") .passwordParameter("pass") .defaultSuccessUrl("/"); }
На адрес /login (указанный в loginPage(), адрес можно указать и другой) необходимо добавить контроллер, который выдает шаблон.
Контроллер
@Controller public class LoginController { @GetMapping("/login") public String get(Model model) { model.addAttribute("title", "Форма входа"); return "login"; } }
Как видите, мы передали tittle в модель, но это необязательно — его можно сразу прописать в шаблоне.
Шаблон
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title th:text="${title}"></title> </head> <body> <div> <form name="f" th:action="@{/perform-login}" method="post"> <fieldset> <legend>Войдите</legend> <div th:if="${param.error}" class="alert alert-error"> Неправильные имя и пароль. </div> <div th:if="${param.logout}" class="alert alert-success"> You have been logged out. </div> <label for="user">Имя</label> <input type="text" id="user" name="user"/> <label for="pass">Пароль</label> <input type="password" id="pass" name="pass"/> <div class="form-actions"> <button type="submit" class="btn">Войти</button> </div> </fieldset> </form> </div> </body> </html>
Выглядит так:
В шаблоне обратите внимание на ряд параметров, которые важны для конечной точки, обрабатывающей имя и пароль по POST-запросу.
Во-первых, это имена элементов input — они превращаются в имена параметров POST-запроса.
usernameParameter() и passwordParameter()
В автоматически генерируемой форме они называются username и password, а поскольку у нас это user и pass (см. атрибуты name):
<input type="text" id="user" name="user"/> <input type="password" id="pass" name="pass"/>
то в коде настройки HttpSecurity мы должны переопределить их в методах:
.usernameParameter("user") .passwordParameter("pass")
Эти строки есть в первом сниппете.
loginProcessingUrl()
Также переопределен loginProcessingUrl():
.loginProcessingUrl("/perform-login")
Это адрес конечной точки, куда по методу POST отправляются имя и пароль при нажатии кнопки входа. Адрес переопределен, но сама точка осталась автогенерируемой (ее генерирует Spring Security).
В шаблоне этот адрес соответствует action формы:
<form name="f" th:action="@{/perform-login}" method="post">
defaultSuccessUrl()
Наконец, можно задать
.defaultSuccessUrl("/");
это адрес страницы, куда пользователь перенаправляется при успешном входе.
Обычно он перенаправляется на ту самую закрытую страницу, куда пытался попасть (а после этого был перенаправлен на страницу входа /login).
Но если он не пытался попасть на закрытую страницу, а сразу заходил с /login, то куда-то его надо перенаправить. Вот тут и указывается этот адрес. У нас это главная страница.
Сообщение об ошибке
Наконец, если были введены неправильные имя и пароль, необходимо перенаправить пользователя на страницу с ошибкой. По умолчанию этот тот же адрес /login, но с параметром:
/login?error
В шаблоне есть обработка этого параметра:
<div th:if="${param.error}" class="alert alert-error"> Неправильные имя и пароль. </div>
Не стоит пытаться уточнять сообщение (что именно неправильное: имя или пароль). Spring Security рекомендует оставить все как есть, чтобы злоумышленник не пытался подобрать другое имя — в общем лишней информации тут давать не нужно.
Исходный код
Скачать пример полностью можно на GitHub.
Сегодня дополним наш предыдущий урок Spring Boot и Thymeleaf JAR добавив Spring Sequrity.
Итак, собираем SPRING BOOT + SPRING SECURITY приложение в связке с встроенным сервером приложений Tomcat и движком шаблонов Thymeleaf в качестве исполняемого JAR-файла.
содержание
-
-
- Технологии:
-
- 1. Структура каталогов
- 2. Зависимости
- 3. Spring Security
- 4. Spring Boot
- 5. Thymeleaf + Resources + статические файлы
- 6. Запуск приложения
- Ссылки
Технологии:
- Spring Boot 1.5.9.RELEASE
- Spring 4.3.13.RELEASE
- Spring Security 4.2.2
- Thymeleaf 2.1.5.RELEASE
- Thymeleaf extras Spring Security4 2.1.3
- Tomcat Embed 8.5.14
- Maven 3
- Java 8
1. Структура каталогов
2. Зависимости
Объявляем зависимость spring-boot-starter-security, она даст нам все необходимые инструменты для Spring Boot + Spring Security приложения.
Я как обычно использую Spring Initializr
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ru.leodev.examples.springboot</groupId> <artifactId>spring-boot-web-spring-security</artifactId> <version>0.0.1</version> <packaging>jar</packaging> <name>Spring Boot Web Spring Security</name> <description>Spring Boot Web Spring Security Example</description> <url> http://leodev.ru/blog/news/spring-boot-spri…hymeleaf-example/</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- наш старый добрый thymeleaf шаблонизатор --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- опционально, библиотека добавит полезные теги для SPRING SECURITY в Thymeleaf шаблонах --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency> <!-- содержит в себе необходимые библиотеки для создания ВЕБ приложения --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- горячая подмена, выключенный кеш для шаблонов, включенный live reload --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>runtime</scope> </dependency> <!-- опционально, подгружаем bootstrap --> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> </dependencies> <build> <!-- так же мы можем указать жесткое имя при сборке приложения, что бы maven не дописывал версию например или сразу подготовить для деплоя на сервер --> <finalName>spring-boot-web-spring-security</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3.1 Наследуемся от WebSecurityConfigurerAdapter, и переопределяем правила безопасности в void configure(HttpSecurity http)
Подобные конфиги я обычно кладу в пакет config, в Spring MVC их было штуки 4
Для пользователя «admin»:
Разрешен доступ к странице /admin
Запрещен доступ к странице /user , будет перенаправлять на 403 access deniedДля пользователя «user»:
Разрешен доступ к странице /user
Запрещен доступ к странице /admin, будет перенаправлять на 403 access denied
package ru.leodev.examples.springboot.springbootwebspringsecurity.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.access.AccessDeniedHandler; @Configuration public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AccessDeniedHandler accessDeniedHandler; // роль admin всегда есть доступ к /admin/** // роль user всегда есть доступ к /user/** // Наш кастомный "403 access denied" обработчик @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/", "/index", "/about").permitAll() .antMatchers("/admin/**").hasAnyRole("ADMIN") .antMatchers("/user/**").hasAnyRole("USER") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll() .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler); } // создаем пользоватлелей, admin и user @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("admin").password("password").roles("ADMIN"); } }
[stextbox id=’warning’ caption=’configure(HttpSecurity)’ collapsing=»true»]
Метод configure(HttpSecurity) определяет, какие URL пути должны быть защищены, а какие нет. В частности, «/» и «/home» настроены без требования к авторизации. Ко всем остальным путям должна быть произведена аутентификация.
Когда пользователь успешно войдет в систему, он будет перенаправлен на предыдущую запрашиваемую страницу, требующую авторизацию.
[/stextbox]
[stextbox id=’warning’ caption=’configure(AuthenticationManagerBuilder)’ collapsing=»true»]метод configure(AuthenticationManagerBuilder), то он создает в памяти хранилище пользователей, в нашем случае их 2 user и admin[/stextbox]
3.2 Наш кастомный 403 Access denied обработчик, логирует запрос и перенаправляет на /403
package ru.leodev.examples.springboot.springbootwebspringsecurity.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * обрабатывает 403 ошибку перенаправляя в случае ее вызова на /403 страницу */ @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { private static Logger logger = LoggerFactory.getLogger(MyAccessDeniedHandler.class); @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null){ logger.info("User '" + auth.getName() + "' attempted to access the protected URL: " + httpServletRequest.getRequestURI()); } httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/403"); } }
4. Spring Boot
4.1 Добавим контроллер, определим в нем имена вьюх и http запросы
[stextbox id=’info’ caption=’Почему @GetMapping?’ collapsing=»true» collapsed=»true» shadow=»false»]
В контроллере мы используем @GetMapping, по сути он эквивалентен @RequestMapping(method = RequestMethod.GET) но более лаконичен.
Так же бывают такие варианты:
@GetMapping,
@PostMapping,
@PutMapping,
@DeleteMapping,
@PatchMapping
[/stextbox]
package ru.leodev.examples.springboot.springbootwebspringsecurity.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class MainController { //Мы могли бы расписать эти 2 маппинга отдельно, но смысла дублировать одинаковый код нет. // этот метод будет слушать запросы на "/" и "/index" @GetMapping(value = {"/", "/index"}) public String index() { return "/index"; } @GetMapping("/admin") public String admin() { return "/admin"; } @GetMapping("/user") public String user() { return "/user"; } @GetMapping("/about") public String about() { return "/about"; } @GetMapping("/login") public String login() { return "/login"; } @GetMapping("/403") public String error403() { return "/error/403"; } }
4.2 Наша точка входа — сам Spring Boot application, если вы создавали проект в ручную
package ru.leodev.examples.springboot.springbootwebspringsecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringBootWebSpringSecurityApplication { public static void main(String[] args) { SpringApplication.run(SpringBootWebSpringSecurityApplication.class, args); } }
5. Thymeleaf + Resources + статические файлы
5.1 Для Thymeleaf файлов, создаем каталог в src/main/resources/templates/
5.2 Thymeleaf фрагменты, для шаблонов – header
<html xmlns:th="http://www.thymeleaf.org"> <head> <div th:fragment="header-css"> <!-- this is header-css --> <link rel="stylesheet" type="text/css" href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" th:href="@{/css/main.css}" href="../../css/main.css" /> </div> </head> <body> <div th:fragment="header"> <!-- this is header --> <nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" th:href="@{/}">Spring Boot</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a th:href="@{/}">Home</a></li> </ul> </div> </div> </nav> </div> </body> </html>
5.3 Thymeleaf фрагменты, для макета шаблона – footer. Обратите внимание на тег sec, он очень полезен для отображения информации Spring Security в шаблонах, подробнее Thymeleaf extra Spring Security
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> </head> <body> <div th:fragment="footer"> <div class="container"> <footer> <!-- FOOTER --> <!-- Благодаря дополнительной библиотеке которую мы опционально подключали в pom мы моежм использовать вспомогательные теги и без особых трудностей выводить имя пользователя на странице ВАЖНО! Старайтесь писать на английском, даже если не имеете подобного опыта(переводчик), это сэкономит вам нервы(проблемы с кодировками, фильтры) и даст бесценный опыт в будущем --> © 2018 leodev.ru <span sec:authorize="isAuthenticated()">| Logged user: <span sec:authentication="name"></span> | Roles: <span sec:authentication="principal.authorities"></span> | <a th:href="@{/logout}">Sign Out</a> </span> <script type="text/javascript" src="webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script> </footer> </div> </div> </body> </html>
5.4 Перечисляем Thymeleaf файлы и добавляем ссылки на страницы для более удобной навигации.
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Spring Boot Thymeleaf + Spring Security</title> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>Spring Boot Web Thymeleaf + Spring Security</h1> <h2>1. Посетите <a th:href="@{/admin}">страницу Admin (Защищено с помошью Spring Security, нужны права Администратора(Admin Role))</a></h2> <h2>2. Посетите <a th:href="@{/user}">страницу User (Защищено с помошью Spring Security, нужны права пользователя(User Role))</a></h2> <h2>3. Посетите <a th:href="@{/about}">Открытую страницу(доступна всем)</a></h2> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>Admin page (Spring Security protected)</h1> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>User page (Spring Security protected)</h1> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>Normal page (No need login)</h1> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" > <head> <title>Spring Security Example </title> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="row" style="margin-top:20px"> <div class="col-xs-12 col-sm-8 col-md-6 col-sm-offset-2 col-md-offset-3"> <form th:action="@{/login}" method="post"> <fieldset> <h1>Please Sign In</h1> <div th:if="${param.error}"> <div class="alert alert-danger"> Invalid username and password. </div> </div> <div th:if="${param.logout}"> <div class="alert alert-info"> You have been logged out. </div> </div> <div class="form-group"> <input type="text" name="username" id="username" class="form-control input-lg" placeholder="UserName" required="true" autofocus="true"/> </div> <div class="form-group"> <input type="password" name="password" id="password" class="form-control input-lg" placeholder="Password" required="true"/> </div> <div class="row"> <div class="col-xs-6 col-sm-6 col-md-6"> <input type="submit" class="btn btn-lg btn-primary btn-block" value="Sign In"/> </div> <div class="col-xs-6 col-sm-6 col-md-6"> </div> </div> </fieldset> </form> </div> </div> </div> <div th:replace="fragments/footer :: footer"/> </body> </html>
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>403 - Access is denied</h1> <div th:inline="text">Hello '[[${#httpServletRequest.remoteUser}]]', you do not have permission to access this page.</div> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
4.2 Статические файлы, такие как стили CSS или Javascript кладем в /src/main/resources/static/
h1{ color: #4a2bff; } h2{ color: #ff171c; } footer{ margin-top:60px; }
[stextbox id=’black’]Прочтите эту статью о Spring Boot Serving static content для общего понимания работы с ресурсами в Spring[/stextbox]
6. Запуск приложения
6.1 Запускаем приложение. Путь /admin/**
защищен ,нам нужно войти в систему как админ что бы получить к нему доступ
mvn spring-boot:run
6.2 Смотрим результат http://localhost:8080
6.3 Попытка доступа к http://localhost:8080/admin, автоматически отправляет нас на страницу авторизации http://localhost:8080/login
6.4 Не верный логин или пароль так же обрабатывается без нашего участия http://localhost:8080/login
6.5 При правильной авторизации нас автоматически направляет на страницу Админа http://localhost:8080/admin, к тому же мы избавились от дубликатов в написании header и footer благодаря фрагментам thymeleaf
6.6 А доступ к http://localhost:8080/user, по прежнему редиректит нас на 403 страницу http://localhost:8080/403 как мы и описывали в конфигурации Spring Security
6.7 Кликнув в низу страницы на ссылку sign out(выход), нас перенаправляет на http://localhost:8080/login?logout
Как видим и здесь Spring Security позаботился о нас 😉
Ссылки
- http://www.mkyong.com/
- Securing a Web Application
- Spring Security Reference
- Spring Boot Security features
- Spring Boot Hello World Example – Thymeleaf
- Spring Security Hello World Annotation Example
- Thymeleaf – Spring Security integration basics
- Thymeleaf extra – Spring Security integration basics
- Thymeleaf – Standard URL Syntax
- Spring Boot + Spring MVC + Spring Security + MySQL
- Spring Boot – Static content
- Spring MVC – Inlucde CSS file