When creating a Map field in a form in Spring and using it, even if I intended to specify the order using LinkedHashMap, the order was changed when I submitted the value.
At this time, if you click the submit button, the following screen will appear.
If "spring" and "java" are swapped ...
■ Controller The point is the init method
@RequestMapping("/contents/convertor/app")
@Controller
public class ConvertorController {
private ConvertorForm form;
@Autowired
public ConvertorController(ConvertorForm form){
this.form = form;
}
/**
*For initial display
*/
@GetMapping
public void init(Model model) {
Map<String, String> map = new LinkedHashMap<String, String>(); //Try to register in order with LinkedHashMap
map.put("spring", "SPRING");
map.put("java", "JAVA");
form.setMap(map);
model.addAttribute("convertorForm", form); //Register form in model
}
/**
*Works when submitted.
*/
@PostMapping
public void submit(Model model, ConvertorForm form) {
}
}
■ Form (SessionScope bean with only Map)
@Component
@SessionScope
public class ConvertorForm {
private Map<String, String> map;
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
■ HTML(Thymeleaf)
<form th:action="@{/contents/convertor/app}" method="post" th:object="${convertorForm}">
<th:block th:each="element : *{map}">
<span th:text="${element.getKey()}"></span>
<input type="text" th:field="${convertorForm.map[__${element.getKey()}__]}"/>
<br />
</th:block>
<button>submit</button>
</form>
Modify the Controller as follows. To modify, just set the part where map was added to the model with model.addAttribute with @ModelAttribute. I knew that I could add attributes to the model by using @ModelAttribute, but I thought that the difference from model.addAttribute was not so different because of the timing of registration. The explanation will continue below, so please read it.
@Controller
public class ConvertorController {
private ConvertorForm form;
@Autowired
public ConvertorController(ConvertorForm form){
this.form = form;
}
/**
*Additional points
*/
@ModelAttribute
public ConvertorForm setup() {
return form;
}
/**
*For initial display
* @param model
*/
@GetMapping
public void init(Model model) {
Map<String, String> map = new LinkedHashMapCustom<String, String>();
map.put("spring", "SPRING");
map.put("java", "JAVA");
form.setMap(map);
//Delete ↓@Make it a Model Attribute
// model.addAttribute("convertorForm", form); //Register form in model
}
/**
*Works when submitted.
*
* @param model
* @param form
*/
@PostMapping
public void submit(Model model, ConvertorForm form) {
}
}
In Spring, how it is mapped to the form when the value is sent in the request becomes very important. When the submit button is clicked, it tries to generate ConverterForm which is an argument of the submit method of the controller from the sent request. At this time, the following operation is performed.
As a result, "spring" and "java" are sorted, so the order is changed.
As a result, "spring" and "java" are only put, so the order is not changed. Obviously, @ModelAttribute works before binding the value. This was the key. When I'm addicted, I don't notice this kind of thing ...
Personally, the storage in Map is in the order of request. In other words, I just thought that Spring would put it in the order stored in ServletRequest. So why is it sorted in the first place? Why not the order of requests? I was curious about it, so I investigated it.
In Spring, value binding is done in org.springframework.web.bind.ServletRequestDataBinder.bind.
public void bind(ServletRequest request) {
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); //← here
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {
HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
if (httpServletRequest != null) {
StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());
}
}
addBindValues(mpvs, request);
doBind(mpvs);
}
Following the constructor of ServletRequestParameterPropertyValues on the first line of this bind method is implemented as follows. Although it is described in the comment, it seems that it is sorted because the request.getParameter is stored in TreeMap.
public static Map<String, Object> getParametersStartingWith(ServletRequest request, @Nullable String prefix) {
Assert.notNull(request, "Request must not be null");
Enumeration<String> paramNames = request.getParameterNames();
Map<String, Object> params = new TreeMap<>(); //← I'm using TreeMap! !! !! !!
if (prefix == null) {
prefix = "";
}
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
if (prefix.isEmpty() || paramName.startsWith(prefix)) {
String unprefixed = paramName.substring(prefix.length());
String[] values = request.getParameterValues(paramName);
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
}
else if (values.length > 1) {
params.put(unprefixed, values);
}
else {
params.put(unprefixed, values[0]);
}
}
}
return params;
}
Hmmm, personally, it's more intuitive not to sort, but what about? Please let me know if anyone has any opinions. .. .. ..
--The execution order is natural, but @ModelAttribute⇒DataBinder⇒Controller method (model.addAttribute) --Basically, I think it's safer to use @ModelAttribute. -Be careful because it is related to @InitBinder (maybe I will write an article soon)
Recommended Posts