记一次Java poi导出Excel全部出现损坏无法打开的问题解决

记一次Java poi导出Excel全部出现损坏无法打开的问题解决

一、问题背景

因为需要做权限控制,而历史原因认证模块是另一个服务,无法进行依赖,所以项目中重复引入了SpringSecurity,然后进行了正常的配置,由于需要拿到当前用户信息,于是重写了OncePerRequestFilterdoFilterInternal方法进行拦截验证。代码如下

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);



// 前后端分离情况下,前端登录后将token储存在cookie中,每次访问接口时通过token去拿用户权限
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都出现文件损坏了,无法打开。

二、问题解决

我初步以为是responseheader出现问题,导致解析出现问题,于是查找网上资料说设置content-length,但是我是写好workbook后直接写到输出流里,所以无法指定content-length,而之前这样配置都没有出现过问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 设置浏览器下载响应头
*/
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();
}
}

可无论我怎么修改responseheader都还是一样的问题。于是我把注意力转移到导出的代码部分,网上有资料说原因可能是没有写sheet,导致excel错误,所以猜测可能是写workbook的时候写入出现错误,但是导出的代码没有动过,而且是所有导出都出现了问题,于是我尝试将输出流该为文件输出流,直接将Excel输出到本地,发现可以正常打开。

然后排除了导出代码部分的问题。

后来看到这篇文章。

Content-Length不正确导致导出的Excel无法打开 - 代码先锋网 (codeleading.com)

文中是response的header中出现了content-length,我测试发现是transfer-encodingtransfer-encoding表示不确定的content-length,也就是正确的,所以也不是文中出现的问题。

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

文中这段话给了我提示。

response是不是被改变了?!

但是我的response没有出现任何提示或报错,使用的是常规的HttpServletResponse接收。

1
public void exportTypeData(HttpServletResponse response, @RequestParam("typeName") String typeName)

于是我返回到security的filter中,发现有这样一句代码

1
MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response);

这里包装了response!

进去看看

1
2
3
4
5
6
7
8
9
10
11
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,构造函数赋了一些初值,这些值没有影响,后面可以改写。再继续往下看。

1
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解析出错。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!