官方Github地址:https://github.com/alibaba/easyexcel
官方使用说明:Alibaba Easy Excel - 简单、省内存的Java解析Excel工具 | 首页
使用步骤:
- 在页面上新增导入按钮和文件选择框,代码参考:
<a id="btnImport" class="easyui-linkbutton" data-options="iconCls:'icon-save'" onclick="selectFile()" style="float: right;">导入</a> <input id="filebox" name="filebox" type="file" onchange="uploadFile()" style="float:right;display:none;" />
-
JavaScript调用后台方法,以JQuery为例:
-
function selectFile(){ $("#filebox").click(); } function uploadFile() { if(isLowerIE10()){ $.messager.alert('错误', "当前浏览器版本太低,请使用IE10及以上或者其他浏览器",'error'); } else { var file = $("#filebox").val(); if (file.endWith(".xlsx")) { var options = { type : 'post', url : "upload.do", dataType : 'json', complete : function(result) { $('#dg').datagrid("loaded"); var result = result.responseJSON; $.messager.alert('操作提示', result.msg,'info',function(){ $('#dg').datagrid('reload'); }); } } $('#dg').datagrid("loading"); $("#searchForm").ajaxSubmit(options); } else { $.messager.alert('错误', "请选择Excel2007以上版本文件,扩展名:xlsx",'error'); } } }
- 后台Controller接受文件流并传给业务层做进一步处理,代码参考
@RequestMapping("upload.do") @ResponseBody public Result upload(HttpServletRequest req, @RequestParam(value = "filebox") MultipartFile file) { Result result = Result.getInstanceError(); try { result = prodlineBugService.importByExcel(file); } catch (Exception e) { log.error("产线不良Excel导入异常:", e); result.setMsg(e.getMessage()); } return result; }
- 业务层service代码参考。接收到流之后转换成业务对象。
public Result importByExcel(MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), ProdlineBugVO.class, new ProdlineBugListener(this)).sheet().autoTrim(true).doRead(); return Result.getInstanceSuccess(); }
- 业务对象VO用注解标识一下列名。
@ExcelIgnoreUnannotated public class ProdlineBugVO { /** * ID */ @ExcelProperty(value = "ID") private Long id; /** * 期间(yyyy-mm) */ private String yearMonth; /** * 供应商编码 */ @ExcelProperty(value = "供应商编码") private String supplierCode; /** * 物料编码 */ @ExcelProperty(value = "物料编码") private String itemNumber; /** * 包含Y 不包含N */ @ExcelProperty(value = "不良数") private BigDecimal bugNum; /** * 总数 */ private BigDecimal totalNum; /** * 包含Y 不包含N */ @ExcelProperty(value = "工单号") private String icmoCode; /** * 描述 */ @ExcelProperty(value = "描述") private String description; /** * 季度考核ID */ private Long quarterExamineId; /** * 发生时间 */ @ExcelProperty(value = "发生日期") private Date actualTime; private String itemName; private String supplierName; /** excel行号 */ private Integer rowIndex; /** * 设置 ID */ public Long getId() { return id; } /** * 获取 ID */ public void setId(Long id) { this.id = id; } /** * 设置 期间(yyyy-mm) */ public String getYearMonth() { return yearMonth; } /** * 获取 期间(yyyy-mm) */ public void setYearMonth(String yearMonth) { this.yearMonth = yearMonth; } /** * 设置 供应商编码 */ public String getSupplierCode() { return supplierCode; } /** * 获取 供应商编码 */ public void setSupplierCode(String supplierCode) { this.supplierCode = supplierCode; } /** * 设置 物料编码 */ public String getItemNumber() { return itemNumber; } /** * 获取 物料编码 */ public void setItemNumber(String itemNumber) { this.itemNumber = itemNumber; } public BigDecimal getBugNum() { return bugNum; } public void setBugNum(BigDecimal bugNum) { this.bugNum = bugNum; } public BigDecimal getTotalNum() { return totalNum; } public void setTotalNum(BigDecimal totalNum) { this.totalNum = totalNum; } /** * 设置 包含Y 不包含N */ public String getIcmoCode() { return icmoCode; } /** * 获取 包含Y 不包含N */ public void setIcmoCode(String icmoCode) { this.icmoCode = icmoCode; } /** * 设置 描述 */ public String getDescription() { return description; } /** * 获取 描述 */ public void setDescription(String description) { this.description = description; } /** * 设置 季度考核ID */ public Long getQuarterExamineId() { return quarterExamineId; } /** * 获取 季度考核ID */ public void setQuarterExamineId(Long quarterExamineId) { this.quarterExamineId = quarterExamineId; } /** * 设置 发生时间 */ public Date getActualTime() { return actualTime; } /** * 获取 发生时间 */ public void setActualTime(Date actualTime) { this.actualTime = actualTime; } /** * 创建人(邮箱) */ private String createBy; /** * 创建时间 */ private Date createTime; /** * 更新人(邮箱) */ private String updateBy; /** * 更新时间 */ private Date updateTime; public String getCreateBy() { return createBy; } public void setCreateBy(String createBy) { this.createBy = createBy; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public String getUpdateBy() { return updateBy; } public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } public String getItemName() { return itemName; } public void setItemName(String itemName) { this.itemName = itemName; } public String getSupplierName() { return supplierName; } public void setSupplierName(String supplierName) { this.supplierName = supplierName; } public Integer getRowIndex() { return rowIndex; } public void setRowIndex(Integer rowIndex) { this.rowIndex = rowIndex; } }
- 定义好Listener来调用保存方法。
public class ProdlineBugListener extends AnalysisEventListener<ProdlineBugVO> { private Logger log = LoggerFactory.getLogger(this.getClass()); /** 一次处理的数量,防止一次处理的过多造成oom */ private static final int BATCH_COUNT = 5000; List<ProdlineBugVO> list = new ArrayList<ProdlineBugVO>(); private ProdlineBugService prodlineBugService; public ProdlineBugListener() { } public ProdlineBugListener(ProdlineBugService prodlineBugService) { this.prodlineBugService = prodlineBugService; } @Override public void invoke(ProdlineBugVO data, AnalysisContext context) { data.setRowIndex(context.readRowHolder().getRowIndex() + 1); list.add(data); if (list.size() >= BATCH_COUNT) { // 达到BATCH_COUNT再处理,防止内存中存储过大数据,容易OOM prodlineBugService.batchSave(list); list.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext arg0) { // 这里也要保存数据,确保最后遗留的和不到batch_count的数据也存储到数据库 prodlineBugService.batchSave(list); } }
注意事项
- 事务问题:如果不想Excel处理过程中被分成好多比事务,那请将读Excel的方法写到有事务控制的业务层。这样,当某一行内容校验失败,抛出异常,前面的操作就会回滚。
- 对于日期格式,可以用Date来接收。框架默认已经支持很多常用的数字格式,例如:"yyyy-MM-dd"等,但如果没有解析成功,那就需要使用@DateTimeFormat("yyyy年MM月dd日")注解配置
- @ExcelProperty注解支持按列名或列索引来配置表头,如果使用列名的方式,那即使列顺序变更也不会影响读数据。但千万不要2种方式同时配置。
- 对于少数Excel单元格内容格式不确定的情况(比如动态内容导入)。可能无法定义明确的VO值对象,可以使用Map<Integer, Object>来接受行内容。key是列索引。
- 如果单元格内容为空,map中不会有这个单元格对应的Entry,所以不要用EntrySet来循环
- 可以一开始就记录好表头的列数量,然后用这个列数量来循环Map,这样就知道哪一列是空的了。
- 如果Excel中有空行,框架会自动跳过不会调用invoke方法。
- autoTrim如果是true,读单元格内容时会自动trim。
- 框架默认第一行是表头,数据是从第二行开始的。如果不是可以使用headRowNumber方法指定。
- 该框架已经解决了POI头疼的性能问题而且简化了很多Excel读写的代码。更多功能可以参考官方github