记一次Java
poi导出Excel全部出现损坏无法打开的问题解决
一、问题背景
因为需要做权限控制,而历史原因认证模块是另一个服务,无法进行依赖,所以项目中重复引入了SpringSecurity
,然后进行了正常的配置,由于需要拿到当前用户信息,于是重写了OncePerRequestFilter
的doFilterInternal
方法进行拦截验证。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { System.out.println("请求头类型: " + request.getContentType()); if ((request.getContentType() == null && request.getContentLength() > 0) || (request.getContentType() != null && !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE))) { filterChain.doFilter(request, response); return; }
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request); MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response);
try { logRequestBody(wrappedRequest);
String jwtToken = wrappedRequest.getHeader(Constants.REQUEST_HEADER); log.debug("后台检查令牌:{}", jwtToken); if (StringUtils.isNotBlank(jwtToken) && !"undefined".equals(jwtToken)) {
String userStr = userInfoFeignClient.getUserInfo(jwtToken); if ("null".equals(userStr) || StringUtils.isEmpty(userStr)) { throw new BadCredentialsException("TOKEN已过期,请重新登录!"); } SecurityUser securityUser = JSON.parseObject(userStr, SecurityUser.class);
if (securityUser.getCurrentUserInfo() == null) { throw new BadCredentialsException("TOKEN已过期,请重新登录!"); } UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(wrappedRequest, wrappedResponse);
} catch (AuthenticationException e) { SecurityContextHolder.clearContext(); throw new HttpClientErrorException(HttpStatus.FORBIDDEN,"Token已过期"); }
}
|
在权限测试基本没有出现问题的时候,突然发现之前的导出Excel都出现文件损坏了,无法打开。

二、问题解决
我初步以为是response
的header
出现问题,导致解析出现问题,于是查找网上资料说设置content-length
,但是我是写好workbook后直接写到输出流里,所以无法指定content-length
,而之前这样配置都没有出现过问题。
|
private static void setResponseHeader(HttpServletResponse response, String fileName) { try { fileName = new String(fileName.getBytes(), StandardCharsets.ISO_8859_1); response.setContentType("application/octet-stream;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=" + fileName); response.addHeader("Pargam", "no-cache"); response.addHeader("Cache-Control", "no-cache"); } catch (Exception ex) { ex.printStackTrace(); } }
|
可无论我怎么修改response
的header
都还是一样的问题。于是我把注意力转移到导出的代码部分,网上有资料说原因可能是没有写sheet,导致excel错误,所以猜测可能是写workbook的时候写入出现错误,但是导出的代码没有动过,而且是所有导出都出现了问题,于是我尝试将输出流该为文件输出流,直接将Excel输出到本地,发现可以正常打开。
然后排除了导出代码部分的问题。
后来看到这篇文章。
Content-Length不正确导致导出的Excel无法打开
- 代码先锋网 (codeleading.com)
文中是response的header
中出现了content-length
,我测试发现是transfer-encoding
,transfer-encoding
表示不确定的content-length,也就是正确的,所以也不是文中出现的问题。

本地输出excel可以正常打开,而http请求返回后却出现损坏,可以确定问题就出现在response
里,但header
的属性我修改后还是没有解决问题,那么问题出现在哪呢?
文中这段话给了我提示。

response是不是被改变了?!
但是我的response没有出现任何提示或报错,使用的是常规的HttpServletResponse接收。
| public void exportTypeData(HttpServletResponse response, @RequestParam("typeName") String typeName)
|
于是我返回到security的filter中,发现有这样一句代码
| MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response);
|
这里包装了response!
进去看看
| public class MultiReadHttpServletResponse extends HttpServletResponseWrapper {
private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); private HttpServletResponse response;
public MultiReadHttpServletResponse(HttpServletResponse response) { super(response); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); this.response = response; }
|
继承了HttpServletResponseWrapper,构造函数赋了一些初值,这些值没有影响,后面可以改写。再继续往下看。
| public class HttpServletResponseWrapper extends ServletResponseWrapper implements HttpServletResponse
|
好家伙,自定义的MultiReadHttpServletResponse
继承了HttpServletResponseWrapperHttpServletResponseWrapper
而它实现了HttpServletResponse
接口,所以参数接收的是接口实现类完全没有问题。
但这个response不是最原始的response,就如我想要一支笔,但是收到的是一只笔套,里面除了原来的笔,还有其他东西。
然后通过debug,发现接收到的确不是最原始的response。

而它里面被包装的response才是我想要的response。
但是不能通过修改代码来获取里面的response,因为参数是HttpServletResponse
接口。
于是不让它包装我的response。
filterChain.doFilter(wrappedRequest, response);
然后进行调试,发现接收到了原来的response。
然后excel也可以正常打开了。

三、问题原因
由于reponse
中的content-type
设置的是application/octet-stream
,表示是二进制文件流,浏览器可以获取然后下载,原来的response只包含了生成的excel文件信息,而被包装后的response
多了一些其他的属性内容,但浏览器仍然将其所有内容都解析为excel文件内容,从而导致excel解析出错。