近期有个需求,说是要用到excel导入导出,一般我们的想法都是按照行数,于是实现了,后面发现公司需求的是列读,甚至不规则的单个excel的读。于是就用poi自己写了按照单元格读的实现。
一、按行读
想起了之前用到的poi,经过搜索发现,开源的项目中有比较好的封装poi的框架,一个是阿里出的easyExcel,另一个是easypoi,感觉使用起来都很方便。网上说easyExcel能解决大文件内存溢出问题,于是项目中就使用easyExcel了。
简单普及下easyExcel原理,不做底层码农,了解点上层设计有好处:
easyExcel核心原理
写有大量数据的xlsx文件时,POI为我们提供了SXSSFWorkBook类来处理,这个类的处理机制是当内存中的数据条数达到一个极限数量的时候就flush这部分数据,再依次处理余下的数据,这个在大多数场景能够满足需求。
读有大量数据的文件时,使用WorkBook处理就不行了,因为POI对文件是先将文件中的cell读入内存,生成一个树的结构(针对Excel中的每个sheet,使用TreeMap存储sheet中的行)。
如果数据量比较大,则同样会产生java.lang.OutOfMemoryError: Java heap space错误。POI官方推荐使用“XSSF and SAX(event API)”方式来解决。
分析清楚POI后要解决OOM有3个关键.
- 读取的数据转换流程
- easyexcel解析数据的 设计思想和相关角色。
根据上面官网给的信息,我们得有个模型来接收每行的数据,本例用CommonUser
对象,该对象上在这上面也可以加数据校验,还需要个解析每个行的监听器CommonUserListener
,可以来处理每行的数据,然后进行数据库操作读写。
来个小demo(用的mybatis-plus框架)
controler
@RestController
@RequestMapping("info/commonuser")
public class CommonUserController {
@Autowired
private CommonUserService commonUserService;
/**
* excel导入(按照行读)
* <p>
* 1. 创建excel对应的实体对象 参照{@link CommonUser}
* <p>
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link CommonUserListener}
* <p>
* 3. 直接读即可
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), CommonUser.class, new CommonUserListener(commonUserService))
.sheet()
.doRead();
return "success";
}
/**
* 文件下载(失败了会返回一个有部分数据的Excel)
* <p>
* 1. 创建excel对应的实体对象 参照{@link CommonUser}
* <p>
* 2. 设置返回的 参数
* <p>
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("用户表", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), CommonUser.class).sheet("模板").doWrite(data());
}
/**
* excel导出
* 文件下载并且失败的时候返回json(默认失败了会返回一个有部分数据的Excel)
*
* @since 2.1.1
*/
@GetMapping("downloadFailedUsingJson")
public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
try {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// 这里需要设置不关闭流
EasyExcel.write(response.getOutputStream(), CommonUser.class).autoCloseStream(Boolean.FALSE).sheet("模板")
.doWrite(data());
} catch (Exception e) {
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = new HashMap<String, String>();
map.put("status", "failure");
map.put("message", "下载文件失败" + e.getMessage());
response.getWriter().println(new Gson().toJson(map));
}
}
private List<CommonUser> data() {
List<CommonUser> list = commonUserService.list();
return list;
}
}
CommonUserService (和读excel无关,业务需要)
/**
* 用户Service
*
* @author hfl [email protected]
* @date 2020-05-16 08:42:50
*/
public interface CommonUserService extends IService<CommonUser> {
}
CommonUserServiceImpl
@Service("commonUserService")
public class CommonUserServiceImpl extends ServiceImpl<CommonUserMapper, CommonUser> implements CommonUserService {
private final static Logger logger = LoggerFactory.getLogger(CommonUserServiceImpl.class);
}
CommonUserListener(负责获取每行的数据,然后根据需要进行db保存)
public class CommonUserListener extends AnalysisEventListener<CommonUser> {
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
* mybatis-plus 默认1000
*/
private static final int BATCH_COUNT = 1000;
List<CommonUser> list = new ArrayList<>();
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private CommonUserService commonUserService;
public CommonUserListener(CommonUserService commonUserService) {
// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
this.commonUserService = commonUserService;
}
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
@Override
public void invoke(CommonUser data, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", new Gson().toJson(data));
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存