I encountered an error when trying to use the ResponseBodyAdvice
interface to customize the response after executing the controller method with @ResponseBody
annotation. The issue occurred when a controller method was returning a String
, resulting in the error 'org.example.MyHttpResponse cannot be cast to java.lang.String'.
The MyHttpResponse
is a custom entity used to return globally formatted data types.
1. Reproduce the Error
First, let’s see the code
MyTestController:
@RestController
@RequestMapping("/api/v1")
public class MyTestController {
@GetMapping("/")
public String hello() {
// Call service to get result
return "Hello, World!";
}
}
GlobalResponseBodyAdvice:
@RestControllerAdvice(basePackages = "org.example.controller")
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public MyHttpResponse<Object> globalExceptionHandler(Exception exception) {
System.out.println("System Internal Error: " + exception.getMessage());
return MyHttpResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR);
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
@SuppressWarnings("unchecked")
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 4xx error handling
if(body instanceof MyHttpResponse) {
return body;
}
// controller handling
if(body instanceof List) {
return MyHttpResponse.success((List<Object>) body);
}
return MyHttpResponse.success(body);
}
}
MyHttpResponse:
public class MyHttpResponse<T> {
private Integer status;
private String message;
private List<T> data;
public MyHttpResponse() {
}
public MyHttpResponse(Integer status, String message, List<T> data) {
this.status = status;
this.message = message;
this.data = data;
}
/**
* success response
* @param data data
* @return response
* @param <U> data type
*/
public static <U> MyHttpResponse<U> success(List<U> data) {
return new MyHttpResponse<>(
HttpStatus.OK.value(),
HttpStatus.OK.getReasonPhrase(),
data
);
}
public static <U> MyHttpResponse<U> success(U data) {
return new MyHttpResponse<>(
HttpStatus.OK.value(),
HttpStatus.OK.getReasonPhrase(),
Collections.singletonList(data)
);
}
public static MyHttpResponse<Object> success() {
return new MyHttpResponse<>(
HttpStatus.OK.value(),
HttpStatus.OK.getReasonPhrase(),
Collections.emptyList()
);
}
/**
* fail response
* @param status fail details
* @return response
*/
public static MyHttpResponse<Object> fail(HttpStatus status) {
return new MyHttpResponse<>(
status.value(),
status.getReasonPhrase(),
Collections.emptyList()
);
}
// getter, setter...
}
When we send a request curl
localhost:8080/api/v1/
, we can see the result: {"status": 500, "message": "Internal Server Error", "data":[]}
Checking the log:
System Internal Error: org.example.entity.MyHttpResponse cannot be cast to java.lang.String
It is obvious that somehow we got an exception and the exception is captured by the ExceptionHandler in GlobalResponseAdvice.
2. Analyze the Error
We know that when requests like localhost:8080/api/v1/
come in, Spring uses DispatcherServlet to handle them.
Checking source code doDispatch()
:
Now we know that the error occurs because we change the type of the value from java.lang.String to org.example.entity.MyHttpResponse in GlobalResponseAdvice.
3. Handle the Error
There are two ways to sort this out:
Perform specific processing on String in GlobalResponseAdvice beforeBodyWrite() method:
@Override @SuppressWarnings("unchecked") public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 4xx error handling if(body instanceof MyHttpResponse) { return body; } // controller handling if(body instanceof List) { return MyHttpResponse.success((List<Object>) body); } // handle string to avoid type change. if(body instanceof String) { try { return new ObjectMapper().writeValueAsString(MyHttpResponse.success(body)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } return MyHttpResponse.success(body); }
Add custom converter(Recommended):
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CustomStringHttpMessageConverter extends AbstractHttpMessageConverter<MyHttpResponse<String>> { private final ObjectMapper objectMapper; public CustomStringHttpMessageConverter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.singletonList(MediaType.APPLICATION_JSON); } @Override protected boolean supports(Class<?> clazz) { return String.class == clazz; } @Override protected MyHttpResponse<String> readInternal(Class<? extends MyHttpResponse<String>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { throw new UnsupportedOperationException("This converter can only support writing operation."); } @Override protected void writeInternal(MyHttpResponse<String> s, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { String str = this.objectMapper.writeValueAsString(s); StreamUtils.copy(str.getBytes(StandardCharsets.UTF_8), outputMessage.getBody()); } }
4. Bonus Part: What exactly do these converters do?
Here’s a description on the official site:
Spring MVC uses the
HttpMessageConverter
interface to convert HTTP requests and responses. Sensible defaults are included out of the box. For example, objects can be automatically converted to JSON (by using the Jackson library) or XML (by using the Jackson XML extension, if available, or by using JAXB if the Jackson XML extension is not available). By default, strings are encoded inUTF-8
.
😁And here’s ChatGPT’s answer with a question Can you play as an export of SpringBoot and tell me what does HttpMessageConverter do?
In Spring Boot, the HttpMessageConverter interface is responsible for converting HTTP requests and responses to and from Java objects. It facilitates the conversion of HTTP payloads (such as JSON, XML, or other data formats) into corresponding Java objects and vice versa.
HttpMessageConverters are employed by Spring Boot’s web component to handle the process of serializing Java objects into an HTTP response body or deserializing an HTTP request body into Java objects.
Spring Boot comes with a variety of built-in HttpMessageConverters that support different content types such as JSON, XML, form data, and more. These converters are automatically configured and selected based on the request’s Content-Type and the supported media types defined in the application.
Additionally, developers can also define custom HttpMessageConverters by implementing the HttpMessageConverter interface. This allows for handling specific content types or customization of the conversion process.
Overall, the HttpMessageConverters in Spring Boot play a crucial role in enabling the seamless exchange of data between Java objects and the outside world via HTTP requests and responses.
You can also find this blog on my Medium blog: