java导出excel报错:No converter for [class xxx] with preset Content-Type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'

org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class xxx] with preset Content-Type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'

先说解决方法

报错日志意思为“无法根据 Content-Type 转换返回值”,解决方法有两种:

第一种:把接口返回值改为 void。

第二种:用 response.reset() 置空返回值里的 ContentType,让处理器自己选择合适的 ContentType 来转换返回值,不过这样的话返回给前端的 ContentType 也为空,前端就没法根据 ContentType 来判断文件的格式,需要前端自行设置文件格式。


问题代码

问题现象为导出功能可以正常使用,但日志里会报错“无法转换返回值”,这报错虽然不影响使用,但看着总是很不顺眼的嘛。

public MessageResp<String> exportExcel(@RequestBody Req req, HttpServletResponse response) throws Exception {
try (ServletOutputStream out = response.getOutputStream()) {
     String fileName = URLEncoder.encode("导出表格.xlsx", "UTF-8");
     response.reset();
     response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
     response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ";" + "filename*=utf-8''" + fileName);
     DgExcelUtil.getXlsx("导出 sheet", exportClass, titleList, exportList).write(out);
} catch (Exception e) {
     LOGGER.error("导出报错:", e);
     throw new SmkException(ErrorCode.FAIL, "导出报错");
}
return new MessageResp<>();

报错日志

org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.sk.financial.dg.supervision.response.MessageResp] with preset Content-Type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:312)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:181)
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:124)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1063)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at com.sk.financial.dg.supervision.config.filter.SimpleCORSFilter.doFilter(SimpleCORSFilter.java:24)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at com.sk.financial.dg.supervision.config.filter.LogbackFilter.doFilter(LogbackFilter.java:43)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at com.sk.financial.dg.supervision.config.filter.ReplaceStreamFilter.doFilter(ReplaceStreamFilter.java:36)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1723)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

原因排查

点进报错日志的第一个文件 AbstractMessageConverterMethodProcesso 的 writeWithMessageConverters 方法,可以看到这样一段代码,它检测返回值中是否设置了 contentType,有则把它赋给 selectedMediaType,没有就从所有 MediaType 里自动选择匹配到的 MediaType,赋给 selectedMediaType。

MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
    if (logger.isDebugEnabled()) {
        logger.debug("Found'Content-Type:" + contentType + "' in response");
    }
    selectedMediaType = contentType;
}
else {
    HttpServletRequest request = inputMessage.getServletRequest();
    List<MediaType> acceptableTypes;
    try {
        acceptableTypes = getAcceptableMediaTypes(request);
    }
    catch (HttpMediaTypeNotAcceptableException ex) {
        int series = outputMessage.getServletResponse().getStatus() / 100;
        if (body == null || series == 4 || series == 5) {
            if (logger.isDebugEnabled()) {
                logger.debug("Ignoring error response content (if any)." + ex);
            }
            return;
        }
        throw ex;
    }
    List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
<span class="hljs-keyword">if</span> (body != <span class="hljs-literal">null</span> &amp;&amp; producibleTypes.isEmpty()) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpMessageNotWritableException</span>(
            <span class="hljs-string">"No converter found for return value of type: "</span> + valueType);
}
List&lt;MediaType&gt; mediaTypesToUse = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span>&lt;&gt;();
<span class="hljs-keyword">for</span> (MediaType requestedType : acceptableTypes) {
    <span class="hljs-keyword">for</span> (MediaType producibleType : producibleTypes) {
        <span class="hljs-keyword">if</span> (requestedType.isCompatibleWith(producibleType)) {
            mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
        }
    }
}
<span class="hljs-keyword">if</span> (mediaTypesToUse.isEmpty()) {
    <span class="hljs-keyword">if</span> (body != <span class="hljs-literal">null</span>) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpMediaTypeNotAcceptableException</span>(producibleTypes);
    }
    <span class="hljs-keyword">if</span> (logger.isDebugEnabled()) {
        logger.debug(<span class="hljs-string">"No match for "</span> + acceptableTypes + <span class="hljs-string">", supported: "</span> + producibleTypes);
    }
    <span class="hljs-keyword">return</span>;
}

MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

<span class="hljs-keyword">for</span> (MediaType mediaType : mediaTypesToUse) {
    <span class="hljs-keyword">if</span> (mediaType.isConcrete()) {
        selectedMediaType = mediaType;
        <span class="hljs-keyword">break</span>;
    }
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
        selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
        <span class="hljs-keyword">break</span>;
    }
}

<span class="hljs-keyword">if</span> (logger.isDebugEnabled()) {
    logger.debug(<span class="hljs-string">"Using '"</span> + selectedMediaType + <span class="hljs-string">"', given "</span> +
            acceptableTypes + <span class="hljs-string">" and supported "</span> + producibleTypes);
}

}

而在下一段代码中,selectedMediaType 被用于转换返回值,报错是因为找到的 selectedMediaType 与返回值不匹配,没有通过 ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) 判断,且返回值 body 非空,从而抛出异常。

if (selectedMediaType != null) {
    selectedMediaType = selectedMediaType.removeQualityValue();
    for (HttpMessageConverter<?> converter : this.messageConverters) {
        GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                (GenericHttpMessageConverter<?>) converter : null);
        if (genericConverter != null ?
                ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
                converter.canWrite(valueType, selectedMediaType)) {
            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);
                }
                else {
                    ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                }
            }
            else {
                if (logger.isDebugEnabled()) {
					logger.debug("Nothing to write: null body");
				}
			}
			return;
		}
	}
}

if (body != null) {
Set<MediaType> producibleMediaTypes =
(Set<MediaType>) inputMessage.getServletRequest()
.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

<span class="hljs-keyword">if</span> (isContentTypePreset || !<span class="hljs-title class_">CollectionUtils</span>.<span class="hljs-title function_">isEmpty</span>(producibleMediaTypes)) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpMessageNotWritableException</span>(
            <span class="hljs-string">"No converter for ["</span> + valueType + <span class="hljs-string">"] with preset Content-Type '"</span> + contentType + <span class="hljs-string">"'"</span>);
}
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpMediaTypeNotAcceptableException</span>(<span class="hljs-title function_">getSupportedMediaTypes</span>(body.<span class="hljs-title function_">getClass</span>()));

}

因此可以通过置空返回值,或者置空 ContentType,让处理器自行处理返回值,来解决报错问题。