在实习中遇到的关于excel导出的问题
POI导出excel中文乱码问题
前言
最近在工作中遇到这样一个场景,需要将表格内容导出成 excel
在浏览器中下载
这里因为目前表格是分页的,导出的时候需要根据查询字段作全量查询,然后转换成
excel 表格导出,这里我使用了 apache 的 poi 组件去做这件事
导入poi依赖
1 2 3 4 5 6 7 8 9 10 11
| <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);
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"; httpServletResponse.reset(); 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大致就分为三步:
- 创建 workbook(Workbook workbook = new HSSFWorkbook();)
- 创建 sheet(Sheet sheet = workbook.createSheet();)
- 在 sheet
中创建行,并给行中的单元格赋值(sheet.createRow(),cell.setCellValue())
这里如果正常导出应该是下图中的结果:
中文乱码问题
在前后端联调时,我通过前端下载下来的文件出现了乱码:
是很经典的“锟斤拷”乱码问题,这个问题出现的原因一般是因为 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 下载就行了,但这样做有几个问题:
- 上传至 OSS
再下载会影响性能,耗时比较长,特别是还做了一次全量查询
- 如果下载的次数多,OSS
中会有很多文件,查询字段稍微变一下就得上传一次
将数据存到前端
可以直接将查询的数据存到前端,再从前端生成
excel,这样就没有编码的问题,但这只适用于数据量小的场景下,一旦数据量大了,前端就有可能挂掉
将导出按钮的接口的网络改成浏览器直连
其实将接口的网络改成浏览器直连就行,但这样又会导致安全问题
修改服务端转发的方式
这应该是最好的方法了