Combine functionality of #ModelAttribute and #RequestBody in Spring MVC - java

Consider a #PostMapping in Spring MVC, and we want to map the request body to a DTO, as well as other request parameters like query and path variables.
For mapping the request body we can use the #RequestBody annotation on a parameter, which will tell Spring to use the RequestResponseBodyMethodProcessor.
For mapping the request parameters we can use the #ModelAttribute annotation (or avoid any annotations; same effect), which will tell Spring to use the ServletModelAttributeMethodProcessor.
But is there an easy way to combine these two? Is there a way to make spring first map the DTO with request parameters and then override with data deserialized from the JSON in the body?
The only way I see it at the moment is to create a custom HandlerMethodArgumentResolver but I'd like to reuse any existing Spring functionality first.

Turns out no, not at time of writing anyway.
RequestMappingHandlerAdapter.getDefaultArgumentResolvers holds the default configuration for the argument resovlers, and we have:
RequestResponseBodyMethodProcessor handling the #RequestBody annotation, and
ServletModelAttributeMethodProcessor handling the #ModelAttribute, or any parameter without any annotations.
It's always just one single resolver that gets executed, even if you try using a composite.
My solution
#ControllerAdvice
#RequiredArgsConstructor
#Order(HIGHEST_PRECEDENCE)
public class ServletRequestBinderRequestBodyAdvice extends RequestBodyAdviceAdapter {
private final ServletRequest servletRequest;
#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) {
copyDefaultPropertiesThatWhereOverwritenWithNull(parameter, body);
new ExtendedServletRequestDataBinder(body).bind(servletRequest);
return body;
}
private void copyDefaultPropertiesThatWhereOverwritenWithNull(MethodParameter parameter, Object arg) {
Object argWithDefaults = instantiateClass(parameter.getParameterType());
copyPropertiesSkippingNulls(argWithDefaults, arg);
}
}

Related

Spring Boot: Maunally bind an object from a webRequest using HandlerMethodArgumentResolver

I need to build an object before it even reaches the controller and one way I found to do that is by using HandlerMethodArgumentResolver.
Basically, I have a pojo which gets mapped to the request parameters, but I want to set some other fields in that pojo before it reaches the controller.
POJO: UserParams.java
#AllArgsConstructor
public class UserParams {
private String firstName;
private String lastName;
private String sessionId;
}
Let's say my request comes in as localhost:8080/user?firstName=John&lastName=Doe
So, in my resolver I want to bind the UserParams object using the request params from the above request and populate sessionId field and return the bound object with additional value.
#Component
public class UserParamsResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(UserParams.class);
}
#Override
public Object resolveArgument(final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) {
WebDataBinder binder = new WebDataBinder(BeanUtils.instantiateClass(parameter.getParameterType()));
ServletRequestParameterPropertyValues values = new ServletRequestParameterPropertyValues(((ServletWebRequest) webRequest).getRequest());
binder.bind(values);
BindingResult result = binder.getBindingResult();
// UserParams userParams = how to get this object?
// userParams.setSessionId(userParams.getLastName + Math.random())
return userParams;
}
So, when the request eventually reaches the controller, I've the userParams with sessionId in it.
I tried looking at many places (programcreek.com had lot of examples for WebDataBinder) and tried to find out how Spring binds the objects with request params before coming to the Controller, but I had no luck.
One solution is to use spring AOP in order to process all #Controller methods having UserParams as a parameter. Then you have to get access to WebRequest in order to get sessionId and this is a little bit trickier; you'll have to create a #Bean #Scope("request") (e.g. name it WebRequestAccessor) which to contain an #Autowired field of type WebRequest. Autowire that WebRequestAccessor bean into your #Aspect in order to use its WebRequest field which then gives you access to sessionId. Set the sessionId on the UserParams parameter then let the advised method continue its work.
Take a look here about how to write & use an #Aspect.
UPDATE
You could use JSESSIONID instead of HttpSession.getId(). Just annotate private String sessionId with #CookieValue("JSESSIONID").
UPDATE 2
I'm pretty sure you should use ServletModelAttributeMethodProcessor (or ModelAttributeMethodProcessor) instead of HandlerMethodArgumentResolver.

How can I instantiate a specific sub-type for a #RequestBody parameter based on the requested URI for a Spring MVC controller method?

Given the following basic domain model:
abstract class BaseData { ... }
class DataA extends BaseData { ... }
class DataB extends BaseData { ... }
I want to write a Spring MVC controller endpoint thus ...
#PostMapping(path="/{typeOfData}", ...)
ResponseEntity<Void> postData(#RequestBody BaseData baseData) { ... }
The required concrete type of baseData can be inferred from the typeOfData in the path.
This allows me to have a single method that can handle multiple URLs with different body payloads. I would have a concrete type for each payload but I don't want to have to create multiple controller methods that all do the same thing (albeit each would do very little).
The challenge that I am facing is how to "inform" the deserialization process so that the correct concrete type is instantiated.
I can think of two ways to do this.
First use a custom HttpMessageConverter ...
#Bean
HttpMessageConverter httpMessageConverter() {
return new MappingJackson2HttpMessageConverter() {
#Override
public Object read(final Type type, final Class<?> contextClass, final HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// TODO How can I set this dynamically ?
final Type subType = DataA.class;
return super.read(subType, contextClass, inputMessage);
}
};
}
... which gives me the challenge to determine the subType based on the HttpInputMessage. Possibly I could use a Filter to set a custom header earlier when the URL is available to me, or I could use a ThreadLocal also set via a Filter. Neither sounds ideal to me.
My second approach would be to again use a Filter and this time wrap the incoming payload in an outer object which would then provide the type in a way that enables Jackson to do the work via #JsonTypeInfo. At the moment this is probably my preferred approach.
I have investigated HandlerMethodArgumentResolver but if I try to register a custom one it is registered AFTER the RequestResponseBodyMethodProcessor and that class takes priority.
Hmm, so after typing all of that out I had a quick check of something in the RequestResponseBodyMethodProcessor before posting the question and found another avenue to explore, which worked neatly.
Excuse the #Configuration / #RestController / WebMvcConfigurer mash-up and public fields, all for brevity. Here's what worked for me and achieved exactly what I wanted:
#Configuration
#RestController
#RequestMapping("/dummy")
public class DummyController implements WebMvcConfigurer {
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#interface BaseData {}
public static class AbstractBaseData {}
public static class DataA extends AbstractBaseData {
public String a;
}
public static class DataB extends AbstractBaseData {
public String b;
}
private final MappingJackson2HttpMessageConverter converter;
DummyController(final MappingJackson2HttpMessageConverter converter) {
this.converter = converter;
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(
new RequestResponseBodyMethodProcessor(Collections.singletonList(converter)) {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BaseData.class)
&& parameter.getParameterType() == AbstractBaseData.class;
}
#Override
protected <T> Object readWithMessageConverters(
NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
throws IOException, HttpMediaTypeNotSupportedException,
HttpMessageNotReadableException {
final String uri =
webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI();
return super.readWithMessageConverters(
webRequest, parameter, determineActualType(webRequest, uri));
}
private Type determineActualType(NativeWebRequest webRequest, String uri) {
if (uri.endsWith("data-a")) {
return DataA.class;
} else if (uri.endsWith("data-b")) {
return DataB.class;
}
throw new HttpMessageNotReadableException(
"Unable to determine actual type for request URI",
new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class)));
}
});
}
#PostMapping(
path = "/{type}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<? extends AbstractBaseData> post(#BaseData AbstractBaseData baseData) {
return ResponseEntity.ok(baseData);
}
}
The key to this is that I stopped using #RequestBody because that is what was preventing me overriding the built-in behaviour. By using #BaseData instead I get a HandlerMethodArgumentResolver that uniquely supports the parameter.
Other than that it was a case of assembling the two objects that already did what I needed, so autowire a MappingJackson2HttpMessageConverter and instantiate a RequestResponseBodyMethodProcessor with that one converter. Then pick the right method to override so that I could control what parameter type was used at a point that I had access to the URI.
Quick test. Given the following payload for both requests ...
{
"a": "A",
"b": "B"
}
POST http://localhost:8081/dummy/data-a
... gives a response of ...
{
"a": "A"
}
POST http://localhost:8081/dummy/data-b
... gives a response of ...
{
"b": "B"
}
In our real-world example this means that we will be able to write one method each that supports the POST / PUT. We need to build the objects and configure the validation possibly - or alternatively if we use OpenAPI 3.0 which we are investigating we could generate the model and validate without writing any further code ... but that's a separate task ;)

How to customize Bean Validations ConstraintViolationException?

I want to validate the #RequestBody of an endpoint in my Spring #RestController. So I created a method like this:
#RequestMapping(value = ...)
public ResponseEntity<...> myPostMethod(#RequestBody MyBean myBean) throws Exception {
MyBean is decorated with a custom #Constraint and its respective validation logic is implemented on a ConstraintValidator class that I created. This validator has a method like:
#Override
public boolean isValid(MyBean value, ConstraintValidatorContext context) {
That's where all the validation logic takes place. When it fails, isValid returns false and I can use that context param to build a validation error message the way I want. In addition, myPostMethod also fails with an automatic (because I do not throw it myself) MethodArgumentNotValidException that I'm going to capture on a global handler in order to render a generic ResponseEntity. It all works as expected. The question is: how do I customize not only the validation error message, but also the whole ConstraintViolationException? I want to provide more data (from my business domain) inside the exception to render in the response body json.
I found the answer:
https://docs.jboss.org/hibernate/validator/5.4/api/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorContext.html#withDynamicPayload-java.lang.Object-
public boolean isValid(MyBean value, ConstraintValidatorContext constraintValidatorContext) {
HibernateConstraintValidatorContext context = constraintValidatorContext.unwrap(HibernateConstraintValidatorContext.class);
// (...)
context.buildConstraintViolationWithTemplate( "{foo}" )
.withDynamicPayload(anyAdditionalInfo)
.addConstraintViolation();
return false;
}
Let's assume that the additional parameter you wanted to pass is called myParam.
First, declare an accessor for that parameter in your Constraint interface;
public #interface MyConstraint {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String myParam() default "some.value";
}
Then in the ConstraintValidator, you could access these params like:
public class MyValidator implements ConstraintValidator<MyConstraint, String> {
private String myParam;
#Override
public void initialize(OpcoConstraint parameters) {
code = parameters.myParam();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
HibernateConstraintValidatorContext hibernateContext = context.unwrap(
HibernateConstraintValidatorContext.class
);
// you can pass any object as the payload
hibernateContext.withDynamicPayload(Map.of("myParam", myParam));
}
}
If you catch the ConstraintViolationException from an exception handler and want to access the parameter from the Exception itself:
To retrieve ConstraintViolation (s) out of ConstraintViolationException use:
constraintViolationException.getConstraintViolations();
Then to retrieve the dynamic payload out of a ConstraintViolation:
(Map) ((ConstraintViolationImpl) constraintViolation).getDynamicPayload(Map.class))

How can I decorate the REST response in Spring (Boot)?

I've got a Spring Boot application that returns various objects that get encoded as JSON responses and I'd like to post-process them and add information to certain super classes.
Is there a way to filter, intercept, etc. the object responses from my REST endpoints before they get encoded to JSON with Jackson.
A filter won't work since it operates at the HttpServlet{Request,Response} level.
I guess ResponseBodyAdvice is your friend. Basically it:
Allows customizing the response after the execution of an
#ResponseBody or a ResponseEntity controller method but before the
body is written with an HttpMessageConverter.
Implementations may be may be registered directly with
RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or
more likely annotated with #ControllerAdvice in which case they will
be auto-detected by both.
Here i'm intercepting all the returned Strings and make them uppercase:
#ControllerAdvice
public class MyResponseBodyAdvisor implements ResponseBodyAdvice<String> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getParameterType().equals(String.class);
}
#Override
public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return body.toUpperCase();
}
}

Spring + Jackson: Wrapping Response Body in response object

This may be a strange question, although I wonder why it hasn't been asked or proposed before... so please correct me if any ignorance.
First off, I am using Jackson in conjunction with Spring and the #ResponseBody annotation.
Currently, for every request handler I am returning a "Response" wrapper object, as that is what the client expects. This wrapper is quite simple:
{ "response": { "data" : ACTUAL_DATA } }
Thing is, I'm not a fan of explicitly wrapping each return value for all my request handlers. I also do not like having to unwrap these response wrappers in my unit tests.
Rather, I wonder if it were possible to return the ACTUAL_DATA as it were, and to intercept and wrap this data elsewhere.
If this is in fact possible, would it then be possible to read the annotations attached to the intercepted request handler? This way I can use custom annotations to decide how to wrap the data.
For example something like this would be amazing (note that #FetchResponse and #ResponseWrapper are made up proposed annotations):
#RequestMapping(...)
#FetchResponse
#ResponseBody
public List<User> getUsers() {
...
}
#ResponseWrapper(FetchResponse.class)
public Object wrap(Object value) {
ResponseWrapper rw = new ResponseWrapper();
rw.setData(value);
return rw;
}
Anyone familiar with this territory? Or alternatively, and reasons why this might be bad practice?
Well, looks like I'm looking for Spring's "ResponseBodyAdvice" and "AbstractMappingJacksonResponseBodyAdvice".
For anybody looking on more info on this topic: I was facing the same issue as well, and thanks to the tip of kennyg, i managed to come up with the following solution:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
#ControllerAdvice
public class JSendAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof JSendResponse) {
return body;
}
return new JSendResponse<>().success(body);
}
}
This solution wraps all the objects returned in your controllers inside a (for this example) JSendResponse class, which saves you the hassle of returning JSendResponses in all of your controller methods.
I know it's been a while since the answer was accepted but I recently stumbled onto an issue with Jackson that allowed me to discover a problem with using ResponseBodyAdvice.
Jackson will not correctly serialize your polymorphic types that use #JsonTypeInfo / #JsonSubTypes if during runtime the values of your types are not known: i.e. for example if you have a generic container type like class ResponseWrapper<T> { List<T> objects; }. That is unless you provide Jackson with specialization of that generic type before you ask it to serialize your value, refer to Why does Jackson polymorphic serialization not work in lists? . Spring does this for you when you return say a list of T and that T is known because it's provided explicitly in the method return type (as in public List<MyEntity> getAllEntities();).
If you simply implement ResponseBodyAdvice and return a new, wrapped value from beforeBodyWrite() then Spring will no longer know your full generic type with its specialization, and it will serialize your response as ResponseWrapper<?> instead of ResponseWrapper<MyEntity>.
The only way around this is to both extend from AbstractJackson2HttpMessageConverter and override writeInternal(). See how the method treats the type here: https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java#L437
And you also need to implement a Controller advice using AbstractMappingJacksonResponseBodyAdvice and your own custom MappingJacksonValue that includes Type targetType that custom HttpMessageConverter will use.
ResponseWrapper
public class ResponseWrapper<T> {
#Nullable Error error;
T result;
public ResponseWrapper(T result) {
this.result = result;
}
}
WrappingAdvice
#Component
public class WrappingAdvice extends AbstractMappingJacksonResponseBodyAdvice {
#Override
protected MappingJacksonValue getOrCreateContainer(Object body) {
MappingJacksonValue cnt = super.getOrCreateContainer(body);
if (cnt instanceof MyMappingJacksonValue) {
return cnt;
}
return new MyMappingJacksonValue(cnt);
}
#Override
protected void beforeBodyWriteInternal(
MappingJacksonValue bodyContainer, MediaType contentType,
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
MyMappingJacksonValue cnt = (MyMappingJacksonValue) bodyContainer;
Type targetType = getTargetType(bodyContainer.getValue(), returnType);
cnt.setValue(new ResponseWrapper(cnt.getValue()));
cnt.setTargetType(TypeUtils.parameterize(
ResponseWrapper.class,
targetType));
}
/**
* This is derived from AbstractMessageConverterMethodProcessor
*/
private Type getTargetType(Object value, MethodParameter returnType) {
if (value instanceof CharSequence) {
return String.class;
}
Type genericType;
if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
genericType = ResolvableType.forType(returnType.getGenericParameterType()).getGeneric().getType();
} else {
genericType = returnType.getGenericParameterType();
}
return GenericTypeResolver.resolveType(genericType, returnType.getContainingClass());
}
public static class MyMappingJacksonValue extends MappingJacksonValue {
private Type targetType;
public MyMappingJacksonValue(MappingJacksonValue other) {
super(other.getValue());
setFilters(other.getFilters());
setSerializationView(other.getSerializationView());
}
public Type getTargetType() {
return targetType;
}
public void setTargetType(Type targetType) {
this.targetType = targetType;
}
}
}
JsonHttpMessageBodyConverter
#Component
public class JsonHttpMessageBodyConverter extends AbstractJackson2HttpMessageConverter {
// omitted all constructors
#Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof WrapAPIResponseAdvice.MyMappingJacksonValue) {
type = ((WrapAPIResponseAdvice.MyMappingJacksonValue) object).getTargetType();
}
super.writeInternal(object, type, outputMessage);
}
}

Categories