For all my RestControllers handlers, which all return a subtype of MyResponseType, I would like to write a Spring ResponseBodyAdvice which manipulates the response based on some #ModelAttribute. However, I don't know how to access the ModelView at this place.
#ControllerAdvice
public class MyGoldenAdvice implements ResponseBodyAdvice<MyResponseType> {
#Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public RepresentationModel<?> beforeBodyWrite(
final MyResponseType body,
final MethodParameter returnType,
final MediaType selectedContentType,
final Class<? extends HttpMessageConverter<?>> selectedConverterType,
final ServerHttpRequest request,
final ServerHttpResponse response) {
// here I would like to access the ModelView
return body;
}
}
Unfortunately, since I have to implement the interface ResponseBodyAdvice, I cannot have #ModelAttribute arguments.
Of course, I could also use an interceptor. However, there the response is already written, thus I could only manipulate the JSON of my MyResponseType instance.
Any idea?
Although it is not a solution to the concrete question (Access zu #ModelAttributes) and violates the idea behind #ControllerAdvice, you can use #RequestAttributes for the same purpose. For this, the advice needs a dependency to current HttpServletRequest to store and get the attribute (the ServerHttpRequest argument of beforeBodyWrite does not provide request attributes).
public class MyGoldenAdvice implements ResponseBodyAdvice<MyResponseType> {
public final static String ATTRIBUTE_NAME="MyGoldenAdviceAttribute";
private final HttpServletRequest currentRequest;
// spring injects a proxy to the current request
#Autowired
public MyGoldenAdvice(HttpServletRequest currentRequest) {
this.currentRequest = currentRequest;
}
// in order that the advice method is triggered, we have to annotate it
// with #ModelAttribute, although we store the attribute in the request
#ModelAttribute
public MyModelAttributeType createModelAttribute() {
MyModelAttributeType attribute = MyModelAttributeType.build();
currentRequest.setAttribute(ATTRIBUTE_NAME, attribute);
return attribute;
}
#Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public RepresentationModel<?> beforeBodyWrite(
final MyResponseType body,
final MethodParameter returnType,
final MediaType selectedContentType,
final Class<? extends HttpMessageConverter<?>> selectedConverterType,
final ServerHttpRequest request,
final ServerHttpResponse response) {
// get the request attribute
MyModelAttributeType attribute = (MyModelAttributeType) currentRequest.getAttribute(ATTRIBUTE_NAME);
// do something with the body
return body;
}
}
In some REST controller, you can use the request attribute as follows
#GetMapping
public handleGetRequest(#RequestAttribute(name=MyGoldenAdvice.ATTRIBUTE_NAME)
MyModelAttributeType attribute) {
// pass attribute around in services and so on...
}
Related
I would like to wrap the response of my #RestController method into different object structure before Jackson starts to serialize the response to JSON. Let's say I work with the following Spring controller.
#RestController
#RequestMapping("/api/susu")
public class SusuController {
#RequestMapping(path = "/{id}", method = RequestMethod.GET)
public Susu hello(String id) {
Susu susu = new Susu();
susu.setDate(LocalDate.now());
susu.setName("Peter Pan");
return susu;
}
}
In JEE7 I used JAX-RS Interceptor the get access to the Susu instance and wrap it.
#Provider
#Priority(1)
public class JsonStructureInterceptor implements WriterInterceptor {
private final JsonResponseBuilder jsonResponseBuilder = new JsonResponseBuilder();
#Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
Susu s = (Susu) context.getEntity(); // read the JAX-RS response entity
JsonObject jsonObjectWithStructure = jsonResponseBuilder.toResponse(s); // wrap it
// add it back into the JAX-RS context
context.setEntity(jsonObjectWithStructure);
context.proceed();
}
}
When using Spring Boot what is the preferred way to to something equivalent without using JAX-RS features?
Update 1: Using a HandlerInterceptorAdapter
I added the following HandlerInterceptorAdapter to my application context and the postHandle method gets called. Everything works fine so far but I cannot figure out how to get the Susu instance and how to pass the wrapped instance over for further processing.
#Component
public class SusuHandlerInterceptor extends HandlerInterceptorAdapter {
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Susu s = ; // how to get access to my Susu instance?
Wrapper w = new Wrapper(s);
// how to pass Wrapper instance on?
}
}
Update 2: Implementing a ResponseBodyAdvice
I found another approach that allows me to access the return value of my controller action. The problem here is that I cannot change the type of the return value. It seems it is not possible to wrap Susu instance in a Wrapper instance.
#ControllerAdvice
public class JsonFilter implements ResponseBodyAdvice<SusuController.Susu> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public SusuController.Susu beforeBodyWrite(SusuController.Susu body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return body;
}
}
Implementing a ResponseBodyAdvice lets you modify the object before it's converted.
If the return-type should be modified, one has to omit the Generic-types:
#ControllerAdvice
class JsonModifyingAdvice implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return new WrappedResponse(body);
}
}
I am using Spring Boot 1.4.1 which includes spring-web-4.3.3. I have a class annotated with #ControllerAdvice and methods annotated with #ExceptionHandler to handle exceptions thrown by the service code. When handling these exceptions, I would like to log the #RequestBody that was part of the request for PUT and POST operations so I can see the request body that caused the problem which in my case is crucial for diagnosis.
Per Spring Docs the method signature for #ExceptionHandler methods can include various things including the HttpServletRequest. The request body can normally be obtained from here via getInputStream() or getReader(), but if my controller methods parse the request body like "#RequestBody Foo fooBody" as all of mine do, the HttpServletRequest's input stream or reader is already closed by the time my exception handler method is called. Essentially the request body has already been read by Spring, similar to the issue described here. It is a common problem working with servlets that the request body can only be read once.
Unfortunately #RequestBody is not one of the options available for the exception handler method, if it were then I could use that.
I can add an InputStream to the exception handler method, but that ends up being the same thing as the HttpServletRequest's InputStream and so has the same issue.
I also tried getting the current request with ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest() which is another trick for getting the current request, but this ends up being the same HttpServletRequest that Spring passes into the exception handler method and so has the same problem.
I have read about a few solutions like this and this that involve inserting a custom request wrapper in the filter chain that will read the contents of the request and cache them so they can be read more than once. I don't like this solution because I don't want to interrupt the entire filter/request/response chain (and potentially introduce performance or stability problems) just to implement logging, and if I have any large requests such as uploaded documents (which I do), I don't want to cache that in memory. Besides, Spring probably has the #RequestBody cached somewhere already if I could only find it.
Incidentally many solutions recommend using the ContentCachingRequestWrapper Spring class but in my experience this does not work. Aside from not being documented, looking at its source code it looks like it only caches the parameters, but not the request body. Trying to get the request body from this class always results in an empty string.
So I am looking for any other options that I may have missed. thanks for reading.
Accepted answer creates a new POJO to pass things around, but the same behaviour can be achieved without creating additional objects by reusing the http request.
Example code for Controller mapping:
public ResponseEntity savePerson(#RequestBody Person person, WebRequest webRequest) {
webRequest.setAttribute("person", person, RequestAttributes.SCOPE_REQUEST);
And later in the ExceptionHandler class / method you can use:
#ExceptionHandler(Exception.class)
public ResponseEntity exceptionHandling(WebRequest request,Exception thrown) {
Person person = (Person) request.getAttribute("person", RequestAttributes.SCOPE_REQUEST);
You can reference the request body object to a request-scoped bean. And then inject that request-scoped bean in your exception handler to retrieve the request body (or other request-context beans that you wish to reference).
// #Component
// #Scope("request")
#ManagedBean
#RequestScope
public class RequestContext {
// fields, getters, and setters for request-scoped beans
}
#RestController
#RequestMapping("/api/v1/persons")
public class PersonController {
#Inject
private RequestContext requestContext;
#Inject
private PersonService personService;
#PostMapping
public Person savePerson(#RequestBody Person person) throws PersonServiceException {
requestContext.setRequestBody(person);
return personService.save(person);
}
}
#ControllerAdvice
public class ExceptionMapper {
#Inject
private RequestContext requestContext;
#ExceptionHandler(PersonServiceException.class)
protected ResponseEntity<?> onPersonServiceException(PersonServiceException exception) {
Object requestBody = requestContext.getRequestBody();
// ...
return responseEntity;
}
}
You should be able to get the content of the request body by using the RequestBodyAdvice interface. If you implement this on a class annotated with #ControllerAdvice it should be picked up automatically.
To get other request information like the HTTP method and query params I'm using an interceptor. I'm capturing all this request info for error reporting in a ThreadLocal variable which I clear on the afterCompletion hook in that same interceptor.
The class below implements this and can be used in your ExceptionHandler to get all request information:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
#ControllerAdvice
public class RequestInfo extends HandlerInterceptorAdapter implements RequestBodyAdvice {
private static final Logger logger = LoggerFactory.getLogger(RequestInfo.class);
private static final ThreadLocal<RequestInfo> requestInfoThreadLocal = new ThreadLocal<>();
private String method;
private String body;
private String queryString;
private String ip;
private String user;
private String referrer;
private String url;
public static RequestInfo get() {
RequestInfo requestInfo = requestInfoThreadLocal.get();
if (requestInfo == null) {
requestInfo = new RequestInfo();
requestInfoThreadLocal.set(requestInfo);
}
return requestInfo;
}
public Map<String,String> asMap() {
Map<String,String> map = new HashMap<>();
map.put("method", this.method);
map.put("url", this.url);
map.put("queryParams", this.queryString);
map.put("body", this.body);
map.put("ip", this.ip);
map.put("referrer", this.referrer);
map.put("user", this.user);
return map;
}
private void setInfoFromRequest(HttpServletRequest request) {
this.method = request.getMethod();
this.queryString = request.getQueryString();
this.ip = request.getRemoteAddr();
this.referrer = request.getRemoteHost();
this.url = request.getRequestURI();
if (request.getUserPrincipal() != null) {
this.user = request.getUserPrincipal().getName();
}
}
public void setBody(String body) {
this.body = body;
}
private static void setInfoFrom(HttpServletRequest request) {
RequestInfo requestInfo = requestInfoThreadLocal.get();
if (requestInfo == null) {
requestInfo = new RequestInfo();
}
requestInfo.setInfoFromRequest(request);
requestInfoThreadLocal.set(requestInfo);
}
private static void clear() {
requestInfoThreadLocal.remove();
}
private static void setBodyInThreadLocal(String body) {
RequestInfo requestInfo = get();
requestInfo.setBody(body);
setRequestInfo(requestInfo);
}
private static void setRequestInfo(RequestInfo requestInfo) {
requestInfoThreadLocal.set(requestInfo);
}
// Implementation of HandlerInterceptorAdapter to capture the request info (except body) and be able to add it to the report in case of an error
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
RequestInfo.setInfoFrom(request);
return true;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
RequestInfo.clear();
}
// Implementation of RequestBodyAdvice to capture the request body and be able to add it to the report in case of an error
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return inputMessage;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
RequestInfo.setBodyInThreadLocal(body.toString());
return body;
}
#Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
Just an enhancement to quintencls answer
I got request body and can use it anywhere inside exception handler class.
#ControllerAdvice
public class CustomErrorHandler extends ResponseEntityExceptionHandler implements RequestBodyAdvice {
...
private Object reqBody;
...
#ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<Object> handleNoSuchElementException(final NoSuchElementException ex,
final WebRequest request) {
System.out.println("===================================" + reqBody);
return handleNotFoundException(ex, request);
}
...
#Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// capture request body here to use in our controller advice class
this.reqBody = body;
return body;
}
#Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
Here is a solution in Kotlin syntax that I used for some fields validation control.
I needed to enhance the default handleMethodArgumentNotValid(...) method from the #RestControllerAdvice, to systematically log a field that was embedded in that same request body object.
override fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatus, request: WebRequest): ResponseEntity<Any> {
val error = e.bindingResult.fieldErrors.first()
val requestBody = try {
val field = error.javaClass.getDeclaredField("violation").apply { trySetAccessible() }
((field[error] as ConstraintViolationImpl<Any>).rootBean as MyRequestBodyObject)
} catch (ex : Exception) {
//do some failsafe here
}
}
See here: https://stackoverflow.com/a/61813076/1036433 - for a clean way to have access to the HttpServerRequest.
How can I achive that the #ResponseBody (in my case a class of type SomePojoInterface) is automatically validated (lets say through JSR-303 validation). Nice to have would be, that in case of a validation-failure the handler would throw an Exception which can be handled in some #ControllerAdvice annotated class.
My code so far.
#RestController
public class MyRestController {
#GetMapping(value = "validate", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
protected SomePojo validateResponse() {
return new SomePojo();
}
}
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.XXX)
#ExceptionHandler(MyResponseValidationException.class)
public void handleResponseValidationException() {
// ...
}
}
public class SomePojo implements SomePojoInterface {
#NotNull
private String someValue;
// getter / setter
}
If you have annotated your class SomePojo, then:
#GetMapping(value = "validate", produces = MediaType.APPLICATION_JSON_VALUE)
protected SomePojo validateResponse() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
SomePojo somePojo = new SomePojo(null);
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(somePojo);
// Other stuff
}
#Valid annotation is for request. More examples from their docs. I am not sure what all you want to validate
I managed to achieve this through the #RestControllerAdvice.
#RestControllerAdvice
public class RestPostProcessingAdvice implements ResponseBodyAdvice<SomePojoInterface> {
#Inject
private Validator validator;
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if(doSomeChecksIfEligiable(returnType, converterType)) {
return true;
}
return false;
}
#Override
public SomePojoInterface beforeBodyWrite(SomePojoInterface body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(body);
if(constraintViolations.size() > 0) {
response.setStatusCode(HttpStatus.XXX);
LOG.fatal("Sorry, I'm sending crap");
}
return body;
}
}
Be aware that throwing an Exception and catching it in an #ExceptionHandler that is sending the same (mofified) object out in the #ResponseBody could lead to an endless loop, since the object will be checked again this #RestControllerAdvice.
For example, here's a method which returns a User:
#RequestMapping(method = GET, value = "/user")
public User getUser() {
return new Users();
}
For some reasons, the client expect an other type
class CommonResponse<T> {
int code;
T data;
}
So I need to convert all return value from T(User for this e.g.) to CommonResponse<T> before it handled by the MessageConverter.
Cause there're many request hanlders should be modified, is there any way to write the convert data just once?
Finally I find ResponseBodyAdvice to do such work.
Here the sample code:
#RestControllerAdvice
public class CommonAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getDeclaringClass().getPackage().getName().startsWith("foo.bar.demo");
// you can change here to your logic
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return new CommonResponse<Object>().setCode(200).setData(body);
}
}
you need to add/configure your custom converter. so that your custom converter is executed before others
#EnableWebMvc
#Configuration
#ComponentScan({ "org.app.web" })
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
messageConverters.add(createCustomConverter());
super.configureMessageConverters(converters);
}
private HttpMessageConverter<Object> createCustomConverter() {
....
}
}
I want all json response is
{
"status":"ok"
"data":"..."
}
I only care #ResponseBody function return value;Don't need wrap any object to do it;
example:
#ResponseBody
public String test(){
return "Hello,World"
}
I want get
{
"status":"ok"
"data":"Hello,World"
}
ResponseBodyAdvicecan do it.you can implement the interface to handle ReqeustBody. Here is the api.
In the documents.
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 registered directly with RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or more likely annotated with #ControllerAdvice in which case they will be auto-detected by both.
here is a simple code.
#ControllerAdvice
public customerResponseBody implements ResponseBodyAdvice{
#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){
body = new ResponseTemplate<Object>("001",body);
return body;
}
}
You must return a Object instead of String for example :
public class CustomResponse {
private String status;
private String data;
// Getters & Setters
}
#ResponseBody
public CustomResponse test(){
CustomResponse response = new CustomResponse();
response.setStatus("OK");
response.setData("Hello,World");
return response;
}
I thought it is not a good solution but you can format string
#RequestMapping(value="testData")
public #ResponseBody String testData(){
String sdata="ok";
String value ="Hello,World";
return "{\"status\" :\""+sdata+"\",\"data \":\""+value+"\"}";
}
I found this solve problem.
rewrite default RequestResponseBodyMethodProcessor can wrap any object to return value