文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间
案例25:返回结果无合适的转化器
在直接用 Spring MVC 而非 Spring Boot 来编写 Web 程序时,以下例子会遇到 “No converter found for return value of type” 错误
//定义的数据对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private Integer age;
}
//定义的 API 借口
@RestController
public class HelloController {
@GetMapping("/hi1")
public Student hi1() {
return new Student("xiaoming", Integer.valueOf(12));
}
}
解析:在 Spring MVC 处理完后,需要根据 request 的可接受类型 AcceptableMediaTypes 和自身能返回的类型 ProducibleMediaTypes ,这两者间挑选合适的作为返回信息的处理类型,这个步骤参考 AbstractMessageConverterMethodProcessor#writeWithMessageConverters
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
-----------------------------------------------------------------------------------
protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
Set<MediaType> mediaTypes =
(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<>();
// 遍历所有的已经注册的 HttpMessageConverter 查看是否支持当前类型
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
Spring MVC 启动后,通过构建 AnnotationDrivenBeanDefinitionParser 这个 Bean 负责路由和处理请求。而在它的构建过程中,会决策出以后要使用哪些 HttpMessageConverter,相关代码参考 AnnotationDrivenBeanDefinitionParser#getMessageConverters
messageConverters.add(createConverterDefinition(ByteArrayHttpMessageConverter.class, source));
RootBeanDefinition stringConverterDef = createConverterDefinition(StringHttpMessageConverter.class, source);
stringConverterDef.getPropertyValues().add("writeAcceptCharset", false);
messageConverters.add(stringConverterDef);
messageConverters.add(createConverterDefinition(ResourceHttpMessageConverter.class, source));
//省略其他非关键代码
if (jackson2Present) {
Class<?> type = MappingJackson2HttpMessageConverter.class;
RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
messageConverters.add(jacksonConverterDef);
}
else if (gsonPresent) { messageConverters.add(createConverterDefinition(GsonHttpMessageConverter.class, source));
}
//省略其他非关键代码
而其中由于没有添加 JSON 的相关库,故 jackson2Present、gsonPresent 等变量为 false,便没有 JSON 相关转化器,因此针对 Student 等自定义类无合适的转化器,就会抛出 No converter found for return value of type 错误。
解决:引入 JSON 的相关库
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version></dependency>
若使用 spring-boot-starter-web ,则会自动引入 JSON 的相关库
案例 26:Jackson 和 Gson 是否序列化 Null
在请求中的 Body 中获取转化为 Student 对象并返回,但得到返回结果却会有所不同
@RestController
public class HelloController {
@PostMapping("/hi2")
public Student hi2(@RequestBody Student student) {
return student;
}
}
POST http://localhost:8080/springmvc3_war/app/hi2
Content-Type: application/json
{“name”: “xiaoming”}
---------------------------------------------------------------------
// 得到结果1
{“name”: “xiaoming”}
---------------------------------------------------------------------
// 得到结果2
{“name”: “xiaoming” , "age" : null }
解析:Jackson 和 Gson 在同时存在的情况下,会优先选择 Jaskson
对于 Gson 而言,是默认不序列化 null 的
而 Jackson 默认对于 null 的处理是做序列化的
解决:为保证结果返回始终如一
1. 使用 @JsonInclude 注解,让 Jackson 和 Gson 的默认行为对于 null 的处理变成一致
@Data
@NoArgsConstructor
@AllArgsConstructor
// 使用 @JsonInclude 注解
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Student {
private String name;
//或直接加在 age 上:@JsonInclude(JsonInclude.Include.NON_NULL)
private Integer age;
}
2. 用自动注入的方式获取到 RequestMappingHandlerAdapter,并找到 Jackson 解析器做自定义配置
@RestController
public class HelloController {
public HelloController(RequestMappingHandlerAdapter requestMappingHandlerAdapter){
List<HttpMessageConverter<?>> messageConverters =
requestMappingHandlerAdapter.getMessageConverters();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
if(messageConverter instanceof MappingJackson2HttpMessageConverter ){
(((MappingJackson2HttpMessageConverter)messageConverter).getObjectMapper()).setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
}
}
//省略其他非关键代码
}
案例 27:多次读取请求流中的 Body
自定义一个 Filter 来统一输出具体的请求中 requestBody 的内容
public class ReadBodyFilter implements Filter {
//省略其他非关键代码
@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String requestBody = IOUtils.toString(request.getInputStream(), "utf-8");
System.out.println("print request body in filter:" + requestBody);
chain.doFilter(request, response);
}
}
----------------------------------------------------------------
@PostMapping("/hi3")
public Student hi3(@RequestBody Student student) {
return student;
}
// 请求运行结果
print request body in filter:{“name”: “xiaoming”,“age”: 10}
25-Mar-2021 11:04:44.906 璀﹀憡 [http-nio-8080-exec-5]
org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.logException Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.puzzles.Student com.puzzles.HelloController.hi3(com.puzzles.Student)]
解析:请求 Body 转化的相关代码 RequestResponseBodyMethodProcessor#readWithMessageConverters)
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
//读取 Body 并进行转化
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
----------------------------------------------------------------
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType){
//省略非关键代码
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
//省略非关键代码:读取并转化 body
else {
//处理没有 body 情况,默认返回 null
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
//省略非关键代码
return body;
}
protected boolean checkRequired(MethodParameter parameter) {
RequestBody requestBody = parameter.getParameterAnnotation(RequestBody.class);
return (requestBody != null && requestBody.required() && !parameter.isOptional());
}
------------------------------------------------------------------
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
InputStream inputStream = inputMessage.getBody();
if (inputStream.markSupported()) {
//省略其他非关键代码
}
else {
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
int b = pushbackInputStream.read();
if (b == -1) {
this.body = null;
}
else {
this.body = pushbackInputStream;
pushbackInputStream.unread(b);
}
}
}
public InputStream getBody() {
return (this.body != null ? this.body : StreamUtils.emptyInput());
}
请求流中有 Body,但是 Body 本身代表的流已经被前面读取过了,到 Controller 的方法中,则再获取不到 Body 中的数据
解决:定义一个 RequestBodyAdviceAdapter 的 Bean
@ControllerAdvice
public class PrintRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
System.out.println("print request body in advice:" + body);
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
----------------------------------------------------------------------------------
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType){
//省略其他非关键代码
if (message.hasBody()) {
HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message,parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :((HttpMessageConverter<T>)converter).read(targetClass, msgToUse));
// 调用上面定义的 PrintRequestBodyAdviceAdapter
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
//省略其他非关键代码
}
//省略其他非关键代码
return body;
}
传递给 PrintRequestBodyAdviceAdapter 的 Body 对象已经是一个解析过的对象,而不再是一个流