4

I want to choose a response media type on run time in a method.

For example, the following code:

@RequestMapping(value = "/getRecord",
    produces = {"application/octet-stream", "application/json;charset=UTF-8" })
public byte[] getData(
    @RequestParam(value="id", required=true) Integer id)
    throws IOException
{
    if (id == 1)
        return createByteArray();
    throw new MyDataException();
}

In this code, the kind of the possible response types are actually 2.

  1. byte[] (by the normal execution path)
  2. MyDataException (by the exception execution path)

MyDataException is later handled by an exception handler, and converted to a simple class. It can be converted to a json response.

First, I thought that if I provide 2 response types for produces option of the @RequestMapping annotation, the message converter would convert the 2 types according to the actual return object. But it was not the case.

In the spring class org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor, writeWithMessageConverters() method just ignores the actual return object type when selecting the response type if the produces option is present.

How can I let Spring to choose the response type on run time based on the actual return object?

zeodtr
  • 10,645
  • 14
  • 43
  • 60

3 Answers3

1

Self-answer.

  • Remove produces.
  • Change return type to ResponseEntity<byte[]>.
  • Return as follows:

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    return new ResponseEntity<byte[]>(createByteArray(), responseHeaders, HttpStatus.OK);
    

As a result, the code on the question is transformed as follows:

@RequestMapping(value = "/getRecord")
public ResponseEntity<byte[]> getData(@RequestParam(value="id", required=true) Integer id)
    throws IOException
{
    if (id == 1)
    {
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        return new ResponseEntity<byte[]>(createByteArray(), responseHeaders, HttpStatus.OK);
    }
    throw new MyDataException();
}

Now the response types will be as follows:

  • On normal execution path, appliaction/octet-stream.
  • ON exception execution path, application/json.

I referenced a StackOverflow answer https://stackoverflow.com/a/4483387/3004042 for this. Also see the answer by Kumar Sambhav for setting up the exception handler.

If no better answer is posted in a few days, I'll choose this answer.

Community
  • 1
  • 1
zeodtr
  • 10,645
  • 14
  • 43
  • 60
1

I would suggest you to use @ControllerAdvice annotation to handle exception in in your Spring MVC handler. It's very elegant ways (there are actually 3 ways you can move out your exception handling concerns) of separating error handling concerns like setting appropriate HTTP response code (something other than 2xx) and sending back error message /object.

There is an excellent blog here.

Examples (borrowed from a Spring blog):-

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

In you case, I would suggest to go for @ControllerAdvice approach like:-

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ResponseBody
    @ExceptionHandler(MyDataException.class)
    public AnyReturnType handleConflict(Exception exception) {
         return exception.getDetails();
    }
}

The return type of the handler can also be a ModelAndView object which will pass of the error object to your view layer.

Refer blog for more details.

Kumar Sambhav
  • 7,503
  • 15
  • 63
  • 86
  • Actually I use `@ControllerAdvice` for MyDataException. The problem is that Spring does not respect the exception's object type when the response conversion is done. It just tries to convert it to application/octet-stream (which is the first type I've specified for `produces`) and fails, while I want it to be converted to application/json. – zeodtr May 14 '15 at 04:59
  • That should not be the case. I assume yours are RESTful controller (JSON responses, no view layer). If thats the case then use @ RestController instead of @ Controller so that you don't have to mention 'produces' explicitly. Putting the @ ResponseBody annotation on the handleConflict should make it work. Refer mentioned blog to have complete picture. – Kumar Sambhav May 14 '15 at 05:05
  • My controller is RESTful. But it returns application/octet-stream body in a normal case. And if an exception is thrown, it should return application/json body (which contains the error information) with HTTP 400 or 500 via `@ControllerAdvice` exception handler. – zeodtr May 14 '15 at 05:11
  • I already use @ResponseBody. In fact, my ExceptionHandler class' code is almost identical to your GlobalControllerExceptionHandler. (Except for the ResponseStatus is HttpStatus.BAD_REQUEST) Anyway I'll read the blog post you provided. Thanks. – zeodtr May 14 '15 at 05:26
  • The blog has virtually no further information than you provided, I think. I will stick to my answer for now. – zeodtr May 14 '15 at 10:57
0

Another possibility would be a mixed approach between the two described solutions:

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler({ MyDataException.class })
protected ResponseEntity<Object> handleInvalidRequest(RuntimeException e, WebRequest request) {
    MyDataExceptionire = (MyDataException) e;
    List<FieldErrorResource> fieldErrorResources = new ArrayList<>();

    List<FieldError> fieldErrors = ire.getErrors().getFieldErrors();
    for (FieldError fieldError : fieldErrors) {
        FieldErrorResource fieldErrorResource = new FieldErrorResource();
        fieldErrorResource.setResource(fieldError.getObjectName());
        fieldErrorResource.setField(fieldError.getField());
        fieldErrorResource.setCode(fieldError.getCode());
        fieldErrorResource.setMessage(fieldError.getDefaultMessage());
        fieldErrorResources.add(fieldErrorResource);
    }

    ErrorResource error = new ErrorResource("MyDataException", ire.getMessage());
    error.setFieldErrors(fieldErrorResources);

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);

    return handleExceptionInternal(e, error, headers, HttpStatus.UNPROCESSABLE_ENTITY, request);
}}

Solution proposed on this blog

EDIT:

I've added also the FieldError and ErrorResource classes from the blog since it might get deleted in the future:

ErrorResource:

@JsonIgnoreProperties(ignoreUnknown = true)
public class ErrorResource {
private String code;
private String message;
private List<FieldErrorResource> fieldErrors;

public ErrorResource() { }

public ErrorResource(String code, String message) {
    this.code = code;
    this.message = message;
}

public String getCode() { return code; }

public void setCode(String code) { this.code = code; }

public String getMessage() { return message; }

public void setMessage(String message) { this.message = message; }

public List<FieldErrorResource> getFieldErrors() { return fieldErrors; }

public void setFieldErrors(List<FieldErrorResource> fieldErrors) {
    this.fieldErrors = fieldErrors;
}
}

FieldErrorResource:

@JsonIgnoreProperties(ignoreUnknown = true)
public class FieldErrorResource {
private String resource;
private String field;
private String code;
private String message;

public String getResource() { return resource; }

public void setResource(String resource) { this.resource = resource; }

public String getField() { return field; }

public void setField(String field) { this.field = field; }

public String getCode() { return code; }

public void setCode(String code) { this.code = code; }

public String getMessage() { return message; }

public void setMessage(String message) { this.message = message; }}
Bogdan Emil Mariesan
  • 5,529
  • 2
  • 33
  • 57
  • Interesting. This is the reverse of my answer. While I used ResponseEntity for the normal flow, this uses it for the exception flow. Either approach can be selected based on the number of the exception handlers and the number of the request handlers. BTW in this solution's case, `produces="application/octet-stream"` option must be added to the request handlers that returns application/octet-stream. – zeodtr May 14 '15 at 11:28
  • @zeodtr yes you are right :), this example uses json as return type but you can chose any MediaType. Usually I like to have such logic in the ControllerAdvice rather than catching it in the Controller. – Bogdan Emil Mariesan May 14 '15 at 11:47
  • I choose this answer since it's more appropriate to put the special code (ResourceEntity related) to the exception handlers, rather than to the controllers. But maybe the example is somewhat too complex. In that case, my Self-answer can be referenced for handling ResourceEntity. – zeodtr May 22 '15 at 00:05