Notice
I wrote this article and was originally published on Qiita on 17 September 2019.
Code is extracted from my notice board example application.
Thymeleaf template of input form 'resources/templates/private/message.html'
<!DOCTYPE html>
<html th:lang="${#locale.language}" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>[[#{applicationName}]] - [[${isEdit}? #{editMessage}: #{newMessage}]]</title>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
<h4 th:text="${isEdit}? #{editMessage}: #{newMessage}"></h4>
<form th:action="@{/manage/__${postHandler}__}" method="post" th:object="${message}" class="ui form">
<fieldset>
<legend>[[#{message}]]</legend>
<div class="field">
<label>[[#{message.publishDate}]]</label>
<input type="text" th:field="*{publishDate}" />
</div>
<div class="field">
<label>[[#{message.removeDate}]]</label>
<input type="text" th:field="*{removeDate}" />
</div>
<div class="field">
<label>[[#{message.description}]]</label>
<textarea name="description" th:text="*{description}"></textarea>
</div>
<button class="ui mini primary button">[[#{save}]] <i class="send icon"></i></button>
</fieldset>
</form>
</div>
</body>
</html>
Actual HTML code received by browner when calling http://localhost:8080/manage/new
<!DOCTYPE html>
<html lang="en">
<head>
<title>Notice board - New message</title>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
<h4>New message</h4>
<form action="/manage/new/save" method="post" class="ui form"><input type="hidden" name="_csrf" value="87baeee6-deea-4c4b-8b2b-b17e9be876e0"/>
<fieldset>
<legend>Message</legend>
<div class="field">
<label>Publish Date</label>
<input type="text" id="publishDate" name="publishDate" value="" />
</div>
<div class="field">
<label>Remove Date</label>
<input type="text" id="removeDate" name="removeDate" value="" />
</div>
<div class="field">
<label>Message</label>
<textarea name="description"></textarea>
</div>
<button class="ui mini primary button">Save <i class="send icon"></i></button>
</fieldset>
</form>
</div>
</body>
</html>
After filling data like below
click 'Save', a HTTP POST request with following parameter sends to http://localhost:8080/manage/new/save
saveCreateMessage() method in info.saladlam.example.spring.noticeboard.controller.PrivateController responses for handle this request
@Controller
@RequestMapping("/manage")
public class PrivateController {
@PostMapping("/new/save")
public String saveCreateMessage(@ModelAttribute MessageDto message, BindingResult errors) {
message.setOwner(this.getLoginName());
this.messageService.save(message);
return "redirect:/manage";
}
// ...
}
MessageDto instance is built by class org.springframework.web.method.annotation.ModelAttributeMethodProcessor, actual building operation is defined on method resolveArgument()
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
MessageDto instance obtains after running
bindRequestParameters(binder, webRequest);
and validation performs if validator is defined
validateIfApplicable(binder, parameter);
finally, MessageDto instance as parameter message is passed into controller