以 post 方式在 controller 层接收 excel 文件,controller 方法的参数设置为: @RequestParam(“excelFile”) MultipartFile file。
注意问题:
当收到 excel 文件时,当前线程(假设为线程 A)会在临时路径下创建一个临时文件(即 excel 文件内容), MultipartFile file 就指向这个临时文件。
处理 excel 文件往往需要比较长的时间,用同步处理不太合适,往往使用异步方式,将 MultipartFile 传递给异步线程,当异步线程(假设为线程 B)启动时,如果线程 A 结束了,那么临时文件会被删除,此时异步线程 B 手里的 MultipartFile 就没用了,因为实际上的 excel 内容已经没有了,MultipartFile 仅仅只包含 excel 文件的一些属性信息,比如文件名,文件大小等等,通过 MultipartFile 读不出任何内容。
解决方法:
线程 A 传递文件给线程 B 时,改为传递 InputStream,而不是 MultipartFile。 即 file.getInputStream();
EasyExcel 类什么都没有,只是简单继承了 EasyExcelFactory,因此,EasyExcelFactory 才是核心。
public class EasyExcel extends EasyExcelFactory {
public EasyExcel() {
}
}
从如下源码可以看出,EasyExcelFactory 使用了构造模式。 下列源码中,有非常多个 read 方法,最终使用的都是 ExcelReaderBuilder 类的构造模式来创建读 excel 的对象。
read 方法的参数无非就是:file(excel 文件),readListener(专门一行一行读取 excel 文件的工具),head(将 excel 按照头部映射为一个 class),pathName(excel 文件路径名),inputstream(excel 文件的输入流)。
关键在于看 ExcelReaderBuilder 究竟要构造怎么样的对象。
public static ExcelReaderBuilder read() {
return new ExcelReaderBuilder();
}
public static ExcelReaderBuilder read(File file) {
return read((File)file, (Class)null, (ReadListener)null);
}
public static ExcelReaderBuilder read(File file, ReadListener readListener) {
return read((File)file, (Class)null, readListener);
}
public static ExcelReaderBuilder read(File file, Class head, ReadListener readListener) {
ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
excelReaderBuilder.file(file);
if (head != null) {
excelReaderBuilder.head(head);
}
if (readListener != null) {
excelReaderBuilder.registerReadListener(readListener);
}
return excelReaderBuilder;
}
public static ExcelReaderBuilder read(String pathName) {
return read((String)pathName, (Class)null, (ReadListener)null);
}
public static ExcelReaderBuilder read(String pathName, ReadListener readListener) {
return read((String)pathName, (Class)null, readListener);
}
public static ExcelReaderBuilder read(String pathName, Class head, ReadListener readListener) {
ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
excelReaderBuilder.file(pathName);
if (head != null) {
excelReaderBuilder.head(head);
}
if (readListener != null) {
excelReaderBuilder.registerReadListener(readListener);
}
return excelReaderBuilder;
}
public static ExcelReaderBuilder read(InputStream inputStream) {
return read((InputStream)inputStream, (Class)null, (ReadListener)null);
}
public static ExcelReaderBuilder read(InputStream inputStream, ReadListener readListener) {
return read((InputStream)inputStream, (Class)null, readListener);
}
public static ExcelReaderBuilder read(InputStream inputStream, Class head, ReadListener readListener) {
ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
excelReaderBuilder.file(inputStream);
if (head != null) {
excelReaderBuilder.head(head);
}
if (readListener != null) {
excelReaderBuilder.registerReadListener(readListener);
}
return excelReaderBuilder;
}
ReadWorkbook 类记录的是读 excel 这个动作的所有属性,比如读取的是哪种 excel 文件(xls,还是 xlsx),哪个文件,是否忽略空行,等等。
具体有哪些属性,看看下面源码:
public class ReadWorkbook extends ReadBasicParameter {
private ExcelTypeEnum excelType; //读取哪类excel,这是枚举,包含 .xls 和 .xlsx
private InputStream inputStream; // 文件的输入流
private File file; // 文件
private Boolean mandatoryUseInputStream; // 是否强制使用文件输入流方式
private Boolean autoCloseStream; // 是否自动关闭输入流
private Object customObject; // 不清楚
private ReadCache readCache; // 缓存,当需要把 excel 的全部/部分读取到内存时,用缓存存储
private Boolean ignoreEmptyRow; // 是否忽略空行
private ReadCacheSelector readCacheSelector; // 不清楚
private String password; // 打开excel文件的密码
private String xlsxSAXParserFactoryName;
private Boolean useDefaultListener; // 是否使用默认的监听器
private Set<CellExtraTypeEnum> extraReadSet; // 额外读取单元格的哪些信息
}
public class ReadBasicParameter extends BasicParameter {
private Integer headRowNumber; // 表头为多少行
private List<ReadListener> customReadListenerList = new ArrayList();
}
public class BasicParameter {
private List<List<String>> head; // 不清楚怎么用
private Class clazz; // excel 数据映射的类
private List<Converter> customConverterList;
private Boolean autoTrim; // 自动去除首尾空格
private Boolean use1904windowing;
private Locale locale;
}
传递过程挺复杂,没必要扣细节。
最终会调用 ReadListener 的几个方法,ReadListener 是接口,我们可以实现接口,并实现接口的几个方法,这样,就可以达到在该几个方法中处理 excel 数据的目的。
public interface ReadListener<T> extends Listener {
void onException(Exception var1, AnalysisContext var2) throws Exception;
void invokeHead(Map<Integer, CellData> var1, AnalysisContext var2);
void invoke(T var1, AnalysisContext var2);
void extra(CellExtra var1, AnalysisContext var2);
void doAfterAllAnalysed(AnalysisContext var1);
boolean hasNext(AnalysisContext var1);
}
从下列例子可以看出,如果 excel 的数据量不是很大,且只是简单地把数据读取出来,可以直接使用默认的同步监听器来读取即可,默认监听器也是 invoke 方法一次次执行,将 excel 数据添加到一个 list 中,当读取完毕后,将 list 返回。注意,当 excel 非常大时,list 会占用非常大的内存,容易导致溢出,因此,建议后端在收到 excel 文件时,虽然无法判断 excel 包含多少条数据,但是可以判断一下文件的大小,比如不能超过 5M,超过则不处理。
head 方法是用于表示接收 excel 数据的类是什么,建议类的每个变量都设置成 String ,再做类型转换,在类型转换时,最好使用 try catch 捕获转换失败异常,以便给出具体的失败反馈。
sheet 方法用于表示读取第几页,默认读取第一页。
public class ExcelReadEntity {
// @ExcelProperty(index = 0, value = "标识")
private String id;
// @ExcelProperty(index = 1, value = "名字")
private String name;
// 省略 getter setter
}
List<ExcelReadEntity> list = EasyExcel.read(file.getInputStream())
.useDefaultListener(true)
.ignoreEmptyRow(true)
.autoCloseStream(true)
.mandatoryUseInputStream(true)
.excelType(ExcelTypeEnum.XLSX)
.head(ExcelReadEntity.class)
.sheet()
.headRowNumber(1)
.autoTrim(true)
.doReadSync();
System.out.println(list);
运行结果:
@ExcelProperty(index = 0, value = “标识”) 用以表示 excel 表头, index 表示匹配第几列,从 0 开始, value 表示匹配什么内容的表头。 index 和 value 二选一就可以,也可以同时使用,当同时使用时,index 与 value 是或的关系,只要有一个能匹配上就行。
当 excel 文件的数据量非常大时,处理完需要较长时间,如果同步读取,那将等待很久,考虑使用异步读取方式。 建议使用 try catch 包裹,这样就能接收到来自监听器中 onException 方法抛出的异常了,可以根据异常的消息来判断异常的类型,便于给用户反馈异常的原因。
try{
EasyExcel.read(file.getInputStream(), ExcelReadEntity.class, new ExcelReadListener())
.excelType(ExcelTypeEnum.XLSX)
.autoCloseStream(true)
.autoTrim(true)
.ignoreEmptyRow(true)
.sheet()
.headRowNumber(1)
.doRead();
} catch (IOException e) {
e.printStackTrace();
} catch (NotOfficeXmlFileException e){
log.info("文件类型不正确");
} catch (ExcelAnalysisException e){
log.info("文件解析出错");
} catch (Exception e){
log.info("未知异常");
}
具体的解析工作都在监听器里面完成,一般会根据实际业务要求,判断 excel 数据的合法性,如果不合法,则抛出异常,或者记录下来。抛出异常的话,建议抛出 ExcelAnalysisException。
public class ExcelReadListener extends AnalysisEventListener<ExcelReadEntity> {
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 处理头部
}
@Override
public void invoke(ExcelReadEntity excelReadEntity, AnalysisContext analysisContext) {
// 处理每一条数据
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 最后的工作
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 当解析过程中,抛出异常,都会进入到这个方法中,用于异常的处理,并且,进一步向外层抛出异常,
}
}
excel 的数据映射为 java 的类,列就对应成员变量,所以写 excel 就是将 List<映射对象> 写入一个文件中。
看看如下 EasyExcelFactory 中写 excel 的关键代码,也就是需要一个文件和映射类,表明将什么样类型的数据写入哪个文件中。
public static ExcelWriterBuilder write(File file, Class head)
public static ExcelWriterBuilder write(String pathName, Class head)
public static ExcelWriterBuilder write(OutputStream outputStream, Class head)
ExcelWriterBuilder 构造类又能设置哪些写属性呢?属性值是用 WriteWorkbook 类存储的。
public class WriteWorkbook extends WriteBasicParameter {
private ExcelTypeEnum excelType; // 写入哪种类型文件,xls 或者 xlsx
private File file; // excel 文件
private OutputStream outputStream; // excel 输出流
private InputStream templateInputStream;
private File templateFile;
private Boolean autoCloseStream; // 自动关闭
private Boolean mandatoryUseInputStream; // 是否强制使用输出流方式
private String password; // excel 文件密码
private Boolean inMemory; // excel 文件放在内存,还是放在磁盘
private Boolean writeExcelOnException;
}
public class WriteBasicParameter extends BasicParameter {
private Integer relativeHeadRowIndex; // 头部最后一行的索引值,从 0 开始
private Boolean needHead; // 是否需要头部
private List<WriteHandler> customWriteHandlerList = new ArrayList();
private Boolean useDefaultStyle; // 是否使用默认样式
private Boolean automaticMergeHead;
private Collection<Integer> excludeColumnIndexes;
private Collection<String> excludeColumnFiledNames;
private Collection<Integer> includeColumnIndexes;
private Collection<String> includeColumnFiledNames;
}
用一个例子:
try {
File excelFile = File.createTempFile("hehe", ".xlsx");
List<ExcelReadEntity> list = new ArrayList<>();
ExcelReadEntity entity = new ExcelReadEntity();
entity.setId(32535L);
entity.setName("张三");
list.add(entity);
EasyExcel.write(excelFile, ExcelReadEntity.class)
.excelType(ExcelTypeEnum.XLSX)
.autoCloseStream(true)
.inMemory(true)
.password("hello")
.sheet()
.sheetName("第一页")
.doWrite(list);
return excelFile;
} catch (Exception e) {
e.printStackTrace();
}