赞
踩
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
github地址: https://github.com/alibaba/easyexcel
官方文档: https://www.yuque.com/easyexcel/doc/easyexcel
B站视频: https://www.bilibili.com/video/BV1Ff4y1U7Qc
Excel解析流程图:
EasyExcel读取Excel的解析原理:
添加maven依赖, 依赖的poi最低版本3.17
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency>
创建实体类,下面也用这个数据模型
@NoArgsConstructor @AllArgsConstructor @Data @Builder public class User { @ExcelProperty(value = "用户编号") private Integer userId; @ExcelProperty(value = "姓名") private String userName; @ExcelProperty(value = "性别") private String gender; @ExcelProperty(value = "工资") private Double salary; @ExcelProperty(value = "入职时间") private Date hireDate; // lombok 会生成getter/setter方法 }
写入
// 根据user模板构建数据
private List<User> getUserData() {
List<User> users = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
User user = User.builder()
.userId(i)
.userName("admin" + i)
.gender(i % 2 == 0 ? "男" : "女")
.salary(i * 1000.00)
.hireDate(new Date())
.build();
users.add(user);
}
return users;
}
@Test
public void testWriteExcel() {
String filename = "D:\\study\\excel\\user1.xlsx";
// 向Excel中写入数据 也可以通过 head(Class<?>) 指定数据模板
EasyExcel.write(filename, User.class)
.sheet("用户信息")
.doWrite(getUserData());
}
@Test
public void testWriteExcel2() {
String filename = "D:\\study\\excel\\user2.xlsx";
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(filename, User.class).build();
// 创建Sheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("用户信息").build();
// 向Excel中写入数据
excelWriter.write(getUserData(), writeSheet);
// 关闭流
excelWriter.finish();
}
指定字段不写入excel
@Test
public void testWriteExcel3() {
String filename = "D:\\study\\excel\\user3.xlsx";
// 设置排除的属性 也可以在数据模型的字段上加@ExcelIgnore注解排除
Set<String> excludeField = new HashSet<>();
excludeField.add("hireDate");
excludeField.add("salary");
// 写Excel
EasyExcel.write(filename, User.class)
.excludeColumnFiledNames(excludeField)
.sheet("用户信息")
.doWrite(getUserData());
}
@Test
public void testWriteExcel4() {
String filename = "D:\\study\\excel\\user4.xlsx";
// 设置要导出的字段
Set<String> includeFields = new HashSet<>();
includeFields.add("userName");
includeFields.add("hireDate");
// 写Excel
EasyExcel.write(filename, User.class)
.includeColumnFiledNames(includeFields)
.sheet("用户信息")
.doWrite(getUserData());
}
将Java对象中指定的属性, 插入到Eexcel表格中的指定列(在Excel表格中进行列排序), 使用index属性指定列顺序。
@NoArgsConstructor @AllArgsConstructor @Data @Builder public class User { @ExcelProperty(value = "用户编号", index = 0) private Integer userId; @ExcelProperty(value = "姓名", index = 1) private String userName; @ExcelProperty(value = "性别", index = 3) private String gender; @ExcelProperty(value = "工资", index = 4) private Double salary; @ExcelProperty(value = "入职时间", index = 2) private Date hireDate; // lombok 会生成getter/setter方法 }
@Test
public void testWriteExcel5() {
String filename = "D:\\study\\excel\\user5.xlsx";
// 向Excel中写入数据
EasyExcel.write(filename, User.class)
.sheet("用户信息")
.doWrite(getUserData());
}
@ExcelProperty注解的value属性是一个数组类型, 设置多个head时会自动合并
数据模板:
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class ComplexHeadUser {
@ExcelProperty(value = {"group1", "用户编号"}, index = 0)
private Integer userId;
@ExcelProperty(value = {"group1", "姓名"}, index = 1)
private String userName;
@ExcelProperty(value = {"group2", "入职时间"}, index = 2)
private Date hireDate;
// lombok 会生成getter/setter方法
}
写excel代码
@Test public void testWriteExcel6() { String filename = "D:\\study\\excel\\user6.xlsx"; List<ComplexHeadUser> users = new ArrayList<>(); for (int i = 1; i <= 10; i++) { ComplexHeadUser user = ComplexHeadUser.builder() .userId(i) .userName("大哥" + i) .hireDate(new Date()) .build(); users.add(user); } // 向Excel中写入数据 EasyExcel.write(filename, ComplexHeadUser.class) .sheet("用户信息") .doWrite(users); }
@Test
public void testWriteExcel7() {
String filename = "D:\\study\\excel\\user7.xlsx";
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(filename, User.class).build();
// 创建Sheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("用户信息").build();
// 向Excel的同一个Sheet重复写入数据
for (int i = 0; i < 2; i++) {
excelWriter.write(getUserData(), writeSheet);
}
// 关闭流
excelWriter.finish();
}
@Test
public void testWriteExcel8() {
String filename = "D:\\study\\excel\\user8.xlsx";
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(filename, User.class).build();
// 向Excel的同一个Sheet重复写入数据
for (int i = 0; i < 2; i++) {
// 创建Sheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("用户信息" + i).build();
excelWriter.write(getUserData(), writeSheet);
}
// 关闭流
excelWriter.finish();
}
对于日期和数字,有时候需要对其展示的样式进行格式化, EasyExcel提供了以下注解
@DateTimeFormat 日期格式化
@NumberFormat 数字格式化(小数或百分数)
数据模板对象:
@NoArgsConstructor @AllArgsConstructor @Data @Builder public class User { @ExcelProperty(value = "用户编号", index = 0) private Integer userId; @ExcelProperty(value = "姓名", index = 1) private String userName; @ExcelProperty(value = "性别", index = 3) private String gender; @ExcelProperty(value = "工资", index = 4) @NumberFormat(value = "###.#") // 数字格式化,保留1位小数 private Double salary; @ExcelProperty(value = "入职时间", index = 2) @DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒") // 日期格式化 private Date hireDate; // lombok 会生成getter/setter方法 }
写入
@Test
public void testWriteExcel9() {
String filename = "D:\\study\\excel\\user9.xlsx";
// 向Excel中写入数据
EasyExcel.write(filename, User.class)
.sheet("用户信息")
.doWrite(getUserData());
}
数据模板(Java对象)
@NoArgsConstructor @AllArgsConstructor @Data @Builder @ContentRowHeight(value = 100) // 内容行高 @ColumnWidth(value = 20) // 列宽 public class ImageData { //使用抽象文件表示一个图片 @ExcelProperty(value = "File类型") private File file; // 使用输入流保存一个图片 @ExcelProperty(value = "InputStream类型") private InputStream inputStream; // 当使用String类型保存一个图片的时候需要使用StringImageConverter转换器 @ExcelProperty(value = "String类型", converter = StringImageConverter.class) private String str; // 使用二进制数据保存为一个图片 @ExcelProperty(value = "二进制数据(字节)") private byte[] byteArr; // 使用网络链接保存为一个图片 @ExcelProperty(value = "网络图片") private URL url; // lombok 会生成getter/setter方法 }
写入
@Test public void testWriteImageToExcel() throws IOException { String filename = "D:\\study\\excel\\user10.xlsx"; // 图片位置 String imagePath = "D:\\study\\excel\\me.jpg"; // 网络图片 URL url = new URL("https://cn.bing.com/th?id=OHR.TanzaniaBeeEater_ZH-CN3246625733_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp"); // 将图片读取到二进制数据中 byte[] bytes = new byte[(int) new File(imagePath).length()]; InputStream inputStream = new FileInputStream(imagePath); inputStream.read(bytes, 0, bytes.length); List<ImageData> imageDataList = new ArrayList<>(); // 创建数据模板 ImageData imageData = ImageData.builder() .file(new File(imagePath)) .inputStream(new FileInputStream(imagePath)) .str(imagePath) .byteArr(bytes) .url(url) .build(); // 添加要写入的图片模型 imageDataList.add(imageData); // 写数据 EasyExcel.write(filename, ImageData.class) .sheet("帅哥") .doWrite(imageDataList); }
@HeadRowHeight(value = 30) // 头部行高
@ContentRowHeight(value = 25) // 内容行高
@ColumnWidth(value = 20) // 列宽, 可以作用在类或字段上
数据模板
@NoArgsConstructor @AllArgsConstructor @Data @Builder @HeadRowHeight(value = 30) // 头部行高 @ContentRowHeight(value = 25) // 内容行高 @ColumnWidth(value = 20) // 列宽 public class WidthAndHeightData { @ExcelProperty(value = "字符串标题") private String string; @ExcelProperty(value = "日期标题") private Date date; @ExcelProperty(value = "数字标题") @ColumnWidth(value = 25) private Double doubleData; // lombok 会生成getter/setter方法 }
写入
@Test public void testWrite11() { String filename = "D:\\study\\excel\\user11.xlsx"; // 构建数据 List<WidthAndHeightData> dataList = new ArrayList<>(); WidthAndHeightData data = WidthAndHeightData.builder() .string("字符串") .date(new Date()) .doubleData(888.88) .build(); dataList.add(data); // 向Excel中写入数据 EasyExcel.write(filename, WidthAndHeightData.class) .sheet("行高和列宽测试") .doWrite(dataList); }
@NoArgsConstructor @AllArgsConstructor @Data @Builder @HeadRowHeight(value = 30) // 头部行高 @ContentRowHeight(value = 25) // 内容行高 @ColumnWidth(value = 20) // 列宽 // 头背景设置成红色 IndexedColors.RED.getIndex() @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 10) // 头字体设置成20, 字体默认宋体 @HeadFontStyle(fontName = "宋体", fontHeightInPoints = 20) // 内容的背景设置成绿色 IndexedColors.GREEN.getIndex() @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 17) // 内容字体设置成20, 字体默认宋体 @ContentFontStyle(fontName = "宋体", fontHeightInPoints = 20) public class DemoStyleData { // 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex() @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 14) // 字符串的头字体设置成20 @HeadFontStyle(fontHeightInPoints = 30) // 字符串的内容背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex() @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40) // 字符串的内容字体设置成20,默认宋体 @ContentFontStyle(fontName = "宋体", fontHeightInPoints = 20) @ExcelProperty(value = "字符串标题") private String string; @ExcelProperty(value = "日期标题") private Date date; @ExcelProperty(value = "数字标题") private Double doubleData; // lombok 会生成getter/setter方法 }
写入
@Test public void testWrite12() { String filename = "D:\\study\\excel\\user12.xlsx"; // 构建数据 List<DemoStyleData> dataList = new ArrayList<>(); DemoStyleData data = DemoStyleData.builder() .string("字符串") .date(new Date()) .doubleData(888.88) .build(); dataList.add(data); // 向Excel中写入数据 EasyExcel.write(filename, DemoStyleData.class) .sheet("样式设置测试") .doWrite(dataList); }
数据模板
@NoArgsConstructor @AllArgsConstructor @Data @Builder @HeadRowHeight(value = 25) // 头部行高 @ContentRowHeight(value = 20) // 内容行高 @ColumnWidth(value = 20) // 列宽 /** * @OnceAbsoluteMerge 指定从哪一行/列开始,哪一行/列结束,进行单元格合并 * firstRowIndex 起始行索引,从0开始 * lastRowIndex 结束行索引 * firstColumnIndex 起始列索引,从0开始 * lastColumnIndex 结束列索引 */ // 例如: 第2-3行,2-3列进行合并 @OnceAbsoluteMerge(firstRowIndex = 1, lastRowIndex = 2, firstColumnIndex = 1, lastColumnIndex = 2) public class DemoMergeData { // 每隔两行合并一次(竖着合并单元格) // @ContentLoopMerge(eachRow = 2) @ExcelProperty(value = "字符串标题") private String string; @ExcelProperty(value = "日期标题") private Date date; @ExcelProperty(value = "数字标题") private Double doubleData; // lombok 会生成getter/setter方法 }
写入
@Test public void testWrite13() { String filename = "D:\\study\\excel\\user13.xlsx"; // 构建数据 List<DemoMergeData> dataList = new ArrayList<>(); DemoMergeData data = DemoMergeData.builder() .string("字符串") .date(new Date()) .doubleData(888.88) .build(); dataList.add(data); // 向Excel中写入数据 EasyExcel.write(filename, DemoMergeData.class) .sheet("单元格合并测试") .doWrite(dataList); }
@ContentLoopMerge
@OnceAbsoluteMerge
在实际应用场景中, 我们系统db存储的数据可以是枚举, 在界面或导出到Excel文件需要展示为对于的枚举值形式.
比如: 性别, 状态等. EasyExcel提供了转换器接口Converter供我们使用, 我们只需要自定义转换器实现接口, 并将自定义转换器类型传入要转换的属性字段中. 以下面的性别gender字段为例:
(1)数据模板
@NoArgsConstructor @AllArgsConstructor @Data @Builder public class UserModel { @ExcelProperty(value = "用户编号", index = 0) private Integer userId; @ExcelProperty(value = "姓名", index = 1) private String userName; // 性别添加了转换器, db中存入的是integer类型的枚举 0 , 1 ,2 @ExcelProperty(value = "性别", index = 3, converter = GenderConverter.class) private Integer gender; @ExcelProperty(value = "工资", index = 4) @NumberFormat(value = "###.#") private Double salary; @ExcelProperty(value = "入职时间", index = 2) @DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒") private Date hireDate; // lombok 会生成getter/setter方法 }
(2)自定义转换器
/** * 类描述:性别字段的数据转换器 * @Author wang_qz * @Date 2021/8/15 19:16 * @Version 1.0 */ public class GenderConverter implements Converter<Integer> { private static final String MALE = "男"; private static final String FEMALE = "女"; private static final String NONE = "未知"; // Java数据类型 integer @Override public Class supportJavaTypeKey() { return Integer.class; } // Excel文件中单元格的数据类型 string @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } // 读取Excel文件时将string转换为integer @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { String value = cellData.getStringValue(); if (Objects.equals(FEMALE, value)) { return 0; // 0-女 } else if (Objects.equals(MALE, value)) { return 1; // 1-男 } return 2; // 2-未知 } // 写入Excel文件时将integer转换为string @Override public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { if (value == 1) { return new CellData(MALE); } else if (value == 0) { return new CellData(FEMALE); } return new CellData(NONE); // 不男不女 } }
(3)导出到Excel的代码
@Test
public void testWriteExcel() {
String filename = "D:\\study\\excel\\user1.xlsx";
// 向Excel中写入数据
EasyExcel.write(filename, UserModel.class)
.sheet("用户信息")
.doWrite(getUserData());
}
// 根据user模板构建数据 private List<UserModel> getUserData() { List<UserModel> users = new ArrayList<>(); for (int i = 1; i <= 10; i++) { UserModel user = UserModel.builder() .userId(i) .userName("admin" + i) .gender(i % 2 == 0 ? 0 : 2) // 性别枚举 .salary(i * 1000 + 8.888) .hireDate(new Date()) .build(); users.add(user); } return users; }
在读取Excel表格数据时, 将读取的每行记录映射成一条LinkedHashMap记录, 而没有映射成实体类.
@Test public void testRead() { String filename = "D:\\study\\excel\\read.xlsx"; // 创建ExcelReaderBuilder对象 ExcelReaderBuilder readerBuilder = EasyExcel.read(); // 获取文件对象 readerBuilder.file(filename); // 指定映射的数据模板 // readerBuilder.head(DemoData.class); // 指定sheet readerBuilder.sheet(0); // 自动关闭输入流 readerBuilder.autoCloseStream(true); // 设置Excel文件格式 readerBuilder.excelType(ExcelTypeEnum.XLSX); // 注册监听器进行数据的解析 readerBuilder.registerReadListener(new AnalysisEventListener() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(Object demoData, AnalysisContext analysisContext) { // 如果没有指定数据模板, 解析的数据会封装成 LinkedHashMap返回 // demoData instanceof LinkedHashMap 返回 true System.out.println("解析数据为:" + demoData.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }); readerBuilder.doReadAll(); /* // 构建读取器 ExcelReader excelReader = readerBuilder.build(); // 读取Excel excelReader.readAll(); // 关闭流 excelReader.finish();*/ }
Excel数据类型
数据模板
注意: Java类中的属性字段顺序和Excel中的表头字段顺序一致, 可以不写@ExcelProperty
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class DemoData {
// 根据Excel中指定列名或列的索引读取
@ExcelProperty(value = "字符串标题", index = 0)
private String name;
@ExcelProperty(value = "日期标题", index = 1)
private Date hireDate;
@ExcelProperty(value = "数字标题", index = 2)
private Double salary;
// lombok 会生成getter/setter方法
}
读取excel代码
关键是写一个监听器,实现AnalysisEventListener, 每解析一行数据会调用invoke方法返回解析的数据, 当全部解析完成后会调用doAfterAllAnalysed方法. 我们重写invoke方法和doAfterAllAnalysed方法即可。
@Test public void testReadExcel() { // 读取的excel文件路径 String filename = "D:\\study\\excel\\read.xlsx"; // 读取excel EasyExcel.read(filename, DemoData.class, new AnalysisEventListener<DemoData>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(DemoData demoData, AnalysisContext analysisContext) { System.out.println("解析数据为:" + demoData.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }).sheet().doRead(); }
读excel的方式二代码
@Test public void testReadExcel2() { // 读取的excel文件路径 String filename = "D:\\study\\excel\\read.xlsx"; // 创建一个数据格式来装读取到的数据 Class<DemoData> head = DemoData.class; // 创建ExcelReader对象 ExcelReader excelReader = EasyExcel.read(filename, head, new AnalysisEventListener<DemoData>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(DemoData demoData, AnalysisContext analysisContext) { System.out.println("解析数据为:" + demoData.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }).build(); // 创建sheet对象,并读取Excel的第一个sheet(下标从0开始), 也可以根据sheet名称获取 ReadSheet sheet = EasyExcel.readSheet(0).build(); // 读取sheet表格数据, 参数是可变参数,可以读取多个sheet excelReader.read(sheet); // 需要自己关闭流操作,在读取文件时会创建临时文件,如果不关闭,会损耗磁盘,严重的磁盘爆掉 excelReader.finish(); }
要读取的源数据, 日期格式是yyyy年MM月dd日 HH时mm分ss秒, 数字带小数点
数据模板
@NoArgsConstructor @AllArgsConstructor @Data @Builder public class DemoData { @ExcelProperty(value = "字符串标题", index = 0) private String name; @ExcelProperty(value = "日期标题", index = 1) // 格式化日期类型数据 @DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒") private Date hireDate; @ExcelProperty(value = "数字标题", index = 2) // 格式化数字类型数据,保留一位小数 @NumberFormat(value = "###.#") private String salary; //注意: @NumberFormat对于Double类型的数据格式化会失效,建议使用String类型接收数据进行格式化 // private Double salary; // lombok 会生成getter/setter方法 }
读取excel代码同上面读取方式一样.
(1)读所有sheet
读方式一:使用ExcelReaderBuilder#doReadAll方法
@Test public void testReadExcel() { // 读取的excel文件路径 String filename = "D:\\study\\excel\\read.xlsx"; // 读取excel EasyExcel.read(filename, DemoData.class, new AnalysisEventListener<DemoData>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(DemoData demoData, AnalysisContext analysisContext) { System.out.println("解析数据为:" + demoData.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }) // .sheet(0).doRead(); .doReadAll(); // 读取全部sheet }
读方式二:使用ExcelReader#readAll方法
@Test public void testReadExcel2() { // 读取的excel文件路径 String filename = "D:\\study\\excel\\read.xlsx"; // 创建一个数据格式来装读取到的数据 Class<DemoData> head = DemoData.class; // 创建ExcelReader对象 ExcelReader excelReader = EasyExcel.read(filename, head, new AnalysisEventListener<DemoData>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(DemoData demoData, AnalysisContext analysisContext) { System.out.println("解析数据为:" + demoData.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }).build(); // 创建sheet对象,并读取Excel的第一个sheet(下标从0开始), 也可以根据sheet名称获取 ReadSheet sheet = EasyExcel.readSheet(0).build(); // 读取sheet表格数据 , 参数是可变参数,可以读取多个sheet // excelReader.read(sheet); excelReader.readAll(); // 读所有sheet // 需要自己关闭流操作,在读取文件时会创建临时文件,如果不关闭,会损耗磁盘,严重的磁盘爆掉 excelReader.finish(); }
(2)读指定的多个sheet
不同sheet表格的数据模板可能不一样,这时候就需要分别构建不同的sheet对象,分别为其指定对于的数据模板.
@Test public void testReadExcel3() { // 读取的excel文件路径 String filename = "D:\\study\\excel\\read.xlsx"; // 构建ExcelReader对象 ExcelReader excelReader = EasyExcel.read(filename).build(); // 构建sheet对象 ReadSheet sheet0 = EasyExcel.readSheet(0) .head(DemoData.class) // 指定sheet0的数据模板 .registerReadListener(new AnalysisEventListener<DemoData>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(DemoData demoData, AnalysisContext analysisContext) { System.out.println("解析数据为:" + demoData.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }).build(); // 读取sheet,有几个就构建几个sheet进行读取 excelReader.read(sheet0); // 需要自己关闭流操作,在读取文件时会创建临时文件,如果不关闭,会损耗磁盘,严重的磁盘爆掉 excelReader.finish(); }
上面的写已经提到了转换器, 读也是一样. 将Excel文件中的字符串枚举值转换成要存入db的整数类型的枚举。
和上面 (14)一样
(1)创建Excel模板格式
填充单个属性使用{}作为占位符, 在大括号里面定义属性名称, 如果{}想不作为占位符展示出来,可以使用反斜杠进行转义
填充数据的Java类(数据模板)
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class FillData {
private String name;
private double number;
// lombok 会生成getter/setter方法
}
填充的代码
@Test
public void testFillExcel() {
// 根据哪个模板进行填充
String template = "D:\\study\\excel\\template.xlsx";
// 填充完成之后的excel
String fillname = "D:\\study\\excel\\fill.xlsx";
// 构建数据
FillData fillData = FillData.builder()
.name("小米")
.number(888.888)
.build();
// 填充excel 单组数据填充
EasyExcel.write(fillname).withTemplate(template).sheet(0).doFill(fillData);
}
(1)列表填充
(2)填充的数据模板
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class FillData {
private String name;
private double number;
// lombok 会生成getter/setter方法
}
(3)填充Excel代码
@Test public void testFillExcel2() { // 根据哪个模板进行填充 String template = "D:\\study\\excel\\template2.xlsx"; // 填充完成之后的excel String fillname = "D:\\study\\excel\\fill2.xlsx"; // 填充excel 多组数据重复填充 EasyExcel.write(fillname) .withTemplate(template) .sheet(0) .doFill(getFillData()); } // 构建数据 private List<FillData> getFillData() { List<FillData> fillDataList = new ArrayList<>(); for (int i = 1; i <= 10; i++) { // 构建数据 FillData fillData = FillData.builder() .name("小米" + i) .number(i * 1000 + 88.88) .build(); fillDataList.add(fillData); } return fillDataList; }
(1)创建Excel填充模板
(2)填充的数据模板
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class FillData {
private String name;
private double number;
// lombok 会生成getter/setter方法
}
(3)组合填充Excel代码
@Test public void testFillExcel3() { // 根据哪个模板进行填充 String template = "D:\\study\\excel\\template3.xlsx"; // 填充完成之后的excel String fillname = "D:\\study\\excel\\fill3.xlsx"; // 创建填充配置 换行填充 FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build(); // 创建写对象 ExcelWriter excelWriter = EasyExcel.write(fillname).withTemplate(template).build(); // 创建Sheet对象 WriteSheet sheet = EasyExcel.writerSheet(0).build(); // 多组填充excel excelWriter.fill(getFillData(), fillConfig, sheet); // 单组填充 HashMap<String, Object> unitData = new HashMap<>(); unitData.put("nickname", "张三"); unitData.put("salary", 8088.66); excelWriter.fill(unitData, sheet); // 关闭流 excelWriter.finish(); }
如果没有设置填充配置换行FillConfig为true , 效果将是单组填充的数据会覆盖所在行的多组数据填充效果.
FillConfig fillConfig = FillConfig.builder().forceNewRow(false).build();
(1)创建Excel填充模板
(2)数据模板
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class FillData {
private String name;
private double number;
// lombok 会生成getter/setter方法
}
(3)水平填充代码
@Test public void testFillExcel4() { // 根据哪个模板进行填充 String template = "D:\\study\\excel\\template4.xlsx"; // 填充完成之后的excel String fillname = "D:\\study\\excel\\fill4.xlsx"; // 创建填充配置 水平填充 FillConfig fillConfig = FillConfig.builder() // .forceNewRow(true) .direction(WriteDirectionEnum.HORIZONTAL).build(); // 创建写对象 ExcelWriter excelWriter = EasyExcel.write(fillname, FillData.class).withTemplate(template).build(); // 创建Sheet对象 WriteSheet sheet = EasyExcel.writerSheet(0).build(); // 多组填充excel excelWriter.fill(getFillData(), fillConfig, sheet); // 关闭流 excelWriter.finish(); }
(4)效果
(1)创建Excel填充模板
(2)会员数据模板
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class MemberVip {
private Integer id;
private String name;
private String gender;
private String birthday;
// lombok 会生成getter/setter方法jav
}
(3)组合填充报表代码
@Test public void testFillExcel5() { // 根据哪个模板进行填充 String template = "D:\\study\\excel\\template5.xlsx"; // 填充完成之后的excel String fillname = "D:\\study\\excel\\fill5.xlsx"; // 创建填充配置 FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build(); // 创建写对象 ExcelWriter excelWriter = EasyExcel.write(fillname) .withTemplate(template).build(); // 创建Sheet对象 WriteSheet sheet = EasyExcel.writerSheet(0).build(); /***准备数据 start*****/ HashMap<String, Object> dateMap = new HashMap<>(); dateMap.put("date", "2021-08-08"); HashMap<String, Object> memberMap = new HashMap<>(); memberMap.put("increaseCount", 500); memberMap.put("totalCount", 999); HashMap<String, Object> curMonthMemberMap = new HashMap<>(); curMonthMemberMap.put("increaseCountWeek", 100); curMonthMemberMap.put("increaseCountMonth", 200); List<MemberVip> memberVips = getMemberVips(); /***准备数据 end*****/ // 多组填充excel excelWriter.fill(dateMap, sheet); excelWriter.fill(memberMap, sheet); excelWriter.fill(curMonthMemberMap, sheet); excelWriter.fill(memberVips, fillConfig, sheet); // 关闭流 excelWriter.finish(); }
(4)效果
(1)数据模板
@NoArgsConstructor @AllArgsConstructor @Data @Builder @HeadRowHeight(value = 30) @ContentRowHeight(value = 25) @ColumnWidth(value = 30) public class UserExcel { @ExcelProperty(value = "用户编号") private Integer userId; @ExcelProperty(value = "姓名") private String username; @ExcelProperty(value = "性别") private String gender; @ExcelProperty(value = "工资") private Double salary; @ExcelProperty(value = "入职时间") private Date hireDate; }
(2)编写controller及下载handler
** * 使用EasyExcel操作excel文件上传/下载 */ @Controller @RequestMapping(value = "/xlsx") public class EasyExcelController { @RequestMapping("/toExcelPage") public String todownloadPage() { return "excelPage"; } /** * 下载Excel * @param request * @param response */ @RequestMapping(value = "/downloadExcel") public void downloadExcel(HttpServletRequest request, HttpServletResponse response) throws Exception { // 设置响应头 response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); // 设置防止中文名乱码 String filename = URLEncoder.encode("员工信息", "utf-8"); // 文件下载方式(附件下载还是在当前浏览器打开) response.setHeader("Content-disposition", "attachment;filename=" + filename + ".xlsx"); // 构建写入到excel文件的数据 List<UserExcel> userExcels = new ArrayList<>(); UserExcel userExce1 = new UserExcel(1001, "张三", "男", 1333.33, new Date()); UserExcel userExce2 = new UserExcel(1002, "李四", "男", 1356.83, new Date()); UserExcel userExce3 = new UserExcel(1003, "王五", "男", 1883.66, new Date()); UserExcel userExce4 = new UserExcel(1004, "赵六", "男", 1393.39, new Date()); userExcels.add(userExce1); userExcels.add(userExce2); userExcels.add(userExce3); userExcels.add(userExce4); // 写入数据到excel EasyExcel.write(response.getOutputStream(), UserExcel.class) .sheet("用户信息") .doWrite(userExcels); } }
(3)编写jsp页面 excelPage.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>测试excel文件下载</title>
</head>
<body>
<h3>点击下面链接, 进行excel文件下载</h3>
<a href="<c:url value='/xlsx/downloadExcel'/>">Excel文件下载</a>
</body>
</html>
(4)启动tomcat测试
访问 http://localhost:8080/mvc/xlsx/toExcelPage 跳转到excel文件下载界面
点击"Excel文件下载", 查看下载文件
(1)数据模板跟上面下载一样
(2)编写上传handler
@RequestMapping("/uploadExcel") public void uploadExcel(HttpServletRequest request, HttpServletResponse response) throws Exception { DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload fileUpload = new ServletFileUpload(factory); // 设置单个文件大小为3M 2的10次幂=1024 fileUpload.setFileSizeMax((long) (3 * Math.pow(2, 20))); // 总文件大小为30M fileUpload.setSizeMax((long) (30 * Math.pow(2, 20))); List<FileItem> list = fileUpload.parseRequest(request); for (FileItem fileItem : list) { // 判断是否为附件 if (!fileItem.isFormField()) { // 是附件 InputStream inputStream = fileItem.getInputStream(); EasyExcel.read(inputStream, UserExcel.class, new AnalysisEventListener<UserExcel>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(UserExcel data, AnalysisContext analysisContext) { System.out.println("解析数据为:" + data.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }).sheet().doRead(); } } }
上面方式不知道啥原因, 通过FileItem获取不到文件, 改为下面方式Part获取上传的文件
@RequestMapping("/uploadExcel") @ResponseBody public String uploadExcel(@RequestParam("file") Part part) throws Exception { // 获取上传的文件流 InputStream inputStream = part.getInputStream(); // 读取Excel EasyExcel.read(inputStream, UserExcel.class, new AnalysisEventListener<UserExcel>() { // 每解析一行数据,该方法会被调用一次 @Override public void invoke(UserExcel data, AnalysisContext analysisContext) { System.out.println("解析数据为:" + data.toString()); } // 全部解析完成被调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("解析完成..."); // 可以将解析的数据保存到数据库 } }).sheet().doRead(); return "上传Excel文件成功"; }
(3)编写jsp页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>测试excel文件下载</title> </head> <body> <h3>点击下面链接, 进行excel文件下载</h3> <a href="<c:url value='/xlsx/downloadExcel'/>">Excel文件下载</a> <hr/> <hr/> <h3>点击下面按钮, 进行excel文件上传</h3> <form action="<c:url value='/xlsx/uploadExcel'/>" method="post" enctype="multipart/form-data"> <input type="file" name="file"/><br/> <input type="submit" value="上传Excel"/> </form> </body> </html>
(4)启动tomcat, 测试
访问 http://localhost:8080/mvc/xlsx/toExcelPage ,跳转到Excel文件上传页面
读取前端页面上传的Excel是成功了 , 但是中文乱码问题有待解决.
中文乱码解决参考: https://blog.csdn.net/gaogzhen/article/details/107307459
上面章节的读取Excel的程序弊端:
(1)每次解析不同数据模型都要新增一个监听器, 重复工作量大;
(2)即使用了匿名内部类,程序也显得臃肿;
(3)数据处理一般都会存在于项目的service中, 监听器难免会依赖dao层, 导致程序耦合度高
解决方案:
(1)通过泛型指定数据模型类型, 针对不同类型的数据模型只需要定义一个监听器即可;
(2)使用jdk8新特性中的函数式接口, 将数据处理从监听器中剥离出去, 进行解耦
监听器代码
/** * 类描述:easyexcel工具类 * @Author wang_qz * @Date 2021/8/15 18:15 * @Version 1.0 */ public class EasyExcelUtils<T> { /** * 获取读取Excel的监听器对象 * 为了解耦及减少每个数据模型bean都要创建一个监听器的臃肿, 使用泛型指定数据模型类型 * 使用jdk8新特性中的函数式接口 Consumer * 可以实现任何数据模型bean的数据解析, 不用重复定义监听器 * @param consumer 处理解析数据的函数, 一般可以是数据入库逻辑的函数 * @param threshold 阈值,达到阈值就处理一次存储的数据 * @param <T> 数据模型泛型 * @return 返回监听器 */ public static <T> AnalysisEventListener<T> getReadListener(Consumer<List<T>> consumer, int threshold) { return new AnalysisEventListener<T>() { /** * 存储解析的数据 T t */ // ArrayList基于数组实现, 查询更快 // List<T> dataList = new ArrayList<>(threshold); // LinkedList基于双向链表实现, 插入和删除更快 List<T> dataList = new LinkedList<>(); /** * 每解析一行数据事件调度中心都会通知到这个方法, 订阅者1 * @param data 解析的每行数据 * @param context */ @Override public void invoke(T data, AnalysisContext context) { dataList.add(data); // 达到阈值就处理一次存储的数据 if (dataList.size() >= threshold) { consumer.accept(dataList); dataList.clear(); } } /** * excel文件解析完成后,事件调度中心会通知到该方法, 订阅者2 * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 最后阈值外的数据做处理 if (dataList.size() > 0) { consumer.accept(dataList); } } }; } /** * 获取读取Excel的监听器对象, 不指定阈值, 默认阈值为 2000 * @param consumer * @param <T> * @return */ public static <T> AnalysisEventListener<T> getReadListener(Consumer<List<T>> consumer) { return getReadListener(consumer, 2000); } }
再来看读取Excel的 代码:
/** * 采用解耦的自定义监听器读取Excel, 可以实现任何数据模型bean的读取 */ @Test public void testReadExcelN() { // 读取的excel文件路径 String filename = "D:\\study\\excel\\user1.xlsx"; // 读取excel EasyExcel.read(filename, UserModel.class, EasyExcelUtils.getReadListener(dataProcess())) .doReadAll(); // 读取全部sheet } /** * 传给监听器的是一个处理解析数据的函数, 当调用consumer的accept方法时就会调用传递的函数逻辑 * 这里传递的函数是对解析结果集的遍历打印操作, 也可以是数据入库操作 * @return */ public Consumer<List<UserModel>> dataProcess() { Consumer<List<UserModel>> consumer = users -> users.forEach(System.out::println); return consumer; }
遇到的问题:文件有数据, EasyExcel读取的数据全为null的坑, 看图。
原因及解决方案: https://blog.csdn.net/qq_19309473/article/details/111322185
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency>
@Data @Slf4j public class CloudVerifyRuleExcelListener extends AnalysisEventListener<Map<Integer, String>> { /** * cachedDataList */ private List<Map<Integer, String>> cachedDataList = Lists.newArrayList(); /** * 获取excel解析结果 * * @param multipartFile 导入文件 * @param sheetNo 页数 * @param headRowNumber 表头所在行数 * @return List */ public static List<Map<Integer, String>> getDataList(MultipartFile multipartFile, int sheetNo, int headRowNumber) throws IOException { log.info("解析文件开始"); CloudVerifyRuleExcelListener noModelDataListener = new CloudVerifyRuleExcelListener(); EasyExcel.read(multipartFile.getInputStream(), noModelDataListener) .headRowNumber(headRowNumber) .sheet(sheetNo) .doRead(); List<Map<Integer, String>> dataList = noModelDataListener.getCachedDataList(); log.info("解析文件结束"); return dataList; } /** * 读取excel内容,一行一行进行读取,进行业务处理 */ @Override public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) { cachedDataList.add(data); } /** * 读取完成后执行 */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { log.info("所有数据解析完成!"); } }
@Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor @Schema(description = "云检验-检查规则创建请求对象") public class CloudVerifyRuleImportRequest { /** * 云检验id */ @Schema(description = "云检验id") private Long cloudVerifyId; /** * 规则编码 */ @ExcelProperty(value = "规则编码", index = 0) @Schema(description = "规则编码") private String ruleCd; /** * 系统名 */ @ExcelProperty(value = "系统名", index = 1) @Schema(description = "系统名") private String sysNm; /** * 是否启用 */ @ExcelProperty(value = "是否启用", converter = BooleanConverter.class, index = 6) @Schema(description = "是否启用") private Boolean initFlag; /** * 告警启始时间 */ @ExcelProperty(value = "告警启始日期", converter = LocalDateStringConverter.class, index = 8) @Schema(description = "告警启始日期") private LocalDateTime warnStratTm; }
public class BooleanConverter implements Converter<Boolean> { @Override public Class supportJavaTypeKey() { return null; } @Override public CellDataTypeEnum supportExcelTypeKey() { return null; } @Override public Boolean convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { Boolean b = false; if ("是".equals(cellData.getStringValue())) { b = true; } return b; } @Override public CellData convertToExcelData(Boolean value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return null; } }
public class LocalDateStringConverter implements Converter<LocalDateTime> { @Override public Class supportJavaTypeKey() { return LocalDateTime.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { return LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern(NORM_DATE_PATTERN)) .atStartOfDay(); } @Override public CellData convertToExcelData(LocalDateTime localDateTime,ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(NORM_DATE_PATTERN); String format = formatter.format(localDateTime); return new CellData(format); } }
@PostMapping(value = "/import")
@Operation(summary = "导入云检验-检查作业", description = "导入云检验-检查作业")
public ApiResponse<Void> importExcel(@RequestParam("cloudVerifyJobFile") MultipartFile cloudVerifyJobFile,
@RequestParam("cloudVerifyRuleFile")
MultipartFile cloudVerifyRuleFile) throws IOException {
return cloudVerifyJobAppService.importExcel(cloudVerifyJobFile, cloudVerifyRuleFile);
}
@Transactional(rollbackFor = Exception.class) public ApiResponse<Void> importExcel(MultipartFile cloudVerifyJobFile, MultipartFile cloudVerifyRuleFile) throws IOException { // 检验云检验-检查作业文件 // 校验导入文件格式是否为xls或xlsx ApiResponse<Void> fileTypeResponseJob = ExcelUtils.checkFileType(cloudVerifyJobFile); if (ComConstants.FAILED.equals(fileTypeResponseJob.getCode())) { return fileTypeResponseJob; } // 校验文件中的表头是否匹配 List<Map<Integer, String>> sheetListJob = CloudVerifyJobExcelListener.getDataList(cloudVerifyJobFile, 0, 0); Map<Integer, String> indexAndNmMapSheetJob = sheetListJob.get(0); ApiResponse<Void> sheetHeadResponseJob = ExcelUtils.invokeHeadMap(indexAndNmMapSheetJob, CloudVerifyJobImportRequest.class); if (ComConstants.FAILED.equals(sheetHeadResponseJob.getCode())) { return sheetHeadResponseJob; } // 检验云检验-检查规则文件 // 校验导入文件格式是否为xls或xlsx ApiResponse<Void> fileTypeResponseRule = ExcelUtils.checkFileType(cloudVerifyRuleFile); if (ComConstants.FAILED.equals(fileTypeResponseRule.getCode())) { return fileTypeResponseRule; } List<Map<Integer, String>> sheetListRule = CloudVerifyRuleExcelListener.getDataList(cloudVerifyRuleFile, 0, 0); Map<Integer, String> indexAndNmMapSheetRule = sheetListRule.get(0); ApiResponse<Void> sheetHeadResponseRule = ExcelUtils.invokeHeadMap(indexAndNmMapSheetRule, CloudVerifyRuleImportRequest.class); if (ComConstants.FAILED.equals(sheetHeadResponseRule.getCode())) { return sheetHeadResponseRule; } // 解析云检验-检查作业excel数据 // 第一行是说明,headRowNumber = 1 List<CloudVerifyJobImportRequest> jobImportRequestList = EasyExcel.read(cloudVerifyJobFile.getInputStream()) .head(CloudVerifyJobImportRequest.class) .sheet() .headRowNumber(1) .doReadSync(); // 校验文件中的数据是否为空 if (CollectionUtils.isEmpty(jobImportRequestList)) { return ApiResponse.fail("导入的云检验-检查作业为空模板,请检查后重新导入"); } // 解析云检验-检查规则excel数据 List<CloudVerifyRuleImportRequest> ruleImportRequestList = EasyExcel.read(cloudVerifyRuleFile.getInputStream()) .head(CloudVerifyRuleImportRequest.class) .sheet() .headRowNumber(1) .doReadSync(); // 校验文件中的数据是否为空 if (CollectionUtils.isEmpty(ruleImportRequestList)) { return ApiResponse.fail("导入的云检验-检查规则为空模板,请检查后重新导入"); } // 将云检验-检查规则的数据根据作业名分成不同的List存在Map中 HashMap<String, List<CloudVerifyRuleImportRequest>> ruleMap = Maps.newHashMap(); ruleImportRequestList.forEach(ruleImportRequest -> { String jobName = ruleImportRequest.getJobNm(); if (ruleMap.get(jobName) == null) { List<CloudVerifyRuleImportRequest> arrayList = Lists.newArrayList(); arrayList.add(ruleImportRequest); ruleMap.put(jobName, arrayList); } else { List<CloudVerifyRuleImportRequest> arrayList = ruleMap.get(jobName); arrayList.add(ruleImportRequest); ruleMap.put(jobName, arrayList); } }); jobImportRequestList.forEach(jobImportRequest -> { // 云检验-检查作业表创建对象 转成 云检验-检查作业表实体 CloudVerifyJob cloudVerifyJob = cloudVerifyJobDTOAssembler.assembler(jobImportRequest); long id = IdWorker.getId(); cloudVerifyJob.setId(id); // 调用创建云检验-检查作业领域服务 cloudVerifyJobService.create(cloudVerifyJob); // 通过关联的作业名找到检查作业对应的检查规则list String jobName = jobImportRequest.getJobNm(); List<CloudVerifyRuleImportRequest> ruleImportList = ruleMap.get(jobName); if (ruleImportList != null) { ruleImportList.forEach(ruleImport -> { // 完成云检验-检查规则的插入 CloudVerifyRule cloudVerifyRule = cloudVerifyRuleConfigDTOAssembler.assembler(ruleImport); cloudVerifyRule.setCloudVerifyId(cloudVerifyJob.getId()); cloudVerifyRuleService.create(cloudVerifyRule); }); } // 完成配置修订信息的插入 ConfigRevision configRevision = new ConfigRevision(); configRevision.setProjId(DEFAULT_ID); configRevision.setTaskId(DEFAULT_ID); configRevision.setFormDataId(id); configRevision.setConfigType(DevTreeNodeType.CLOUD_VERIFY_CONFIG.name()); configRevision.setConfigNm(jobImportRequest.getJobNm()); configRevisionService.create(configRevision); }); // 领域响应结果转成API响应结果 return ApiResponse.ok(); }
@PostMapping(value = "/export")
@Operation(summary = "导出云检验配置", description = "导出云检验配置")
public ApiResponse<String> export(@RequestBody @Valid CloudVerifyJobQueryRequest queryRequest) throws IOException {
return cloudVerifyJobAppService.export(queryRequest);
}
public DomainResponse<String> export(CleanStrategyQueryRequest queryRequest) { if (StrUtil.isBlank(queryRequest.getPath())) { throw new YTRuntimeException("导出路径不能为空"); } List<CleanStrategyQueryResponse> list = this.findAll(queryRequest) .getData(); // 如果没有传文件名称,就用默认的文件名称 String filePath; if (StrUtil.isBlank(queryRequest.getFileName())) { filePath = queryRequest.getPath() + "清理策略.xlsx"; } else { filePath = queryRequest.getPath() + queryRequest.getFileName(); } EasyExcel.write(filePath, CleanStrategyQueryResponse.class) .registerWriteHandler(new Customhandler()) .sheet("清理策略") .doWrite(list); return DomainResponse.ok("导出成功!"); }
Hutool官网:https://www.hutool.cn/
(1)导入相关依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.0.7</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>3.17</version>
</dependency>
(2)实体类
/**
* @description 用户
* @author ln
*/
@Data
public class User {
private String id;
private String name;
private String nickname;
private String sex;
private String age;
}
(1)service层-准备导入的数据集
/** * 获取并组装导出数据 */ public void getUserList() { List<User> userList=new ArrayList<>(); userList.add(new("1","用户1","yonghu1","女","22")); userList.add(new("2","用户2","yonghu2","男","24")); userList.add(new("3","用户3","yonghu3","女","24")); userList.add(new("4","用户4","yonghu4","男","23")); //导出文件名 String fileName="USER.csv"; //自定义标题别名,作为第一行listName String[] listName = new String[3]; listName[0]="用户名"; listName[1]="性别"; listName[2]="年龄"; //获取String[]类型的数据至result中 List result= new ArrayList<String[]>(); //将listName添加到result中 result.add(listName); for(User user:userList){ //遍历用户列表的数据 Field[] declaredFields =new Field[3]; try { //将对象要显示的字段插入对应标题位置 declaredFields[0]=user.getClass().getDeclaredField("name"); declaredFields[1]=user.getClass().getDeclaredField("sex"); declaredFields[2]=user.getClass().getDeclaredField("age"); //获取UserList对象的数据作为listValue String[] listValue = new String[declaredFields.length]; for(int i=0;i<listValue.length;i++){ declaredFields[i].setAccessible(true); //设置为允许操作 //将属性值添加到listValue对应的索引位置下 listValue[i] = String.valueOf(declaredFields[i].get(user)) ; //将listValue添加到result中 } result.add(listValue); } catch (Exception e) { e.printStackTrace(); } } //将组装好的结果集传入导出csv格式的工具类 this.createCsvFile(result,fileName); }
4-导出csv格式工具类
/** * 导出csv格式工具类 * @param result 导出数据 * @param fileName 文件名 */ public void createCsvFile(List result,String fileName){ try { File csvFile = new File(fileName); //构造文件 //导入HuTool中CSV工具包的CsvWriter类 //设置导出字符类型, CHARSET_UTF_8 CsvWriter writer = CsvUtil.getWriter(csvFile, CharsetUtil.CHARSET_UTF_8); writer.write(result); //通过CsvWriter中的write方法写入数据 writer.close(); //关闭CsvWriter //保存文件 FileInputStream fileInputStream=new FileInputStream(csvFile); this.saveFile(fileInputStream,csvFile.getName()); } catch (Exception e){ System.out.println(e); } }
5-把文件导出到本地
/** * 文件保存 * @param inputStream * @param fileName */ private void saveFile(InputStream inputStream, String fileName) { OutputStream os = null; try { //保存文件路径 String path = "D:\\cscFile\\"; // 1K的数据缓冲 byte[] bs = new byte[1024]; // 读取到的数据长度 int len; // 输出的文件流保存到本地文件 File tempFile = new File(path); if (!tempFile.exists()) { tempFile.mkdirs(); } os = new FileOutputStream(tempFile.getPath() + File.separator + fileName); // 开始读取 while ((len = inputStream.read(bs)) != -1) { os.write(bs, 0, len); } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { // 完毕,关闭所有链接 try { os.close(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }
public void exportCsv(HttpServletResponse response) throws IOException {
List<User> result = new ArrayList<>();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + new String("测试.csv".getBytes("UTF-8"), "ISO8859-1"));
response.setContentType(ContentType.APPLICATION_OCTET_STREAM.toString());
CsvWriter csvWriter = CsvUtil.getWriter(response.getWriter());
csvWriter.writeBeans(result);
csvWriter.close();
}
CSV文件通常是指逗号分隔值(Comma-Separated Values)文件,是一种常见的电子表格文件格式。其基本格式是一行代表一条记录,每个字段之间用逗号进行分隔,通常使用纯文本文件存储。CSV文件的格式简单且易于阅读,且不需要使用特殊的软件即可打开,可以用txt软件打开也可以使用Excel软件打开,因此在实际应用中非常方便。
Hutool下的CsvWriter默认使用逗号作为数据分割符,使用‘/r/n’也即CRLF作为换行符,使用‘#’号作为注释的开始使用双引号作为文本包装符,默认使用UTF-8生成CSV文件;这里需要注意的是,如果分隔符使用竖线,当用Excel打开时是无法准确识别的,因为Excel默认的分隔符为逗号,同时CSV文件编码为UTF-8时,Excel打开后有可能会乱码,因为Excel的默认编码格式时跟随系统的。
使用CsvWriteConfig进行设置,CsvWriteConfig类继承自CsvConfig,扩展了一些方法,通过添加一个setCsvWriterConfig方法,当默认CSV默认配置不足以满足需要时可以进行设置,代码如下:
/** * 设置Csv写文件配置 * @return CsvWriteConfig */ public static CsvWriteConfig setCsvWriterConfig(){ CsvWriteConfig csvWriteConfig = new CsvWriteConfig(); //设置 文本分隔符,文本包装符,默认双引号'"' // csvWriteConfig.setTextDelimiter('\t'); // 字段分割符号,默认为逗号 csvWriteConfig.setFieldSeparator('|'); // 设置注释符号 // csvWriteConfig.setCommentCharacter('#'); // 设置是否始终使用文本分隔符,文本包装符,默认false // csvWriteConfig.setAlwaysDelimitText(true); // 换行符默认为CharUtil.CR, CharUtil.LF // csvWriteConfig.setLineDelimiter(lineDelimiter); // 设置标题行的别名,如果不设置则表头为id,name,gender Map<String, String> headerAlias = new LinkedHashMap<>(); headerAlias.put("id", "序号"); headerAlias.put("name", "姓名"); headerAlias.put("gender", "性别"); csvWriteConfig.setHeaderAlias(headerAlias); return csvWriteConfig; }
先添加一个User对象,将User对象写入到CSV,生成CSV文件的方式很多,可以使用不使用上面的配置使用默认配置写入CSV文件,也可以自定义配置同时使用英文状态下的竖线作为数据分割符生成CSV文件。
/** * 使用自定CSV配置生成CSV文件 * @param users 数据来源 * @param config CSV写文件配置 */ public static void generateCsvWithConfig(List<User> users, CsvWriteConfig config){ // 可以通过设置FileWriter的编码来控制输出文件的编码格式 // FileWriter fileWriter = new FileWriter("hutoolCsv.csv", StandardCharsets.UTF_8); try(FileWriter fileWriter = new FileWriter("hutoolCsv.csv"); CsvWriter csvWriter = CsvUtil.getWriter(fileWriter, config)){ csvWriter.writeBeans(users); csvWriter.flush(); } catch (IOException e) { e.printStackTrace(); } }
/**
* 使用默认配置生成CSV文件
* @param users 数据来源
*/
public static void generateCsvWithDefault(List<User> users){
// 构造,覆盖已有文件(如果存在),默认编码UTF-8
File hutoolCsv = new File("hutoolCsv1.csv");
CsvWriter writer = new CsvWriter(hutoolCsv);
// 设置编码
// CsvWriter writer = new CsvWriter(hutoolCsv, StandardCharsets.UTF_8);
writer.writeBeans(users);
writer.flush();
writer.close();
}
String path2 = "/Users/jeckwu/Desktop/test/test.csv";
// 导出csv文件到指定路径
@Test
public void exportDataToCsvFileOfPath(){
List<TILockdataMidModel> allLockData = tiLockdataMidMapper.getAllLockData();
// 业务处理
// 可以把list转map
//指定路径和编码
CsvWriter writer = CsvUtil.getWriter(path2, CharsetUtil.CHARSET_UTF_8);
//按行写出
writer.writeBeans(allLockData);
}
@RequestMapping("/exportCsvFileTest")
public void exportCsvFileTest(HttpServletResponse response) throws IOException {
// 获取业务数据 一般放到service处理
List<TILockdataMidModel> allLockData = tiLockdataMidMapper.getAllLockData();
// 业务处理完成把数据写到流中 响应到页面上
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + new String("exportCsvFileTest.csv".getBytes(StandardCharsets.UTF_8), "ISO8859-1"));
response.setContentType(ContentType.APPLICATION_OCTET_STREAM.toString());
CsvWriter csvWriter = CsvUtil.getWriter(response.getWriter());
csvWriter.writeBeans(allLockData);
csvWriter.close();
}
(1)CsvWriter构造器方法
// 1-使用CsvUtil.getWriter把数据写到本地文件
String tempFile = "/tmp/数据预览-" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + ".csv";
CsvWriter csvWriter = CsvUtil.getWriter(new File(tempFile), Charset.defaultCharset());
// 2-使用File创建构造器
File hutoolCsv = new File("hutoolCsv1.csv");
CsvWriter writer = new CsvWriter(hutoolCsv);
(2)CsvUtil工具类的方法
主要封装了两个方法:
getReader:用于对CSV文件读取,获取CsvReader对象,也可以用new CsvReader构造器创建CsvReader对象
getWriter 用于生成CSV文件,获取CsvWriter对象,也可以用new CsvWriter构造器创建CsvWriter对象
(2)CsvWriter的writeBeans方法
(3)CsvWriter的write方法
其中,write方法用于写入一行数据,而writeBeans方法用于写入多行数据。
write方法接受一个字符串数组作为参数,将数组中的每个元素作为一个字段写入一行数据中。例如,如果我们有一个字符串数组String[] data = {“John”, “Doe”, “25”};,调用csvWriter.write(data);将会写入一行数据"John,Doe,25"到.csv文件中。
writeBeans方法接受一个对象列表作为参数,将列表中的每个对象的属性值作为一个字段写入一行数据中。这些对象必须是JavaBean对象,即具有私有属性和公共getter方法的类。例如,如果我们有一个名为Person的JavaBean类,其中包含属性name、age和gender,我们可以创建一个Person对象列表并调用csvWriter.writeBeans(personList);将会将每个Person对象的属性值写入一行数据中。
总结起来,write方法用于写入一行数据,接受一个字符串数组作为参数;而writeBeans方法用于写入多行数据,接受一个对象列表作为参数,并将每个对象的属性值作为一个字段写入一行数据中。12
(4)CsvWriter的flush方法
flush()强制把缓冲区的数据写到文件,即使缓冲器不满,也就是所谓的刷新;
(5)CsvWriter的close方法
调用close(),关闭流,在关闭之前会把没写入文件中的缓冲区中的数据写入文件。
// 解析本地csv文件
@Test
public void analysisLocalCsvFileByPath(){
final CsvReader reader = CsvUtil.getReader();
//假设csv文件在classpath目录下
final List<TILockdataMidModel> result = reader.read(
ResourceUtil.getUtf8Reader(path2), TILockdataMidModel.class);
log.info("====>{}",result.size());
log.info("====>{}", JSON.toJSONString(result));
}
// 读取csv中的数据 public static List<Map<String,Object>> csvImport(MultipartFile file) throws IOException { //2. 进行配置 CsvReadConfig csvReadConfig=new CsvReadConfig(); // 是否跳过空白行 csvReadConfig.setSkipEmptyRows(true); // 是否设置首行为标题行 csvReadConfig.setContainsHeader(true); //构建 CsvReader 对象 CsvReader csvReader = CsvUtil.getReader(csvReadConfig); // 这里转了下 可能会产生临时文件,临时文件目录可以设置,也可以立马删除 CsvData read = csvReader.read(multipartFile2File(file), CharsetUtil.CHARSET_GBK); List<Map<String,Object>> mapList = new ArrayList<>(); List<String> header = read.getHeader(); List<CsvRow> rows = read.getRows(); for (CsvRow row : rows) { Map<String,Object> map = new HashMap<>(); for (int i = 0; i < row.size(); i++) { map.put(header.get(i),row.get(i)); } // 具体业务根据需求来就行了 if (!ObjectUtils.isEmpty(map.get("mlos单号"))) mapList.add(map); } return mapList; } /** * multipartFile转File **/ public static File multipartFile2File(MultipartFile multipartFile){ File file = null; if (multipartFile != null){ try { file=File.createTempFile("tmp", null); multipartFile.transferTo(file); System.gc(); file.deleteOnExit(); }catch (Exception e){ e.printStackTrace(); log.warn("multipartFile转File发生异常:"+e); } } return file; }
使用CsvReadConfig进行设置,CsvReadConfig类也继承自CsvConfig,扩展了一些方法,通过添加一个setCsvReadConfig方法,当默认CSV默认配置不足以满足需要时可以进行读取设置,CSV读取配置相比于写配置多了一些例如是否包含首行以及跳过空白行等设置,代码如下:
/** * 设置Csv读文件配置 * @return CsvWriteConfig */ public static CsvReadConfig setCsvReadConfig(){ CsvReadConfig csvReadConfig = new CsvReadConfig(); // 设置 文本分隔符,文本包装符,默认双引号'"' //csvReadConfig.setTextDelimiter('\t'); // 字段分割符号,默认为逗号 csvReadConfig.setFieldSeparator('|'); // 设置注释符号 // csvReadConfig.setCommentCharacter('#'); // CSV文件是否包含表头(因为表头不是数据内容) csvReadConfig.setContainsHeader(true); // 或者使用如下配置设置表头开始行号,-1L代表无表头 // csvReadConfig.setHeaderLineNo(1L); //设置开始的行(包括),默认0,此处为原始文件行号 // csvReadConfig.setBeginLineNo(0); // 是否跳过空白行,默认为true // csvReadConfig.setSkipEmptyRows(true); // 设置每行字段个数不同时是否抛出异常,默认false // csvReadConfig.setErrorOnDifferentFieldCount(false); // 将表头的别称对转换为对应的字段 Map<String, String> headerAlias = new LinkedHashMap<>(); headerAlias.put("序号", "id"); headerAlias.put("姓名", "name"); headerAlias.put("性别", "gender"); csvReadConfig.setHeaderAlias(headerAlias); return csvReadConfig; }
使用自定义配置读取CSV文件,代码如下:
/** * 使用自定义配置读取Csv文件返回User对象 * @param config Csv读取配置 * @return List<User> */ public static List<User> readCsvWithConfig(CsvReadConfig config){ List<User> users = new ArrayList<>(); try(FileReader fileReader = new FileReader("hutoolCsv.csv"); CsvReader csvReader = CsvUtil.getReader(fileReader, config)){ // csvReader 读一遍流就没有了,没有办法读第二遍 // csvReader.forEach(System.out::println); // 遍历 CSV 数据行,将每一行数据转换为 User 对象 csvReader.read().getRows().forEach(csvRow -> { System.out.println(csvRow.toString()); Integer id = Integer.valueOf(csvRow.get(0)); String name = csvRow.get(1); String gender = csvRow.get(2); User user = new User(id, name, gender); users.add(user); }); users.forEach(System.out::println); }catch (IOException e){ e.printStackTrace(); } return users; }
使用默认配置读取CSV文件,代码如下:
/** * 使用默认配置读取CSV文件 * @return List<User> */ public static List<User> readCsvWithDefault(){ List<User> users = new ArrayList<>(); File hutoolCsv = new File("hutoolCsv1.csv"); CsvReader reader = new CsvReader(hutoolCsv, new CsvReadConfig()); reader.setContainsHeader(true); reader.stream().forEach(csvRow -> { System.out.println(csvRow.toString()); Integer id = Integer.valueOf(csvRow.get(0)); String name = csvRow.get(1); String gender = csvRow.get(2); User user = new User(id, name, gender); users.add(user); }); users.forEach(System.out::println); return users; }
CsvReader也包含了一个setContainsHeader来跳过行首,不然在进行将数据转换为User对象时会出现行首无法转换出现异常。
单元测试代码如下:
@Test
void testHutoolReadCsv(){
// 使用自定义配置读取CSV文件
List<User> users = HutooCsvUtil.readCsvWithConfig(HutooCsvUtil.setCsvReadConfig());
assertEquals(6, users.size());
// 使用默认配置读取CSV文件
List<User> users1 = HutooCsvUtil.readCsvWithDefault();
assertEquals(6, users1.size());
}
csv和excel的编码不一致,需要在导出csv的时候设置编码GBK,否则用Office Excel打开会出现乱码
public void exportCsvData(HttpServletResponse response){ ExcelWriter excelWriter=null; response.setContentType("text/csv;charset=utf-8"); response.setCharacterEncoding("utf-8"); String fileName="export.csv"; // 设置文件头:最后一个参数是设置下载文件名 response.setHeader("Content-Disposition","attachment;fileName="+new String(fileName.getBytes(StandardCharsets.UTF_8),StandardCharsets.ISO_8859_1)); // 设置文件ContentType类型,这样设置,会自动判断下载文件类型 try{ excelWriter=EasyExcel.write(response.getOutputStream(),WidthAndHeightData.class).excelType(ExcelTypeEnum.CSV).charset(Charset.forName("GBK")).registerWriteHandler(EasyExcelUtils.horizontalCellStyleStrategy()).build(); }catch(IOException e){ e.printStackTrace(); }finally{ // 千万别忘记finish 会帮忙关闭流 if(excelWriter!=null){ excelWriter.finish(); } } }
/** * @Author * @Date Created in 2023/11/2 10:59 * @DESCRIPTION: 读取csv格式的文件数据 * @Version V1.0 */ @RestController @RequestMapping("/api/csv") public class CsvController { /** * 读取传入的csv 文本的内容可以存入数据库 * * @param file * @return */ @PostMapping("/upload") public ResponseEntity<?> uploadCsv(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return ResponseEntity.badRequest().body("文件不能为空"); } //判断csv文件类型是不是csv文件 String contentType = file.getContentType(); String originalFilename = file.getOriginalFilename(); boolean isCsv = ("text/csv".equals(contentType)) || (originalFilename != null && originalFilename.endsWith(".csv")); if (!isCsv) { return ResponseEntity.badRequest().body("文件必须是CSV格式"); } //判断csv文件格式内容是否有误? boolean csvFormatValid = checkCscUtils.isCsvFormatValid(file); if (csvFormatValid) { List<User> userList = new CopyOnWriteArrayList<>(); try { EasyExcel.read(file.getInputStream(), User.class, new PageReadListener<User>(userList::addAll)) .excelType(ExcelTypeEnum.CSV) .sheet() .doRead(); } catch (IOException e) { e.printStackTrace(); return ResponseEntity.status(500).body("文件读取出错"); } // 处理userList... return ResponseEntity.ok(userList); } return ResponseEntity.status(500).body("文件格式出错"); } /** * 使用 easyExcel 导出一个csv 格式,但是版本可能与poi 版本冲突 * * @param response * @return * @throws IOException */ @GetMapping("/exportCsv") public ResponseEntity<?> exportCsv(HttpServletResponse response) throws IOException { // 设置响应头 response.setContentType("application/csv"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("export_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".csv"); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); List<Student> userList = getStudents(); // 使用EasyExcel导出CSV文件response.getOutputStream() try { EasyExcel.write(response.getOutputStream(), Student.class) .excelType(ExcelTypeEnum.CSV) .sheet("我的学生") .doWrite(userList); } catch (IOException e) { throw new RuntimeException(e); } return ResponseEntity.status(200).body("文件导出成功"); } private static List<Student> getStudents() { // 创建数据列表 List<Student> userList = new CopyOnWriteArrayList<>(); // 添加数据(示例) userList.add(new Student("1", "John Doe", "25")); userList.add(new Student("2", "Jane Smith", "30")); userList.add(new Student("3", "Mike Johnson", "35")); return userList; } }
// excludeColumnFiledNames 依据列忽略
// excludeColumnIndexes 指定删除列index
// 还支持 includeColumnFiledNames 定义导出列
EasyExcel.write(filePath, DataDemo.class).excludeColumnFiledNames(Arrays.asList("field3")).sheet("用户").doWrite(dataList);
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。