... There are a few problems when we need to add some additional processing steps for the Spring MVC Page Controller method parameters. Before the Spring 4.2 we needed to add additional Argument Resolver to do that which was a bit problematic if we need to mix our logic with standard Spring MVC existing one like declarative validation using @Valid annotation?
Assume we have the most popular Request Mapping approach (@Controller/@RequestMapping) and want to add some logic deal with with incoming parameters. It could be the problem of HTML-escaping (JavaScript, XML, SQL, whatever) of some fields for incoming POJOs (DTOs).
Here is the Controller code:
And we need to add HTML escape for the content field since it could be published somewhere on other pages.
The solution might be to start using RequestBodyAdvice feature available since Spring MVC.
Let's declare our Advice to MVC configuration:
and our RequestBodyAdvice looks like:
where EscapeBeanUtil is the utility to escape the fields. It is not recursive - just for the demo purposes. It searches for @EscapeHTML and escapes the annotated field.
Annotation:
Now, after these changes we have our POJO/DTO a bit modified (See at @EscapeHTML)
That's it!
Also please note that now validation is broken a bit: after HTML escaping limits for the content length could not fit since more chars are added. To overcome it you can pre-calculate the field size on UI or relax a bit the size or add some custom validator for that field. But very often for the big text inputs where the escape is required there are no limits
Assume we have the most popular Request Mapping approach (@Controller/@RequestMapping) and want to add some logic deal with with incoming parameters. It could be the problem of HTML-escaping (JavaScript, XML, SQL, whatever) of some fields for incoming POJOs (DTOs).
Here is the Controller code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package com.example.demo; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotBlank; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import javax.validation.Valid; @Controller public class ReportController { @GetMapping(value = "/content") void report(@RequestBody @Valid ContentRequest contentRequest) { // process it ... } @JsonTypeName("content") static class ContentRequest { @JsonProperty("id") long id; @JsonProperty("title") @Length(max = 10000, message = "{validation.max.length.exceeds}") @NotBlank(message = "{validation.not.empty}") String content; } } |
And we need to add HTML escape for the content field since it could be published somewhere on other pages.
The solution might be to start using RequestBodyAdvice feature available since Spring MVC.
Let's declare our Advice to MVC configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Configuration @EnableWebMvc class MvcConfig extends WebMvcConfigurationSupport { @Resource private RequestBodyAdvice escapeRequestBodyAdvice; @Override protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { // we can avoid adding new custom since we can use RequestBodyAdvices ... } @Override public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter requestMappingHandlerAdapter = super.requestMappingHandlerAdapter(); // here is the our custom one: escapeRequestBodyAdvice requestMappingHandlerAdapter.setRequestBodyAdvice(Collections.singletonList(escapeRequestBodyAdvice)); return requestMappingHandlerAdapter; } } |
and our RequestBodyAdvice looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Component public class EscapeRequestBodyAdvice extends RequestBodyAdviceAdapter { @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { EscapeBeanUtil.escape(body); return body; } } |
where EscapeBeanUtil is the utility to escape the fields. It is not recursive - just for the demo purposes. It searches for @EscapeHTML and escapes the annotated field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import org.apache.commons.lang3.StringEscapeUtils; import org.springframework.util.ReflectionUtils; class EscapeBeanUtil { /** * Only one level of simple String fields supported * * @param bean */ static void escape(Object bean) { ReflectionUtils.doWithFields( bean.getClass(), field -> { if (!field.isAccessible()) { field.setAccessible(true); } Object value = ReflectionUtils.getField(field, bean); if (value != null) { String escaped = StringEscapeUtils.escapeHtml4( String.valueOf(value) ); ReflectionUtils.setField(field, bean, escaped); } }, field -> field.getAnnotationsByType(EscapeHTML.class).length > 0 && String.class.isAssignableFrom(field.getType()) ); } } |
Annotation:
1 2 3 4 5 6 7 8 9 10 | import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface EscapeHTML { } |
Now, after these changes we have our POJO/DTO a bit modified (See at @EscapeHTML)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotBlank; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import javax.validation.Valid; @Controller public class ReportController { @GetMapping(value = "/content") void report(@RequestBody @Valid ContentRequest contentRequest) { // process it } @JsonTypeName("content") static class ContentRequest { @JsonProperty("id") // other fields which require validation... long id; @JsonProperty("title") @Length(max = 100000, message = "{validation.max.length.exceeds}") @NotBlank(message = "{validation.not.empty}") @EscapeHTML String content; } } |
That's it!
Also please note that now validation is broken a bit: after HTML escaping limits for the content length could not fit since more chars are added. To overcome it you can pre-calculate the field size on UI or relax a bit the size or add some custom validator for that field. But very often for the big text inputs where the escape is required there are no limits