前言
这两天公司项目业务提出需求,要求在前端上传excel文件然后解析展示,因此写篇文章记录一下实现。
技术栈
springboot 2.6.6
1、引入依赖
maven格式:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.14</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.14</version>
</dependency>
2、导入代码实现
package com.nanmu.payment.controller;
import com.nanmu.payment.common.MessageConst;
import com.nanmu.payment.entity.Result;
import com.nanmu.payment.pojo.OrderSetting;
import com.nanmu.payment.service.OrderSettingService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
/**
* @author
* @description
* @date 2022/12/22
**/
@RestController
@RequestMapping("/ordersetting")
@Slf4j
public class OrderSettingController {
/**
* 上传预约设置的excel文件
* @param multipartFile
* @return
*/
@RequestMapping("/upload")
public Result upload(@RequestParam("excelFile") MultipartFile multipartFile) {
log.info("[预约设置-上传]fileName:{},size:{}", multipartFile.getOriginalFilename(), multipartFile.getSize());
String filename = multipartFile.getOriginalFilename();
if(StringUtils.isEmpty(filename)){
return new Result(false,"缺少文件名");
}
//1 抽取excel数据 poi
try (InputStream is = multipartFile.getInputStream();) {
Workbook workbook = null;
//1.1构造workbook
if(filename.endsWith(".xls")){
// excel 2003
workbook = new HSSFWorkbook(is);
}else if(filename.endsWith(".xlsx")){
// excel 2007
workbook = new XSSFWorkbook(is);
}else{
return new Result(false,"文件格式不正确,请检查重试");
}
//1.2遍历取数据
List<OrderSetting> orderSettings = new ArrayList<>();
for (Sheet sheet : workbook) {
// 第0行为表头,直接从第1行取
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
Date date = null;
int number = 0;
try {
date = null != row.getCell(0) ? row.getCell(0).getDateCellValue() : null;
number = null != row.getCell(1) ? Double.valueOf(row.getCell(1).getNumericCellValue()).intValue() : 0;
}catch (IllegalStateException|NumberFormatException e){
log.error("",e);
return new Result(false,String.format("数据格式错误,%s 第%d行",sheet.getSheetName(),row.getRowNum()+1));
}
if(null == date ){
return new Result(false,String.format("缺少必填数据,%s 第%d行",sheet.getSheetName(),row.getRowNum()+1));
}
//构造OrderSetting
orderSettings.add(new OrderSetting(date,number));
}
}
log.info("[预约设置-上传]解析成功,result:{}",orderSettings);
//2 调用service层
//调用service层代码去掉
return new Result(true,MessageConst.IMPORT_ORDERSETTING_SUCCESS);
} catch (RuntimeException|IOException e) {
log.info("",e);
return new Result(false, MessageConst.IMPORT_ORDERSETTING_FAIL);
}
}
}
3、导出代码实现
3.1、准备导出文件模板
将导出文件模板放入resources包下面(如下图黄框所示)
文件内容如下图所示:
3.2、导出代码实现
package com.nanmu.payment.controller;
import com.nanmu.payment.common.MessageConst;
import com.nanmu.payment.entity.Result;
import com.nanmu.payment.service.MemberService;
import com.nanmu.payment.service.OrderService;
import com.nanmu.payment.service.ReportService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 数据报告控制器
*/
@RestController
@RequestMapping("/report")
@Slf4j
public class ReportController {
@Autowired
private ReportService reportService;
/**
* 导出运营数据报表
*
* @return
*/
@GetMapping("exportBusinessReport")
public Result exportBusinessReport(HttpServletResponse response) {
log.info("[导出运营数据报表]开始");
try {
//1-调用service层获取数据
Map<String, Object> reportData = reportService.getBusinessReportData();
log.debug("[导出运营数据报表]rpc rsp:{}", reportData);
// 取出返回结果
String reportDate = (String) reportData.get("reportDate");
// 获取会员相关数据
Long todayNewMember = (Long) reportData.get("todayNewMember");
Long thisWeekNewMember = (Long) reportData.get("thisWeekNewMember");
Long thisMonthNewMember = (Long) reportData.get("thisMonthNewMember");
Long totalMember = (Long) reportData.get("totalMember");
// 获取预约相关数据
Long todayOrderNumber = (Long) reportData.get("todayOrderNumber");
Long thisWeekOrderNumber = (Long) reportData.get("thisWeekOrderNumber");
Long thisMonthOrderNumber = (Long) reportData.get("thisMonthOrderNumber");
// 获取到诊相关数据
Long todayVisitsNumber = (Long) reportData.get("todayVisitsNumber");
Long thisWeekVisitsNumber = (Long) reportData.get("thisWeekVisitsNumber");
Long thisMonthVisitsNumber = (Long) reportData.get("thisMonthVisitsNumber");
// 获取套餐数据
List<Map> hotSetmeal = (List<Map>) reportData.get("hotSetmeal");
//2-读取模板构造workBook
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("report_template.xlsx");
Workbook workbook = new XSSFWorkbook(is);
OutputStream os = response.getOutputStream()) {
Sheet sheet = workbook.getSheetAt(0);
//3-填入数据
//日期
sheet.getRow(2).getCell(5).setCellValue(reportDate);
//会员统计数据
sheet.getRow(4).getCell(5).setCellValue(todayNewMember);
sheet.getRow(4).getCell(7).setCellValue(totalMember);
sheet.getRow(5).getCell(5).setCellValue(thisWeekNewMember);
sheet.getRow(5).getCell(7).setCellValue(thisMonthNewMember);
//预约到诊数据统计
sheet.getRow(7).getCell(5).setCellValue(todayOrderNumber);
sheet.getRow(7).getCell(7).setCellValue(todayVisitsNumber);
sheet.getRow(8).getCell(5).setCellValue(thisWeekOrderNumber);
sheet.getRow(8).getCell(7).setCellValue(thisWeekVisitsNumber);
sheet.getRow(9).getCell(5).setCellValue(thisMonthOrderNumber);
sheet.getRow(9).getCell(7).setCellValue(thisMonthVisitsNumber);
//热门套餐
int rowNum = 12;
for (Map setMeal : hotSetmeal) {
Row row = sheet.getRow(rowNum);
row.getCell(4).setCellValue((String) setMeal.get("name"));
row.getCell(5).setCellValue((Long) setMeal.get("setmeal_count"));
row.getCell(6).setCellValue(((BigDecimal) setMeal.get("proportion")).doubleValue());
rowNum++;
}
//4-写入网络输出流
//写入返回流
response.setContentType("application/vnd.ms-excel");
response.setHeader("content-Disposition", "attachment;fileName=" + reportDate + "_report.xlsx");
workbook.write(os);
return null;
} catch (IOException e) {
// 不会出现/无法处理的受检异常,转换为运行时异常
throw new RuntimeException(e);
}
} catch (RuntimeException e) {
log.error("", e);
return new Result(false, MessageConst.ACTION_FAIL);
}
}
}
4、代码实现解释
- 导入前端上传的文件不进行存储,直接读取之后将内容存入数据库;
- 导入支持.xls和.xlsx两种格式excel文件;
- 导入文件行数不易过大,过大会出现内存泄漏等问题(后面再出关于百万行数据读取、导出的方案);
- 导出文件使用模板,这样便于写入,同样不支持数据行过多;
- 导出直接返回.xlsx格式excel文件给前端。
5、常见问题
-
读取文件失败,提示信息:The supplied data appears to be in the Office 2007+ XML. You are calling the part of POI that deals with OLE2 Office Documents. You need to call a different part of POI to process this data (eg XSSF instead of HSSF)
分析及解决方案为:经分析发现是HSSFWorkbook不支持读取.xlsx,改为XSSFWorkbook即可。 -
内存溢出问题,提示信息:java.lang.OutOfMemoryError: Java heap space
at jdk.internal.reflect.GeneratedConstructorAccessor39.newInstance(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490) ~[na:na]
at org.apache.xmlbeans.impl.schema.SchemaTypeImpl.createUnattachedNode(SchemaTypeImpl.java:1934) ~[xmlbeans-2.6.0.jar:na]
at org.apache.xmlbeans.impl.schema.SchemaTypeImpl.createElementType(SchemaTypeImpl.java:1051) ~[xmlbeans-2.6.0.jar:na]
at org.apache.xmlbeans.impl.values.XmlObjectBase.create_element_user(XmlObjectBase.java:938) ~[xmlbeans-2.6.0.jar:na]
at org.apache.xmlbeans.impl.store.Xobj.getUser(Xobj.java:1675) ~[xmlbeans-2.6.0.jar:na]
at org.apache.xmlbeans.impl.store.Xobj.find_all_element_users(Xobj.java:2098) ~[xmlbeans-2.6.0.jar:na]
at org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRowImpl.getCArray(Unknown Source) ~[poi-ooxml-schemas-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFRow.(XSSFRow.java:73) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFSheet.initRows(XSSFSheet.java:215) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFSheet.read(XSSFSheet.java:178) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFSheet.onDocumentRead(XSSFSheet.java:165) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFWorkbook.parseSheet(XSSFWorkbook.java:417) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFWorkbook.onDocumentRead(XSSFWorkbook.java:382) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.POIXMLDocument.load(POIXMLDocument.java:178) ~[poi-ooxml-3.14.jar:3.14]
at org.apache.poi.xssf.usermodel.XSSFWorkbook.(XSSFWorkbook.java:279) ~[poi-ooxml-3.14.jar:3.14]
分析及解决方案为:经分析发现是导入、导出数据行数太多,目前建议业务分批导出,后续出百万行数据读取、导出的方案。