POI导出excel中文乱码问题

在实习中遇到的关于excel导出的问题

POI导出excel中文乱码问题

前言

最近在工作中遇到这样一个场景,需要将表格内容导出成 excel 在浏览器中下载

表格场景

这里因为目前表格是分页的,导出的时候需要根据查询字段作全量查询,然后转换成 excel 表格导出,这里我使用了 apache 的 poi 组件去做这件事

导入poi依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- 导入poi依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.7</version>
</dependency>

注意:

  • poi 不同版本中方法有些不一样,需要检查一下
  • poi-ooxml 和 poi 版本需要保持一致

编写ExcelUtil工具类

在这个工具类中我目前就写了三个方法,此外还自定义了一个注解用来定义表头名称:

注解:

1
2
3
4
5
6
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelHeaderName {

String value();
}

获取表头:

1
2
3
4
5
6
7
8
9
10
11
12
public static List<String> getHeaderNames(Class<?> clazz) {
List<String> headerNames = new ArrayList<>();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
if (field.isAnnotationPresent(ExcelHeaderName.class)) {
ExcelHeaderName annotation = field.getAnnotation(ExcelHeaderName.class);
// String headerName = new String(annotation.value().getBytes(), StandardCharsets.UTF_8);
headerNames.add(annotation.value());
}
}
return headerNames;
}

获取属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static List<Object> getFieldValues(Object obj) {
List<Object> fieldValues = new ArrayList<>();
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
try {
if (field.get(obj) != null) {
fieldValues.add(field.get(obj));
} else {
fieldValues.add("");
}
} catch (IllegalAccessException e) {
LogUtil.error(e, LOGGER, "获取字段值失败:{0} ", e.getMessage());
}
}
return fieldValues;
}

excel导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void exportExcel(Workbook workbook, HttpServletResponse httpServletResponse) {
try {
SimpleDateFormat df = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss");
String fileName = df.format(new Date()) + ".xls";
//清空response
httpServletResponse.reset();
//设置response的Header
httpServletResponse.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/vnd.ms-excel");
ServletOutputStream out = httpServletResponse.getOutputStream();
// 关闭输出流和工作簿对象
workbook.write(out);
out.flush();
out.close();
} catch (IOException e) {
LogUtil.error(e, LOGGER, "导出文档失败:{0} ", e.getMessage());
}
}

在业务层中导出excel

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
@Override
public void userExport(UserDetailExportRequest request, HttpServletResponse httpServletResponse) {
List<ModelCallDetailDO> modelCallDetailDOList = modelCallDetailStoreService.userDetailList(request.getProject_id(), request.getModel_id());
List<UserDetailExportResponse> userDetailExportResponseList = modelCallDetailDOList.stream().map(ModelCallConvertor::userExcelConvertor).collect(Collectors.toList());
Workbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet();
Row header = sheet.createRow(0);
// 表头赋值
List<String> headerNames = ExcelUtil.getHeaderNames(UserDetailExportResponse.class);
for (int i = 0; i < headerNames.size(); i++) {
Cell cell = header.createCell(i);
cell.setCellValue(headerNames.get(i));
}
// 写入数据
for (int i = 0; i < userDetailExportResponseList.size(); i++) {
Row row = sheet.createRow(i + 1);
UserDetailExportResponse userDetailExportResponse = userDetailExportResponseList.get(i);
List<Object> fieldValues = ExcelUtil.getFieldValues(userDetailExportResponse);
for (int j = 0; j < fieldValues.size(); j++) {
Cell cell = row.createCell(j);
Object value = fieldValues.get(j);
if (value instanceof String) {
String valueStr = new String(((String) value).getBytes(), StandardCharsets.UTF_8);
cell.setCellValue(valueStr);
} else {
cell.setCellValue((Integer) fieldValues.get(j));
}
}
}
try {
ExcelUtil.exportExcel(workbook, httpServletResponse);
} catch (BusinessException be){
throw new BusinessException(ErrorCodeEnum.FAIL, be.getErrorMessage());
}catch (Exception e) {
throw new BusinessException(ErrorCodeEnum.FAIL, e.getMessage());
}
}

poi生成excel大致就分为三步:

  1. 创建 workbook(Workbook workbook = new HSSFWorkbook();)
  2. 创建 sheet(Sheet sheet = workbook.createSheet();)
  3. 在 sheet 中创建行,并给行中的单元格赋值(sheet.createRow(),cell.setCellValue())

这里如果正常导出应该是下图中的结果:

excel导出数据

中文乱码问题

在前后端联调时,我通过前端下载下来的文件出现了乱码:

文件乱码

是很经典的“锟斤拷”乱码问题,这个问题出现的原因一般是因为 UTF-8 和 GBK 两种编码互转,通过上网查询,都说是要设置 response 的 header:

1
2
3
httpServletResponse.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/vnd.ms-excel");

但我设置完后还是一样乱码,通过前端的调试发现,从后端传来的字节流在进入下载函数前就以及乱码了,后来定位是服务转发的问题(坑)

特性 浏览器直连 服务端转发
请求路径 浏览器直接请求目标服务器 浏览器请求中间服务器,中间服务器转发请求到目标服务器
优点 简单直接,实时性好,资源利用率高 解决跨域问题,提高安全性,缓存优化,统一处理
缺点 跨域限制,安全性问题,客户端复杂性高 增加延迟,服务器负载增加,系统复杂性高
应用场景 简单的单页应用,静态资源加载 跨域请求,安全需求高,负载均衡,API网关

结果应该是在转发的途中编码发生改变,导致乱码。。。

解决方案

引入OSS存储桶

OSS(Object Storage Service)存储桶是云存储服务中的一种基本存储单元,用于存储和管理对象数据。OSS 存储桶通常提供高可用性、高可靠性和大规模存储能力,适用于存储图片、视频、备份文件、日志等各种非结构化数据

特性 优点 缺点
高可用性和持久性 提供高可用性和持久性,数据冗余存储,跨地域复制 需要额外配置和成本
无限扩展性 支持大规模存储,几乎无限的存储容量 数据管理和组织可能变得复杂
成本效益 按需付费,提供多种存储类型(标准存储、低频存储、归档存储)以优化成本 不同存储类型间的切换可能产生额外费用
安全性 支持多种访问控制和加密机制,确保数据安全 配置复杂,错误配置可能导致安全漏洞
数据管理 支持生命周期管理、版本控制、对象标签等,便于数据管理 需要熟悉和配置这些管理功能
全球访问 提供全球访问能力,CDN加速,提高数据访问速度 需要考虑数据隐私和合规性问题
集成与兼容性 提供丰富的API接口,易于集成到各种应用和服务中 需要开发人员具备相关技术知识
性能 高并发访问,适用于大规模数据处理和分发 高性能需求下可能需要优化配置和额外成本

引入 OSS 就可以将生成的 excel 存入桶中并生成下载 URL 返回给前端,前端直接通过 URL 下载就行了,但这样做有几个问题:

  1. 上传至 OSS 再下载会影响性能,耗时比较长,特别是还做了一次全量查询
  2. 如果下载的次数多,OSS 中会有很多文件,查询字段稍微变一下就得上传一次

将数据存到前端

可以直接将查询的数据存到前端,再从前端生成 excel,这样就没有编码的问题,但这只适用于数据量小的场景下,一旦数据量大了,前端就有可能挂掉

将导出按钮的接口的网络改成浏览器直连

其实将接口的网络改成浏览器直连就行,但这样又会导致安全问题

修改服务端转发的方式

这应该是最好的方法了