Affects: spring-webmvc-5.1.2.RELEASE
1、There is a controller's return type is String in the controllers。 2、Add the @ControllerAdvice to change the response type of all controllers。 3、The Spring-framework will chose the StringHttpMessageConverter to convert the response body,and got an error。
//the error code:
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
//使用了StringHttpMessageConverter
//调用advice的相关方法,@ControllerAdvice 返回对象被修改。
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
error stack:
java.lang.ClassCastException: classname cannot be cast to java.lang.String
at org.springframework.http.converter.StringHttpMessageConverter.getContentLength(StringHttpMessageConverter.java:43) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.http.converter.AbstractHttpMessageConverter.addDefaultHeaders(AbstractHttpMessageConverter.java:260) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:211) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:292) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
Comment From: bclozel
Instead of pointing to a code snippet in Framework, could you share a small sample application that shows the issue and explain how to reproduce it?
Comment From: jiangwh
👌。 1、The code sample of the controllerAdvice
@ControllerAdvice
@Slf4j
public class ReturnObjectAdvice 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) {
ObjectWrapper objectWrapper = returnType.getMethodAnnotation(ObjectWrapper.class);
if (null != objectWrapper) {
try {
Object object = objectWrapper.value().newInstance();
Method method = objectWrapper.value().getDeclaredMethod(objectWrapper.setData(), Object.class);
if (null != method) {
method.setAccessible(true);
method.invoke(object, body);
// change the response type
return object;
}
} catch (Exception e) {
log.error("", e);
}
}
return body;
}
}
2、The code of the controller
@RestController
@RequestMapping(value = "/a")
public class A {
@GetMapping("/test")
@ObjectWrapper
public String checkCode() {
return "some String";
}
}
3、The annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ObjectWrapper {
Class<?> value() default ResponseWrapper.class;
String setData() default "setData";
String setCode() default "setCode";
String setMessage() default "setMessage";
}
4、The response type
@Getter
@Setter
public class ResponseWrapper<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String message;
private T data;
public ResponseWrapper() {
this.code = HttpStatus.OK.value();
this.message = HttpStatus.OK.getReasonPhrase();
}
public ResponseWrapper(HttpStatus status) {
this.code = status.value();
this.message = status.getReasonPhrase();
}
public ResponseWrapper(T data) {
if (isEmpty(data)) {
this.code = HttpStatus.NO_CONTENT.value();
this.message = HttpStatus.NO_CONTENT.getReasonPhrase();
} else {
this.code = HttpStatus.OK.value();
this.message = HttpStatus.OK.getReasonPhrase();
this.data = data;
}
}
public ResponseWrapper(HttpStatus status, T data) {
this.code = status.value();
this.message = status.getReasonPhrase();
this.data = data;
}
@SuppressWarnings("rawtypes")
public boolean isEmpty(T data) {
return data == null || data instanceof Collection && ((Collection) data).isEmpty();
}
@Override
public String toString() {
try {
return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE);
} catch (Exception e) {
return super.toString();
}
}
}
Comment From: bclozel
This is the expected behavior of the ResponseBodyAdvice
interface. As you can see in its other method supports
, at that point the message converter has been already selected by looking at the return type of the method and the produces
condition in the @GetMapping
.
If you'd like to force one or more specific Content-Type for the response, you should express that in the @GetMapping
annotation.
In any case, your ReturnObjectAdvice
should refine its supports
implementation to only return true
if the selected HttpMessageConverter
can serialize your wrapper object.
For further questions, please use StackOverflow. Thanks!
Comment From: jiangwh
Thank you!
Comment From: swithun-liu
👌。 1、The code sample of the controller device
```java @ControllerAdvice @Slf4j public class ReturnObjectAdvice 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) {
ObjectWrapper objectWrapper = returnType.getMethodAnnotation(ObjectWrapper.class); if (null != objectWrapper) { try { Object object = objectWrapper.value().newInstance(); Method method = objectWrapper.value().getDeclaredMethod(objectWrapper.setData(), Object.class); if (null != method) { method.setAccessible(true); method.invoke(object, body); // change the response type return object; } } catch (Exception e) { log.error("", e); } } return body;
} } ```
2、The code of the controller
```java @RestController @RequestMapping(value = "/a") public class A {
@GetMapping("/test") @ObjectWrapper public String checkCode() { return "some String"; } } ```
3、The annotation
```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ObjectWrapper { Class<?> value() default ResponseWrapper.class;
String setData() default "setData";
String setCode() default "setCode";
String setMessage() default "setMessage"; } ```
4、The response type
```java @Getter @Setter public class ResponseWrapper
implements Serializable { private static final long serialVersionUID = 1L; private int code; private String message; private T data;
public ResponseWrapper() { this.code = HttpStatus.OK.value(); this.message = HttpStatus.OK.getReasonPhrase(); }
public ResponseWrapper(HttpStatus status) { this.code = status.value(); this.message = status.getReasonPhrase(); }
public ResponseWrapper(T data) {
if (isEmpty(data)) { this.code = HttpStatus.NO_CONTENT.value(); this.message = HttpStatus.NO_CONTENT.getReasonPhrase(); } else { this.code = HttpStatus.OK.value(); this.message = HttpStatus.OK.getReasonPhrase(); this.data = data; }
}
public ResponseWrapper(HttpStatus status, T data) { this.code = status.value(); this.message = status.getReasonPhrase(); this.data = data; }
@SuppressWarnings("rawtypes") public boolean isEmpty(T data) { return data == null || data instanceof Collection && ((Collection) data).isEmpty(); }
@Override public String toString() { try { return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE); } catch (Exception e) { return super.toString(); } } } ```
Can I find samples like this in spring documentation?
Comment From: cbsingh1
Any recommendation for how to select correct MessageConverter when applying ResponseBodyAdvice for String return type ?