28

I've built a json REST service with controllers like this one:

@Controller
@RequestMapping(value = "/scripts")
public class ScriptController {

    @Autowired
    private ScriptService scriptService;

    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public List<Script> get() {
        return scriptService.getScripts();
    }
}

It works fine, but now I need to modify all responses and add "status" and "message" fields to all of them. I've read about some solutions:

  1. return from all controller methods object of some specific class, for example, RestResponse, which will contain "status" and "message" fields (but it's not general solution, cause I will have to modify all my controllers and write new controllers in new style)
  2. intercept all controller methods with aspects (but in this case I can't change return type)

Can you suggest some other, general and correct solution, if I want to wrap values returned from controller methods into objects of class:

public class RestResponse {

    private int status;
    private String message;
    private Object data;

    public RestResponse(int status, String message, Object data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }

    //getters and setters
}
Vitalii Ivanov
  • 732
  • 1
  • 7
  • 19
  • 1
    I don't know exact name of class, but there should exist response handler class in spring restful like ResponseHandler class in Wink implemenation of restful. You can set response entity explictily here after modifying the response. hope it tells you where to look at atleast though its not proper answer. something like this in MVC http://www.mkyong.com/spring-mvc/spring-mvc-handler-interceptors-example/ – Jimmy Jul 29 '14 at 16:41
  • 2
    It's probably best to rewrite the handlers. It's the most clean and less magical solution to your problem. Unfortunately it doesn't always pay of to be lazy. You will need to rewrite the clients as well since they will receive data in the new format. In my opinion option 1 is the better approach to future proof your service. – Bart Jul 29 '14 at 17:10
  • possible duplicate of [In Spring MVC, how can I set the mime type header when using @ResponseBody](http://stackoverflow.com/questions/4471584/in-spring-mvc-how-can-i-set-the-mime-type-header-when-using-responsebody) – Serge Ballesta Jul 29 '14 at 17:48
  • 1
    @SergeBallesta Those questions you deem duplicates have nothing to do with this question. – Bart Jul 29 '14 at 21:06
  • @Bart The question is different, but what do you think of the answers (specifically the accepted one) ? – Serge Ballesta Jul 29 '14 at 21:20
  • @SergeBallesta I can see what you mean. It's still a long stretch calling it a duplicate though :) Using a `ResponseEntity` would be the correct return type. – Bart Jul 29 '14 at 21:25
  • @Bart All considered, you are right ... – Serge Ballesta Jul 29 '14 at 21:47
  • @SergeBallesta I think use ResponseEntity has the same weaknesses as the solution with usage of custom class RestResponse (rewrite all methods, etc.), that I've mentioned in answer – Vitalii Ivanov Jul 30 '14 at 11:51
  • @Bart It's not a problem with clients. In Backbone.js you just can override one method, that parses responses from server. – Vitalii Ivanov Jul 30 '14 at 11:54
  • 1
    @Jimmy Thanks, now I am working on solutions with spring interceptors and servlet filters. – Vitalii Ivanov Jul 30 '14 at 11:55
  • Which solution did you implement? I think that it might be possible to use [ResponseBodyAdvice](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.html) but I can't get it work if the method returns a String (StringHttpMessageConverter is used instead of my ResponseBody) – Clemzd Feb 05 '16 at 08:38

2 Answers2

30

I've encountered with similar problem and suggest you to use Servlet Filters to resolve it.

Servlet Filters are Java classes that can be used in Servlet Programming to intercept requests from a client before they access a resource at back end or to manipulate responses from server before they are sent back to the client.

Your filter must implement the javax.servlet.Filter interface and override three methods:

public void doFilter (ServletRequest, ServletResponse, FilterChain)

This method is called every time a request/response pair is passed through the chain due to a client request for a resource at the end of the chain.

public void init(FilterConfig filterConfig)

Called before the filter goes into service, and sets the filter's configuration object.

public void destroy()

Called after the filter has been taken out of service.

There is possibility to use any number of filters, and the order of execution will be the same as the order in which they are defined in the web.xml.

web.xml:

...
<filter>
    <filter-name>restResponseFilter</filter-name>
    <filter-class>
        com.package.filters.ResponseFilter
    </filter-class>
</filter>

<filter>
    <filter-name>anotherFilter</filter-name>
    <filter-class>
        com.package.filters.AnotherFilter
    </filter-class>
</filter>
...

So, this filter gets the controller response, converts it into String, adds as feild to your RestResponse class object (with status and message fields), serializes it object into Json and sends the complete response to the client.

ResponseFilter class:

public final class ResponseFilter implements Filter {

@Override
    public void init(FilterConfig filterConfig) {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response,
                     FilterChain chain) throws IOException, ServletException {

    ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);

    chain.doFilter(request, responseWrapper);

    String responseContent = new String(responseWrapper.getDataStream());

    RestResponse fullResponse = new RestResponse(/*status*/, /*message*/,responseContent);

    byte[] responseToSend = restResponseBytes(fullResponse);

    response.getOutputStream().write(responseToSend);

}

@Override
public void destroy() {
}

private byte[] restResponseBytes(RestResponse response) throws IOException {
    String serialized = new ObjectMapper().writeValueAsString(response);
    return serialized.getBytes();
}
}

chain.doFilter(request, responseWrapper) method invokes the next filter in the chain, or if the calling filter is the last filter in the chain invokes servlet logic.

The HTTP servlet response wrapper uses a custom servlet output stream that lets the wrapper manipulate the response data after the servlet is finished writing it out. Normally, this cannot be done after the servlet output stream has been closed (essentially, after the servlet has committed it). That is the reason for implementing a filter-specific extension to the ServletOutputStream class.

FilterServletOutputStream class:

public class FilterServletOutputStream extends ServletOutputStream {

DataOutputStream output;
public FilterServletOutputStream(OutputStream output) {
    this.output = new DataOutputStream(output);
}

@Override
public void write(int arg0) throws IOException {
    output.write(arg0);
}

@Override
public void write(byte[] arg0, int arg1, int arg2) throws IOException {
    output.write(arg0, arg1, arg2);
}

@Override
public void write(byte[] arg0) throws IOException {
    output.write(arg0);
}
}

To use the FilterServletOutputStream class should be implemented a class that can act as a response object. This wrapper object is sent back to the client in place of the original response generated by the servlet.

ResponseWrapper class:

public class ResponseWrapper extends HttpServletResponseWrapper {

ByteArrayOutputStream output;
FilterServletOutputStream filterOutput;
HttpResponseStatus status = HttpResponseStatus.OK;

public ResponseWrapper(HttpServletResponse response) {
    super(response);
    output = new ByteArrayOutputStream();
}

@Override
public ServletOutputStream getOutputStream() throws IOException {
    if (filterOutput == null) {
        filterOutput = new FilterServletOutputStream(output);
    }
    return filterOutput;
}

public byte[] getDataStream() {
    return output.toByteArray();
}
}

I think this approach will be a good solution for your issue.

Please, ask a questions, if something not clear and correct me if I'm wrong.

Alexander
  • 530
  • 5
  • 13
29

If you use spring 4.1 or above, you can use ResponseBodyAdvice to customizing response before the body is written.

Michal Foksa
  • 11,225
  • 9
  • 50
  • 68
vr3C
  • 1,734
  • 19
  • 16
  • 3
    This probably isn't the right answer. Advice is only executed if call reaches controller. Filter chains are executed first and if an exception occurs like AuthException then advice will not be applied. – Adi Aug 26 '18 at 21:59