使用HuTool读取,写入,删除Excel,自定义Excel中的Sheet名称和列名
1 思路分析
1.1 读取文件
ExcelReader reader = ExcelUtil.getReader(file)//file为读取的目标文件
List<T> list = reader.readAll(clazz);//clazz的类型为class<T>
1.2 写入文件
//destFile为写入的目标文件,不存在则新建文件
//sheetName为写入的sheetName,不存在则新建sheet
ExcelWriter writer = ExcelWriter.getWriter(File destFile, String sheetName);
//list为写入的数据
writer.write(list);
1.3 删除文件
//file为删除的文件
FileUtil.del(file)
1.4 自定义别名
1.4.1 编写@ExcelAlias注解,值为该字段队形的excel中的列名
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelAlias {
String value() default "";
}
1.4.2 每个需要自定义列名的字段都需要加对应的@ExcelAlias注解,如:
@ExcelAlias("学号")
public String stuNo;
1.4.3 在读取或者写入之前,获取字段上@ExcelAlias的值,并调用setHeaderAlias(headerAlias)方法批量设置字段的别名
//headerAlias为Map集合
//读取时为reader批量设置列名 每个键值对格式为:(“别名”, “字段名”), 从excel中读取到“已有的(别名)”, 改为“想改的(字段名)”
reader.setHeaderAlias(headerAlias);
//写入时为writer批量设置列名 每个键值对格式为:(“字段名”, “别名”), 每个字段对应的“已有的(字段名)”, 改为“想改的(别名)”
writer.setHeaderAlias(headerAlias);
1.4.4 批量获取headerAlias的方法getHeaderAlias(Class clazz, boolean isRead)
/**
* 获得一个类所有 有别名的 字段 和 别名
*
* @param clazz 类
* @param isRead 每个键值对格式为 ("已有的","想改的")
* true 读取时 每个键值对格式为:(“别名”, “字段名”), 从excel中读取到“已有的(别名)”, 改为“想改的(字段名)”
* false 写入时 每个键值对格式为:(“字段名”, “别名”), 每个字段对应的“已有的(字段名)”, 改为“想改的(别名)”
* @param <T>
* @return
*/
private static <T> Map<String, String> getHeaderAlias(Class<T> clazz, boolean isRead) {
Map headerAlias = new LinkedHashMap<String, String>();
// Hutool 获取字段集合的方法,无需try/catch
List<Field> fields = Arrays.asList(ReflectUtil.getFields(clazz));
if (fields.isEmpty()) {
return headerAlias;
}
for (Field field : fields) {
// 获得每个字段的 @ExcelAlias注解
ExcelAlias anno = field.getAnnotation(ExcelAlias.class);
if (anno == null || "".equals(anno.value())) {
continue;
}
if (isRead) {
// 读取时 每个键值对格式为:(“别名”, “字段名”)
headerAlias.put(anno.value(), field.getName());
} else {
// 写入时 每个键值对格式为:(“字段名”, “别名”)
headerAlias.put(field.getName(), anno.value());
}
}
return headerAlias;
}
1.5 写入时自定义写入的Sheet名称
1.5.1 自定义@SheetName注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SheetName {
String value() default SheetNameConstant.DEFAULT_NOTE_SHEET_NAME;
}
1.5.2 在类名上添加@SheetName注解,值为Sheet的名称
@Data
@SheetName("自定义Sheet名称")
public class Student {
1.5.3 获取@SheetName注解注解上的值,并构建对应的writer
//获得SheetName注解
SheetName anno = clazz.getAnnotation(SheetName.class);
//如果不需要自定义sheet名称,默认为sheet1
String sheetName =
(anno == null ? "sheet1" : anno.value());
//构建writer,path为写入的路径,sheetName为自定义的sheet名称
ExcelWriter writer = ExcelWriter.getWriter(new File(path), sheetName);
1.6 根据根目录和文件名前缀,获得根目录下所有符合条件的文件路径
/**
* 查找路径pathPre下,名字包含字段namePre的所有文件路径
*
* @param pathPre
* @param namePre
* @return
*/
public static List<String> findAllNameByNamePre(String pathPre, String namePre) {
List<String> list = new ArrayList<>();
File folder = new File(pathPre);
String[] names = folder.list();
if (Objects.isNull(names) || names.length == 0) {
return list;
}
for (String name : names) {
if (name.startsWith(namePre)) {
list.add(name);
}
}
return list;
}
1.7 获得根目录下更新时间最晚的文件
/**
* 获得修改时间最晚的
* 该路径下包含该前缀的文件名称
*
* @param pathPre
* @return
*/
private String findNewestName(String pathPre, String namePre) {
String name = "";
File folder = new File(pathPre);
long maxLastModified = 0l;
File[] files = folder.listFiles();
if (files == null) {
return name;
}
for (File file : files) {
if (file.getName().startsWith(namePre)) {
maxLastModified = Math.max(file.lastModified(), maxLastModified);
name = file.getName();
}
}
return name;
}
1.8 改进其自带writer写入时按列名的Hash排序顺序插入的问题
1.8.1 继承ExcelWriter
public class MyExcelWriter extends ExcelWriter {
1.8.2 重写write方法
public ExcelWriter write(Iterable<?> data) {
int index = 0;
for (Object object : data) {
if (object instanceof Iterable) {
// 普通多行数据
writeRow((Iterable<?>) object);
} else if (object instanceof Map) {
// Map表示一行,第一条数据的key做为标题行
writeRows((Map<?, ?>) object, 0 == index);
} else if (BeanUtil.isBean(object.getClass())) {
// 一个Bean对象表示一行,ExcelWriter中使用的 BeanUtil.beanToMap(object) ,返回结果使用HashMap接收,导致写入excel时列的顺序按Hash值排序
//this.writeRows(BeanUtil.beanToMap(object), 0 == index);
// 使用自定义的beanToMap(),按照读取顺序排序,实现按字段顺序写入excel中的列
writeRows(beanToMap(object), 0 == index);
} else {
break;
}
index++;
}
if (0 == index) {
// 在无法识别元素类型的情况下,做为一行对待
writeRow(data);
}
return this;
}
1.8.3 自定义beanToMap(Object obj),使用LinkedHashMap接收,按插入顺序排序
public Map<String, Object> beanToMap(Object obj) {
Map map = new LinkedHashMap<String, String>();
Class clazz = obj.getClass();
//设置sheet名称
SheetName anno = (SheetName) clazz.getAnnotation(SheetName.class);
if (anno != null) {
super.setOrCreateSheet(anno.value());
}
List<Field> fields = Arrays.asList(ReflectUtil.getFields(clazz));
for (Field field : fields) {
map.put(field.getName(), ReflectUtil.getFieldValue(obj, field));
}
return map;
}
2 代码实现
2.1 注解包Anno
2.1.1 @SheetName注解类
package com.hsh.excelutil.Anno;
import com.hsh.excelutil.constant.SheetNameConstant;
import java.lang.annotation.*;
/**
* @Author hsh
* @DateTime 2023/09/05 17:15
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SheetName {
String value() default SheetNameConstant.DEFAULT_NOTE_SHEET_NAME;
}
2.1.2 @ExcelAlias注解类
package com.hsh.excelutil.Anno;
import java.lang.annotation.*;
/**
* @Author hsh
* @DateTime 2023/09/05 17:15
**/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelAlias {
String value() default "";
}
2.2 常量包constant–SheetNameConstant常量类
package com.hsh.excelutil.constant;
/**
* @Author hsh
* @DateTime 2023/11/24 15:39
**/
public class SheetNameConstant {
public final static String DEFAULT_NOTE_SHEET_NAME = "sheet1";
}
2.3 实体类包poji–Student实体类
package com.hsh.excelutil.poji;
import com.hsh.excelutil.Anno.ExcelAlias;
import com.hsh.excelutil.Anno.SheetName;
import lombok.Data;
@Data
@SheetName("自定义Sheet名称")
public class Student {
@ExcelAlias("学号")
public String stuNo;
@ExcelAlias("姓名")
public String name;
@ExcelAlias("性别")
public String sex;
@ExcelAlias("年龄")
public String age;
@ExcelAlias("班级")
public String className;
}
2.4 工具包util
2.4.1 HuToolExcelUtil工具类,继承HuTool的ExcelUtil,可以使用HuTool的ExcelUtil的自带方法
package com.hsh.excelutil.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import com.hsh.excelutil.Anno.ExcelAlias;
import com.hsh.excelutil.Anno.SheetName;
import com.hsh.excelutil.constant.SheetNameConstant;
import java.io.File;
import java.lang.reflect.Field;
import java.util.*;
/**
* @Author hsh
* @DateTime 2023/09/09 14:56
**/
public class HuToolExcelUtil extends ExcelUtil {
/**
* 读取Excel并转化为list
* 需要指定返回时元素的类
*
* @param path 需要读取的Excel 的路径
* @param clazz 指定的类,
* @return list
*/
public static <T> List<T> redaExcel(String path, Class<T> clazz) {
File file = new File(path);
List<T> list = new ArrayList<>();
try (ExcelReader reader = ExcelUtil.getReader(file)) {
reader.setHeaderAlias(getHeaderAlias(clazz, true));
list = reader.readAll(clazz);
}
System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n读取文件:" + path + "\n内容");
list.stream().forEach(v-> System.out.println());
return list;
}
/**
* 读取Excel并转化为list
* 需要指定返回时元素的类
*
* @param names 需要读取的Excel 的集合
* @param clazz 指定的类,
* @return list
*/
public static <T> List<T> redaExcels(String path, List<String> names, Class<T> clazz) {
List<T> list = new ArrayList<>();
for (String name : names) {
list.addAll(redaExcel(path + name, clazz));
}
return list;
}
/**
* 将list写入Excel中
*
* @param path 需要写入的Excel 的路径
* @param list 写入的内容
* @return
*/
public static <T> void writeExcel(String path, List<T> list, Class<T> clazz) throws Exception {
if (!path.endsWith(".xlsx") && !path.endsWith(".xls")) {
path = path + ".xlsx";
}
Map headerAlias = getHeaderAlias(clazz, false);
SheetName anno = clazz.getAnnotation(SheetName.class);
try (MyExcelWriter writer = MyExcelWriter.getWriter(new File(path), anno == null ? SheetNameConstant.DEFAULT_NOTE_SHEET_NAME : anno.value())) {
writer.setHeaderAlias(headerAlias);
// 写入并刷盘
writer.write(list);
System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n写入文件:" + path + "\n内容");
list.stream().forEach(v-> System.out.println());
}
}
/**
* 删除文件
*
* @param path
* @return
*/
public static int deleteExcel(String path) {
File file = new File(path);
if (FileUtil.del(file)) {
System.out.println("↓↓↓↓↓↓↓↓↓↓\n删除文件:" + path);
return 1;
}
return 0;
}
/**
* 删除文件
*
* @param path
* @return
*/
public static int deleteExcels(String path, List<String> names) {
int flag = 0;
for (String name : names) {
flag += deleteExcel(path + name);
}
return flag;
}
/**
* 获得一个类所有 有别名的 字段 和 别名
*
* @param clazz 类
* @param isRead 每个键值对格式为 ("已有的","想改的")
* true 读取时 每个键值对格式为:(“别名”, “字段名”), 从excel中读取到“已有的(别名)”, 改为“想改的(字段名)”
* false 写入时 每个键值对格式为:(“字段名”, “别名”), 每个字段对应的“已有的(字段名)”, 改为“想改的(别名)”
* @param <T>
* @return
*/
private static <T> Map<String, String> getHeaderAlias(Class<T> clazz, boolean isRead) {
Map headerAlias = new LinkedHashMap<String, String>();
// Hutool 获取字段集合的方法,无需try/catch
List<Field> fields = Arrays.asList(ReflectUtil.getFields(clazz));
if (fields.isEmpty()) {
return headerAlias;
}
for (Field field : fields) {
// 获得每个字段的 @ExcelAlias注解
ExcelAlias anno = field.getAnnotation(ExcelAlias.class);
if (anno == null || "".equals(anno.value())) {
continue;
}
if (isRead) {
// 读取时 每个键值对格式为:(“别名”, “字段名”)
headerAlias.put(anno.value(), field.getName());
} else {
// 写入时 每个键值对格式为:(“字段名”, “别名”)
headerAlias.put(field.getName(), anno.value());
}
}
return headerAlias;
}
/**
* 查找路径pathPre下,名字包含字段namePre的所有文件路径
*
* @param pathPre
* @param namePre
* @return
*/
public static List<String> findAllNameByNamePre(String pathPre, String namePre) {
List<String> list = new ArrayList<>();
File folder = new File(pathPre);
String[] names = folder.list();
if (Objects.isNull(names) || names.length == 0) {
return list;
}
for (String name : names) {
if (name.startsWith(namePre)) {
list.add(name);
}
}
return list;
}
}
2.4.2 自定义MyExcelWriter,继承HuTool的ExcelWriter,改进其自带writer写入时按列名的Hash排序顺序插入的问题
package com.hsh.excelutil.util;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.poi.PoiChecker;
import cn.hutool.poi.excel.ExcelWriter;
import com.hsh.excelutil.Anno.SheetName;
import lombok.NoArgsConstructor;
import java.io.File;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @Author hsh
* @DateTime 2023/11/23 13:53
**/
@NoArgsConstructor
public class MyExcelWriter extends ExcelWriter {
/**
* 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件<br>
* 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动增加<br>
* 样式为默认样式,可使用{@link #getCellStyle()}方法调用后自定义默认样式<br>
* data中元素支持的类型有:
*
* <p>
* 1. Iterable,既元素为一个集合,元素被当作一行,data表示多行<br>
* 2. Map,既元素为一个Map,第一个Map的keys作为首行,剩下的行为Map的values,data表示多行 <br>
* 3. Bean,既元素为一个Bean,第一个Bean的字段名列表会作为首行,剩下的行为Bean的字段值列表,data表示多行 <br>
* 4. 无法识别,不输出
* </p>
*
* @param data 数据
* @return this
*/
public ExcelWriter write(Iterable<?> data) {
int index = 0;
for (Object object : data) {
if (object instanceof Iterable) {
// 普通多行数据
writeRow((Iterable<?>) object);
} else if (object instanceof Map) {
// Map表示一行,第一条数据的key做为标题行
writeRows((Map<?, ?>) object, 0 == index);
} else if (BeanUtil.isBean(object.getClass())) {
// 一个Bean对象表示一行,ExcelWriter中使用的 BeanUtil.beanToMap(object) ,返回结果使用HashMap接收,导致写入excel时列的顺序按Hash值排序
//this.writeRows(BeanUtil.beanToMap(object), 0 == index);
// 使用自定义的beanToMap(),按照读取顺序排序,实现按字段顺序写入excel中的列
writeRows(beanToMap(object), 0 == index);
} else {
break;
}
index++;
}
if (0 == index) {
// 在无法识别元素类型的情况下,做为一行对待
writeRow(data);
}
return this;
}
/**
* 自定义beanToMap(),实现按字段顺序写入excel中的列
* @param obj
* @return 使用LinkedHashMap,按照读取顺序排序
*/
public Map<String, Object> beanToMap(Object obj) {
Map map = new LinkedHashMap<String, String>();
Class clazz = obj.getClass();
//设置sheet名称
SheetName anno = (SheetName) clazz.getAnnotation(SheetName.class);
if (anno != null) {
super.setOrCreateSheet(anno.value());
}
List<Field> fields = Arrays.asList(ReflectUtil.getFields(clazz));
for (Field field : fields) {
map.put(field.getName(), ReflectUtil.getFieldValue(obj, field));
}
return map;
}
/**
* 获得{@link ExcelWriter},默认写出到第一个sheet<br>
* 不传入写出的Excel文件路径,只能调用{@link ExcelWriter#flush(OutputStream)}方法写出到流<br>
* 若写出到文件,还需调用{@link ExcelWriter#setDestFile(File)}方法自定义写出的文件,然后调用{@link ExcelWriter#flush()}方法写出到文件
*
* @return {@link ExcelWriter}
* @since 3.2.1
*/
public static MyExcelWriter getWriter() {
try {
return new MyExcelWriter();
} catch (NoClassDefFoundError e) {
throw PoiChecker.transError(e);
}
}
/**
* 获得{@link ExcelWriter}
*
* @param destFile 目标文件
* @param sheetName sheet表名
* @return {@link ExcelWriter}
*/
public static MyExcelWriter getWriter(File destFile, String sheetName) {
try {
return new MyExcelWriter(destFile, sheetName);
} catch (NoClassDefFoundError e) {
throw PoiChecker.transError(e);
}
}
public MyExcelWriter(File destFile, String sheetName) {
super(destFile, sheetName);
}
}
3 效果测试
3.1 准备文件
3.2编写测试类
package com.hsh.excelutil.util;
import com.hsh.excelutil.poji.Student;
import java.util.List;
public class HuToolExcelUtilTest {
List<Student> students;
List<String> names;
@org.junit.Test
public void redaExcel() {
students = HuToolExcelUtil.redaExcel("d:\\Users\\USER\\Desktop\\java\\javaTest\\学生资料1班.xlsx", Student.class);
}
@org.junit.Test
public void redaExcels() {
names = HuToolExcelUtil.findAllNameByNamePre("d:\\Users\\USER\\Desktop\\java\\javaTest\\", "学生资料");
students = HuToolExcelUtil.redaExcels("d:\\Users\\USER\\Desktop\\java\\javaTest\\", names, Student.class);
}
@org.junit.Test
public void writeExcel() {
redaExcels();
try {
HuToolExcelUtil.writeExcel("d:\\Users\\USER\\Desktop\\java\\javaTest\\学生资料.xlsx", students, Student.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@org.junit.Test
public void deleteExcel() {
HuToolExcelUtil.deleteExcel("d:\\Users\\USER\\Desktop\\java\\javaTest\\学生资料.xlsx");
}
@org.junit.Test
public void deleteExcels() {
names = HuToolExcelUtil.findAllNameByNamePre("d:\\Users\\USER\\Desktop\\java\\javaTest\\", "学生资料");
HuToolExcelUtil.deleteExcels("d:\\Users\\USER\\Desktop\\java\\javaTest\\", names);
}
}
3.3 读取单个文件HuToolExcelUtilTest.redaExcel():学生资料1班
3.4 批量读取文件HuToolExcelUtilTest.redaExcels():前缀为“学生资料”的
3.5 写入文件HuToolExcelUtilTest.writeExcel()