当前位置: 首页 > news >正文

封装PoiExcelUtils

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

为什么要封装PoiExcelUtils

前面两篇其实基本已经完成了需求,只不过存在两个问题:

  • Controller层代码太臃肿了
  • 复用性差

特别是复用性,几乎为0,如果TeacherController也有导出需求,同样的一坨代码还要再写一遍,所以我们有必要抽取通用代码,尝试封装一个PoiExcelUtils。

通常来说,封装工具类的前提是“出现重复代码了”。为了能观察到重复代码,我们可以比对导入Teacher和导入Student的代码。

假设学生表是这样的:

而老师表是这样的:

相比学生表,有两点变化:

  • 数据不再是顶着左上角,而是各自空了一格
  • 字段变了,字段数量也不同

如果把两份导入代码都写出来,大概是这样的:

有4处地方问题要解决:

  • rowIndex、cellIndex:数据从哪行哪列开始读,每份Excel可能不同
  • 如何抽取通用代码,自动为各种POJO设置值(难点一)
  • 抽取Excel文件输入流
  • 解决单元格和POJO字段的映射顺序(难点二)

封装导入代码

第一步:抽取rowIndex、cellIndex

前两个很简单,直接抽取到形参即可,大家自己做。大概类似这样:

public void importExcel(Integer rowIndex, Integer cellIndex) throws Exception;

第二步:反射设置字段值

也就是如何抽取通用代码,自动为各种POJO设置值。

但凡通用代码,必然是对重复代码/重复操作的抽取。如果我们要抽取一段通用的代码一个POJO的不同字段设置值,那么必然是通过一个循环语句(for循环或其他)。

这个结论怎么推导出来的?

因为Teacher可能5个字段,Student6个字段,Worker7个字段。如果要用通用代码为它们各自的字段赋值,那么必然是一个for循环,不然无法解决字段个数不同的问题。也就是5个字段的POJO循环5次,6个字段的POJO循环6次,以此类推。

但for循环本身对被迭代的对象有个隐性要求:每一次操作的对象都是相同/相似的。因为for循环的定义就是:重复N次相同操作。

也就是说,我们如果要写一个for循环对所有POJO的所有字段进行遍历,比如这样:

for (字段 : POJO){字段.set(getCellValue());
}

这要求所有POJO类型相同的,而且POJO内部的所有字段类型也是相同的。

于是我们得到了一个悖论:有差异的POJO们希望抽取通用代码对自己进行无差别迭代,而通用代码却要求迭代对象是无差异的。

我们分析一下为什么会出现这种悖论:

  • 首先,横向比较来看这几个POJO类型是不同的,Teacher、Student、Worker
  • 其次,纵向比较来看,同一个POJO的字段类型也是不同的,Integer age、String name

不同POJO、甚至同一个POJO的不同字段都无法用一个for搞定,似乎只能逐个手写:

Teacher字段1.set(getCellValue());
Teacher字段2.set(getCellValue());
Teacher字段3.set(getCellValue());Student字段1.set(getCellValue());
Student字段2.set(getCellValue());
Student字段3.set(getCellValue());Worker字段1.set(getCellValue());
Worker字段2.set(getCellValue());
Worker字段3.set(getCellValue());

那有没有可能找到POJO与POJO之间、字段与字段之间的共性内容,从而在更高的抽象层面上谋求统一呢?比如,我是中国人,他是日本人,我们不同国籍,不是同一个民族。但是如果站在更高的角度,我们都是地球人。

这给了我们启发,Teacher、Student、Worker向上抽象可以得到Class对象,而Integer age、String name向上抽象可以得到Field对象。

分析到这里,反射就呼之欲出了。

public void method(Class pojoClass) {Object pojo = pojoClass.newInstance();Field[] fields = pojoClass.getDeclaredFields();for (field : fields) {field.set(pojo, getCellValue())}   
}

初步动手实践一下:

public class MyUtils {public static void main(String[] args) throws Exception {MyUtils myUtil = new MyUtils();// 假设我们要读取student_info.xlsx,由于标题和表头各占一行,所以RowStartIndex=2,而数据紧贴左边,所以cellStartIndex=0myUtil.importExcel(2, 0, Student.class);}/*** 第二步,抽取Class、Field** @param RowStartIndex  从哪行开始读取(从0开始)* @param cellStartIndex 从那列开始读取(从0开始)* @param pojoClass      要操作的类型* @throws Exception*/public void importExcel(Integer RowStartIndex, Integer cellStartIndex, Class pojoClass) throws Exception {// 获取工作薄XSSFWorkbook workbook = new XSSFWorkbook("/Users/bravo1988/Desktop/student_info.xlsx");// 获取工作表。一个工作薄中可能有多个工作表,比如sheet1 sheet2,可以根据下标,也可以根据sheet名称。这里根据下标即可。XSSFSheet sheet = workbook.getSheetAt(0);// 得到Pojo所有字段Field[] fields = pojoClass.getDeclaredFields();List<Object> excelDataList = new ArrayList<>();// 收集每一行数据,设置到Model中(跳过表头)for (int i = RowStartIndex; i <= sheet.getLastRowNum(); i++) {XSSFRow row = sheet.getRow(i);// 把单元格数据转为当前字段的类型,并设置Object pojo = pojoClass.newInstance();// 遍历单元格,为pojo字段赋值for (int j = cellStartIndex; j < row.getLastCellNum(); j++) {// 获取单元格的值XSSFCell cell = row.getCell(j);/*** 这里假设单元格顺序和Pojo顺序一致,所以在这个for循环中,row.getCell(j)单元格对应fields[j - cellIndex]字段* fields是从1开始的(id不设置),所以要j - cellIndex,这样单元格和Pojo字段才是匹配的。*/Field field = fields[j - cellStartIndex + 1];field.setAccessible(true);field.set(pojo, convertAttrType(field, cell));}excelDataList.add(pojo);}excelDataList.forEach(System.out::println);}/*** 类型转换 将 cell单元格数据类型 转为 Java类型* <p>* 这里其实分两步:* 1.通过getValue()方法得到cell对应的Java类型的字符串类型,比如Date,getValue返回的不是Date类型,而是Date的格式化字符串* 2.判断Pojo当前字段是什么类型,把getValue()得到的字符串往该类型转** @param field* @param cell* @return* @throws Exception*/private Object convertAttrType(Field field, Cell cell) throws Exception {Class<?> fieldType = field.getType();if (String.class.isAssignableFrom(fieldType)) {return getValue(cell);} else if (Date.class.isAssignableFrom(fieldType)) {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell));} else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {return Integer.parseInt(getValue(cell));} else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {return Double.parseDouble(getValue(cell));} else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {return Boolean.valueOf(getValue(cell));} else if (BigDecimal.class.isAssignableFrom(fieldType)) {return new BigDecimal(getValue(cell));} else {return null;}}/*** 提供POI数据类型 --> Java数据类型的转换* 由于本方法返回值设为String,所以不管转换后是什么Java类型,都要以String格式返回* 所以Date会被格式化为yyyy-MM-dd HH:mm:ss* 后面根据需要自己另外转换** @param cell* @return*/private String getValue(Cell cell) {if (cell == null) {return "";}switch (cell.getCellType()) {case STRING:return cell.getRichStringCellValue().getString().trim();case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {// DateUtil是POI内部提供的日期工具类,可以把原本是日期类型的NUMERIC转为Java的Data类型Date javaDate = DateUtil.getJavaDate(cell.getNumericCellValue());return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(javaDate);} else {// 无论Excel中是58还是58.0,数值类型在POI中最终都被解读为double。这里的解决办法是通过BigDecimal先把Double先转成字符串,如果是.0结尾,把.0去掉String strCell = "";Double num = cell.getNumericCellValue();BigDecimal bd = new BigDecimal(num.toString());if (bd != null) {strCell = bd.toPlainString();}// 去除 浮点型 自动加的 .0if (strCell.endsWith(".0")) {strCell = strCell.substring(0, strCell.indexOf("."));}return strCell;}case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());default:return "";}}@Data@NoArgsConstructor@AllArgsConstructorstatic class Student {private Long id;private String name;private Integer age;private String address;private Date birthday;private Double height;private Boolean isMainlandChina;}
}

输出结果:

第三步:抽取Excel文件流

我们发现,从Student换成Teacher后,我们需要进到MyUtil代码中里把Excel文件路径改成teacher_info.xlsx,这不符合开闭原则。我们已经知道WorkBook可以接收InputStream,所以第三步就是抽取Excel文件流:

public class MyUtils {public static void main(String[] args) throws Exception {MyUtils myUtil = new MyUtils();FileInputStream fileInputStream = new FileInputStream(new File("/Users/bravo1988/Desktop/student_info.xlsx"));// 通过努力,我们再次把“变化的因素”提取出去了,现在MyUtils内部的代码越来越稳定了,稳定意味着通用...myUtil.importExcel(fileInputStream, 2, 0, Student.class);}/*** 第三步,抽取Excel文件流** @param inputStream    要导入的Excel文件流* @param rowStartIndex  从哪行开始读取(从0开始)* @param cellStartIndex 从那列开始读取(从0开始)* @param pojoClass      操作的类型* @throws Exception*/public void importExcel(InputStream inputStream, Integer rowStartIndex, Integer cellStartIndex, Class<?> pojoClass) throws Exception {// 【本次改变】通过输入流构造工作簿,由外界传入!!!XSSFWorkbook workbook = new XSSFWorkbook(inputStream);// 获取工作表。一个工作薄中可能有多个工作表,比如sheet1 sheet2,可以根据下标,也可以根据sheet名称。这里根据下标即可。XSSFSheet sheet = workbook.getSheetAt(0);// 省略...}// 省略其他方法...@Data@NoArgsConstructor@AllArgsConstructorstatic class Student {private Long id;private String name;private Integer age;private String address;private Date birthday;private Double height;private Boolean isMainlandChina;}
}

第四步:引入注解标注顺序

抽取到这一步,其实已经差不多了,至于POJO字段和单元格的映射顺序问题,个人觉得其实无所谓,就默认单元格顺序和字段顺序一致也未尝不可。但客户的Excel表是五花八门的,Student表的单元格可能是按顺序来的,而Teacher表的单元格则可能是反着来的。那么我们反射的代码就不再通用,如果不更改setter的顺序,整个赋值就颠倒了。

以面向对象的思维考虑一下,单元格的哪个值应该设置给哪个字段谁最清楚呢?

答案是,POJO自己。

比如Student的name字段,它要“记得”Excel中哪个单元格的值是自己的。你可以把Excel表中单元格的数据们想象成老婆组,POJO的字段们是老公组。到时候遍历单元格数据时,比如到了朱丽叶时,就喊一句:朱丽叶的老公是谁?然后POJO就把罗密欧送过去即可。此时我们需要一个标记,把POJO字段和单元格绑定起来。这很像JPA/通用Mapper的@Column注解,所以我们也模仿一下。

自定义注解ExcelAttribute:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAttribute {/*** 对应的列名称*/String value() default "";/*** 列序号*/int sort();// 字段类型对应的格式(大家自己可以试着扩展)String format() default "";
}
public class MyUtils {public static void main(String[] args) throws Exception {MyUtils myUtil = new MyUtils();FileInputStream fileInputStream = new FileInputStream(new File("/Users/bravo1988/Desktop/student_info.xlsx"));// 传入Excel文件流,从文件中读取数据并返回ListList<Student> students = myUtil.importExcel(fileInputStream, 2, 0, Student.class);students.forEach(System.out::println);}/*** 第四步,引入注解绑定字段与单元格的映射关系** @param inputStream    要导入的Excel文件流* @param rowStartIndex  从哪行开始读取(从0开始)* @param cellStartIndex 从那列开始读取(从0开始)* @param pojoClass      操作的Class* @param <T>            操作的类型* @return* @throws Exception*/public <T> List<T> importExcel(InputStream inputStream, Integer rowStartIndex, Integer cellStartIndex, Class<T> pojoClass) throws Exception {// 获取工作薄XSSFWorkbook workbook = new XSSFWorkbook(inputStream);// 获取工作表。一个工作薄中可能有多个工作表,比如sheet1 sheet2,可以根据下标,也可以根据sheet名称。这里根据下标即可。XSSFSheet sheet = workbook.getSheetAt(0);// 得到Pojo所有字段Field[] fields = pojoClass.getDeclaredFields();List<T> excelDataList = new ArrayList<>();// 收集每一行数据,设置到Model中(跳过表头)for (int i = rowStartIndex; i <= sheet.getLastRowNum(); i++) {XSSFRow row = sheet.getRow(i);// 把单元格数据转为当前字段的类型,并设置T pojo = pojoClass.newInstance();// 遍历单元格(老婆组),为pojo字段赋值for (int j = cellStartIndex; j < row.getLastCellNum(); j++) {// 获取单元格的值XSSFCell cell = row.getCell(j);// 开始从老公组找出罗密欧for (Field field : fields) {// 遍历,找到与当前单元格匹配的字段,取出单元格的值,把值设置给该字段if (field.isAnnotationPresent(ExcelAttribute.class) && field.getAnnotation(ExcelAttribute.class).sort() == j) {field.setAccessible(true);// 把朱丽叶配给罗密欧field.set(pojo, convertAttrType(field, cell));}}}excelDataList.add(pojo);}return excelDataList;}/*** 类型转换 将 cell单元格数据类型 转为 Java类型* <p>* 这里其实分两步:* 1.通过getValue()方法得到cell对应的Java类型的字符串类型,比如Date,getValue返回的不是Date类型,而是Date的格式化字符串* 2.判断Pojo当前字段是什么类型,把getValue()得到的字符串往该类型转** @param field* @param cell* @return* @throws Exception*/private Object convertAttrType(Field field, Cell cell) throws Exception {Class<?> fieldType = field.getType();if (String.class.isAssignableFrom(fieldType)) {return getValue(cell);} else if (Date.class.isAssignableFrom(fieldType)) {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell));} else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {return Integer.parseInt(getValue(cell));} else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {return Double.parseDouble(getValue(cell));} else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {return Boolean.valueOf(getValue(cell));} else if (BigDecimal.class.isAssignableFrom(fieldType)) {return new BigDecimal(getValue(cell));} else {return null;}}/*** 提供POI数据类型 --> Java数据类型的转换* 由于本方法返回值设为String,所以不管转换后是什么Java类型,都要以String格式返回* 所以Date会被格式化为yyyy-MM-dd HH:mm:ss* 后面根据需要自己另外转换** @param cell* @return*/private String getValue(Cell cell) {if (cell == null) {return "";}switch (cell.getCellType()) {case STRING:return cell.getRichStringCellValue().getString().trim();case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {// DateUtil是POI内部提供的日期工具类,可以把原本是日期类型的NUMERIC转为Java的Data类型Date javaDate = DateUtil.getJavaDate(cell.getNumericCellValue());String dateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(javaDate);return dateString;} else {// 无论Excel中是58还是58.0,数值类型在POI中最终都被解读为Double。这里的解决办法是通过BigDecimal先把Double先转成字符串,如果是.0结尾,把.0去掉String strCell = "";Double num = cell.getNumericCellValue();BigDecimal bd = new BigDecimal(num.toString());if (bd != null) {strCell = bd.toPlainString();}// 去除 浮点型 自动加的 .0if (strCell.endsWith(".0")) {strCell = strCell.substring(0, strCell.indexOf("."));}return strCell;}case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());default:return "";}}@Data@NoArgsConstructor@AllArgsConstructorstatic class Student {private Long id;@ExcelAttribute(sort = 0, value = "姓名")private String name;@ExcelAttribute(sort = 1, value = "年龄")private Integer age;@ExcelAttribute(sort = 2, value = "住址")private String address;@ExcelAttribute(sort = 3, value = "生日")private Date birthday;@ExcelAttribute(sort = 4, value = "身高")private Double height;@ExcelAttribute(sort = 5, value = "是否来自大陆")private Boolean isMainlandChina;}
}

上面的代码好在哪呢?虽然Excel字段顺序变动时仍然要改变注解的顺序,但代码却不用改了,同一套setter可以复用到所有的Excel上。

封装导出代码

有了封装导入代码的经验,封装导出代码就很简单了,这里直接贴代码:

public class MyUtils {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** @param dataList            要导出的数据* @param pojoClass           要操作的类型* @param templateInputStream Excel模板输入流* @param response            response响应,用来导出Excel文件* @param excelName           指定导出文件名* @param rowIndex            模板数据起始行* @param cellIndex           模板数据起始列* @throws IOException* @throws IllegalAccessException*/public void exportExcel(List<T> dataList, Class<T> pojoClass, InputStream templateInputStream, HttpServletResponse response, String excelName, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {// 读取模板XSSFWorkbook workbook = new XSSFWorkbook(templateInputStream);// 获取模板sheet,默认第一张sheetXSSFSheet sheet = workbook.getSheetAt(0);// 从指定行收集单元格样式,方便复用,类似格式刷CellStyle[] templateStyles = getTemplateStyles(rowIndex, cellIndex, sheet);// 得到所有字段Field[] fields = pojoClass.getDeclaredFields();// 创建单元格,并设置样式和数据for (int i = 0; i < dataList.size(); i++) {Object pojo = dataList.get(i);XSSFRow row = sheet.createRow(i + rowIndex);// 为当前行创建单元格(创建老婆组)for (int k = cellIndex; k < templateStyles.length + cellIndex; k++) {// 当前新建了朱丽叶,已经就位XSSFCell cell = row.createCell(k);// 找到朱丽叶的化妆盒,给朱丽叶化妆cell.setCellStyle(templateStyles[k - cellIndex]);// 遍历字段(老公组),找到罗密欧for (Field field : fields) {if (field.isAnnotationPresent(ExcelAttribute.class) && field.getAnnotation(ExcelAttribute.class).sort() == k - cellIndex) {field.setAccessible(true);// 把罗密欧给朱丽叶mappingValue(field, cell, pojo);}}}}// 通过response响应String fileName = new String(excelName.getBytes("UTF-8"), "ISO-8859-1");response.setContentType("application/octet-stream");response.setHeader("content-disposition", "attachment;filename=" + fileName);response.setHeader("filename", fileName);workbook.write(response.getOutputStream());workbook.close();logger.info("导出成功!");}/*** 根据字段类型强制转为字段数据,并设置给cell** @param field* @param cell* @param pojo* @throws IllegalAccessException*/private void mappingValue(Field field, Cell cell, Object pojo) throws IllegalAccessException {Class<?> fieldType = field.getType();if (Date.class.isAssignableFrom(fieldType)) {cell.setCellValue((Date) field.get(pojo));} else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {cell.setCellValue((Integer) field.get(pojo));} else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {cell.setCellValue((Double) field.get(pojo));} else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {cell.setCellValue((Boolean) field.get(pojo));} else if (BigDecimal.class.isAssignableFrom(fieldType)) {cell.setCellValue(((BigDecimal) field.get(pojo)).doubleValue());} else {cell.setCellValue((String) field.get(pojo));}}/*** 收集Excel模板的样式,方便对新建单元格复用,相当于打造一把格式刷** @param rowIndex* @param cellIndex* @param sheet* @return*/private CellStyle[] getTemplateStyles(Integer rowIndex, Integer cellIndex, XSSFSheet sheet) {XSSFRow dataTemplateRow = sheet.getRow(rowIndex);CellStyle[] cellStyles = new CellStyle[dataTemplateRow.getLastCellNum() - cellIndex];for (int i = 0; i < cellStyles.length; i++) {cellStyles[i] = dataTemplateRow.getCell(i + cellIndex).getCellStyle();}return cellStyles;}}

有兴趣的话,可以放在上一篇的Controller中测试一下。

代码优化

上面对POI的封装只能说基本满足要求,但还有很大的优化空间:

  • 方法参数太多
  • 无论导入还是导出,都出现了for循环三层嵌套,效率很低

把公共参数提取到构造器中

来看一下导入导出两个方法中有哪些公共的参数:

public class MyUtils {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** @param dataList            要导出的数据* @param pojoClass           要操作的类型* @param templateInputStream Excel模板输入流* @param response            response响应,用来导出Excel文件* @param excelName           指定导出文件名* @param rowIndex            模板数据起始行* @param cellIndex           模板数据起始列* @throws IOException* @throws IllegalAccessException*/public void exportExcel(List<T> dataList, Class<T> pojoClass, InputStream templateInputStream, HttpServletResponse response, String excelName, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {}/*** @param inputStream    要导入的Excel文件流* @param rowStartIndex  从哪行开始读取(从0开始)* @param cellStartIndex 从那列开始读取(从0开始)* @param pojoClass      操作的Class* @param <T>            操作的类型* @return* @throws Exception*/public <T> List<T> importExcel(InputStream inputStream, Integer rowStartIndex, Integer cellStartIndex, Class<T> pojoClass) throws Exception {}

两个方法看起来相同的参数挺多的,但是含义有些不同。比如都是rowIndex,exportExcel()中指的是模板的样式位置,而importExcel()中指的是要读取的数据位置,所以从语义上不适合等同处理,所以最终我打算只抽取Class,也就是外部在new工具类时要指定Class。

用Map索引替代for遍历

Map代替for循环遍历算是一种很简单高效的方式,我们在《实用小算法》中已经讨论过,有兴趣的同学可以去看看,详细比较了几种小算法的优劣。

PoiExcelUtils完整代码

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAttribute {/*** 对应的列名称*/String value() default "";/*** 列序号*/int sort();// 字段类型对应的格式(大家自己可以试着扩展)String format() default "";
}
public class PoiExcelUtils<T> {/*** 与本次导出相关的POJO类型*/private Class<T> clazz;/*** POJO字段的Map形式,key是字段上ExcelAttribute注解的sort值*/private Map<Integer, Field> fieldMap = new HashMap<>();/*** 构造器,明确POJO的Class类型** @param clazz*/public PoiExcelUtils(Class<T> clazz) {this.clazz = clazz;// 得到所有字段,并用Map为字段建立索引Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(ExcelAttribute.class)) {fieldMap.put(field.getAnnotation(ExcelAttribute.class).sort(), field);}}}/*** 按模板导出到网络* 输入Excel模板+数据,导出Excel表格** @param dataList  要导出的数据* @param excelName 指定本次导出的Excel表名* @param is        Excel模板,InputStream流* @param rowIndex  从模板哪一行开始拷贝样式* @param cellIndex 从模板哪一列开始拷贝样式* @param response  response响应* @throws IOException* @throws IllegalAccessException*/public void exportExcelWithTemplate(List<T> dataList, String excelName, InputStream is, Integer rowIndex, Integer cellIndex, HttpServletResponse response) throws IOException, IllegalAccessException {// 把数据封装到ExcelXSSFWorkbook workbook = mapData2ExcelWithTemplate(dataList, is, rowIndex, cellIndex);//===============response响应导出Excel=================String fileName = new String(excelName.getBytes("UTF-8"), "ISO-8859-1");response.setContentType("application/octet-stream");response.setHeader("content-disposition", "attachment;filename=" + fileName);response.setHeader("filename", fileName);workbook.write(response.getOutputStream());workbook.close();}/*** 按模板导出到本地* 输入Excel模板+数据+Excel保存路径,导出Excel表格** @param dataList  要导出的数据* @param is        Excel模板,InputStream流* @param pathName  本地输出路径(比如/users/document/student_info.xlsx)* @param rowIndex  从模板哪一行开始拷贝样式* @param cellIndex 从模板哪一列开始拷贝样式* @throws IOException* @throws IllegalAccessException*/public void exportExcelToLocalWithTemplate(List<T> dataList, InputStream is, String pathName, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {// 把数据封装到ExcelXSSFWorkbook workbook = mapData2ExcelWithTemplate(dataList, is, rowIndex, cellIndex);//===============导出Excel到本地=================FileOutputStream fileOutputStream = new FileOutputStream(pathName);workbook.write(fileOutputStream);}/*** Excel导出到网络** @param dataList  要导出的数据* @param excelName 指定本次导出的Excel表名* @param sheetName sheet名称* @param response  response响应* @throws IllegalAccessException* @throws IOException*/public void exportExcel(List<T> dataList, String excelName, String sheetName, HttpServletResponse response) throws IllegalAccessException, IOException {XSSFWorkbook workbook = mapData2Excel(dataList, sheetName);//===============response响应导出Excel=================String fileName = new String(excelName.getBytes("UTF-8"), "ISO-8859-1");response.setContentType("application/octet-stream");response.setHeader("content-disposition", "attachment;filename=" + fileName);response.setHeader("filename", fileName);workbook.write(response.getOutputStream());workbook.close();}/*** Excel导出到本地** @param dataList  要导出的数据* @param pathName  本地输出路径(比如/users/document/student_info.xlsx)* @param sheetName sheet名称* @throws IllegalAccessException* @throws IOException*/public void exportExcelToLocal(List<T> dataList, String pathName, String sheetName) throws IllegalAccessException, IOException {XSSFWorkbook workbook = mapData2Excel(dataList, sheetName);//===============导出Excel到本地=================FileOutputStream fileOutputStream = new FileOutputStream(pathName);workbook.write(fileOutputStream);}/*** Excel导入** @param inputStream 要导入的Excel表* @param rowIndex    从哪一行开始读取* @param cellIndex   从哪一列开始读取* @return* @throws Exception*/public List<T> importExcel(InputStream inputStream, Integer rowIndex, Integer cellIndex) throws Exception {//================准备要导入的Excel=================XSSFWorkbook workbook = new XSSFWorkbook(inputStream);XSSFSheet sheet = workbook.getSheetAt(0);List<T> dataList = new ArrayList<>();//================从Excel读取数据,并设置给pojoList================for (int i = rowIndex; i <= sheet.getLastRowNum(); i++) {XSSFRow row = sheet.getRow(i);// 遍历单元格(老婆组)T pojo = (T) clazz.newInstance();for (int j = cellIndex; j < row.getLastCellNum(); j++) {// 获取单元格的值XSSFCell cell = row.getCell(j);// 当前是朱丽叶,已经就位了,罗密欧在哪?Object value = getValue(cell);// 根据索引快速从老公组找出罗密欧Field field = fieldMap.get(j - cellIndex);// 把朱丽叶配给罗密欧(把单元格数据赋值给字段)field.setAccessible(true);field.set(pojo, convertAttrType(field, cell));}dataList.add(pojo);}return dataList;}//======================== private =======================/*** 把查询得到的数据封装到Excel** @param dataList* @return* @throws IllegalAccessException*/private XSSFWorkbook mapData2Excel(List<T> dataList, String sheetName) throws IllegalAccessException {XSSFWorkbook workbook = new XSSFWorkbook();XSSFSheet sheet = workbook.createSheet(sheetName);//=============创建表头===============XSSFRow row = sheet.createRow(0);Field[] fields = clazz.getDeclaredFields();for (int i = 0; i < fields.length; i++) {Field field = fields[i];if (field.isAnnotationPresent(ExcelAttribute.class)) {row.createCell(i).setCellValue(field.getAnnotation(ExcelAttribute.class).value());}}//=========创建单元格,并设置数据(跳过表头)=======int size = fieldMap.keySet().size();for (int i = 0, length = dataList.size(); i < length; i++) {T pojo = dataList.get(i);row = sheet.createRow(i + 1);for (int k = 0; k < size; k++) {XSSFCell cell = row.createCell(k);Field field = fieldMap.get(k);field.setAccessible(true);mappingValue(field, cell, pojo);}}return workbook;}/*** 把查询得到的数据【按指定的模板样式】封装到Excel** @param dataList* @param is* @param rowIndex* @param cellIndex* @return* @throws IOException* @throws IllegalAccessException*/private XSSFWorkbook mapData2ExcelWithTemplate(List<T> dataList, InputStream is, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {//================得到模板=================XSSFWorkbook workbook = new XSSFWorkbook(is);XSSFSheet sheet = workbook.getSheetAt(0);//=============从模板抽取样式===============CellStyle[] templateStyles = getTemplateStyles(rowIndex, cellIndex, sheet);//=========创建单元格,并设置样式和数据=======for (int i = 0; i < dataList.size(); i++) {T pojo = dataList.get(i);XSSFRow row = sheet.createRow(i + rowIndex);// 循环创建单元格(创建老婆组)for (int k = cellIndex; k < templateStyles.length + cellIndex; k++) {// 新建单元格:朱丽叶XSSFCell cell = row.createCell(k);// 为单元格设置样式:找到朱丽叶的化妆盒,给朱丽叶化妆cell.setCellStyle(templateStyles[k - cellIndex]);// 根据索引快速从老公组找出罗密欧Field field = fieldMap.get(k - cellIndex);// 把罗密欧给朱丽叶(把字段的数据赋值给单元格)field.setAccessible(true);mappingValue(field, cell, pojo);}}return workbook;}/*** 把字段Field的值设置给单元格Cell* Filed.get()得到的数据类型是Object,而cell.setCellValue()要求是具体的数据类* 因此需要判断字段类型,并把数据转换为原来的真是类型** @param field* @param cell* @param pojo* @throws IllegalAccessException*/private void mappingValue(Field field, Cell cell, Object pojo) throws IllegalAccessException {Class<?> fieldType = field.getType();if (Date.class.isAssignableFrom(fieldType)) {cell.setCellValue((Date) field.get(pojo));} else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {cell.setCellValue((Integer) field.get(pojo));} else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {cell.setCellValue((Double) field.get(pojo));} else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {cell.setCellValue((Boolean) field.get(pojo));} else if (BigDecimal.class.isAssignableFrom(fieldType)) {cell.setCellValue(((BigDecimal) field.get(pojo)).doubleValue());} else {cell.setCellValue((String) field.get(pojo));}}/*** 收集Excel模板的样式,方便对新建单元格复用,相当于打造一把格式刷** @param rowIndex* @param cellIndex* @param sheet* @return*/private CellStyle[] getTemplateStyles(Integer rowIndex, Integer cellIndex, XSSFSheet sheet) {XSSFRow dataTemplateRow = sheet.getRow(rowIndex);CellStyle[] cellStyles = new CellStyle[dataTemplateRow.getLastCellNum() - cellIndex];for (int i = 0; i < cellStyles.length; i++) {cellStyles[i] = dataTemplateRow.getCell(i + cellIndex).getCellStyle();}return cellStyles;}/*** 类型转换 将 cell单元格数据类型 转为 Java类型* <p>* 这里其实分两步:* 1.通过getValue()方法得到cell对应的Java类型的字符串类型,比如Date,getValue返回的不是Date类型,而是Date的格式化字符串* 2.判断Pojo当前字段是什么类型,把getValue()得到的字符串往该类型转** @param field* @param cell* @return* @throws Exception*/private Object convertAttrType(Field field, Cell cell) throws Exception {Class<?> fieldType = field.getType();if (String.class.isAssignableFrom(fieldType)) {return getValue(cell);} else if (Date.class.isAssignableFrom(fieldType)) {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell));} else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {return Integer.parseInt(getValue(cell));} else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {return Double.parseDouble(getValue(cell));} else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {return Boolean.valueOf(getValue(cell));} else if (BigDecimal.class.isAssignableFrom(fieldType)) {return new BigDecimal(getValue(cell));} else {return null;}}/*** 提供POI数据类型 --> Java数据类型的转换* 由于本方法返回值设为String,所以不管转换后是什么Java类型,都要以String格式返回* 所以Date会被格式化为yyyy-MM-dd HH:mm:ss* 后面根据需要自己另外转换,详见{@link PoiExcelUtils#convertAttrType(Field, Cell)}** @param cell* @return*/private String getValue(Cell cell) {if (cell == null) {return "";}switch (cell.getCellType()) {case STRING:return cell.getRichStringCellValue().getString().trim();case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {// DateUtil是POI内部提供的日期工具类,可以把原本是日期类型的NUMERIC转为Java的Data类型Date javaDate = DateUtil.getJavaDate(cell.getNumericCellValue());String dateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(javaDate);return dateString;} else {// 无论Excel中是58还是58.0,数值类型在POI中最终都被解读为Double。这里的解决办法是通过BigDecimal先把Double先转成字符串,如果是.0结尾,把.0去掉String strCell = "";Double num = cell.getNumericCellValue();BigDecimal bd = new BigDecimal(num.toString());if (bd != null) {strCell = bd.toPlainString();}// 去除 浮点型 自动加的 .0if (strCell.endsWith(".0")) {strCell = strCell.substring(0, strCell.indexOf("."));}return strCell;}case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());default:return "";}}}

测试

@RestController
public class ExcelController {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/exportExcel")public void exportExcel(HttpServletResponse response, HttpServletRequest request) throws Exception {// 模拟从数据库查询数据List<Student> studentList = new ArrayList<>();studentList.add(new Student(1L, "周深(web导出)", 28, "贵州", new SimpleDateFormat("yyyy-MM-dd").parse("1992-9-29"), 161.0, true));studentList.add(new Student(2L, "李健(web导出)", 46, "哈尔滨", new SimpleDateFormat("yyyy-MM-dd").parse("1974-9-23"), 174.5, true));studentList.add(new Student(3L, "周星驰(web导出)", 58, "香港", new SimpleDateFormat("yyyy-MM-dd").parse("1962-6-22"), 174.0, false));// 导出数据PoiExcelUtils<Student> poiExcelUtils = new PoiExcelUtils<>(Student.class);FileInputStream excelTemplateInputStream = new FileInputStream(new File("/Users/bravo1988/Desktop/student_info.xlsx"));poiExcelUtils.exportExcelWithTemplate(studentList, "学生信息表.xlsx", excelTemplateInputStream, 2, 0, response);logger.info("导出成功!");}@PostMapping("/importExcel")public Map<String, Object> importExcel(MultipartFile file) throws Exception {PoiExcelUtils<Student> poiExcelUtils = new PoiExcelUtils<>(Student.class);List<Student> studentList = poiExcelUtils.importExcel(file.getInputStream(), 2, 0);saveToDB(studentList);logger.info("导入{}成功!", file.getOriginalFilename());// 这里用Map偷懒了,实际项目中可以封装Result实体类返回Map<String, Object> result = new HashMap<>();result.put("code", 200);result.put("data", studentList);result.put("msg", "success");return result;}private void saveToDB(List<Student> studentList) {if (CollectionUtils.isEmpty(studentList)) {return;}// 直接打印,模拟插入数据库studentList.forEach(System.out::println);}@Data@NoArgsConstructor@AllArgsConstructorpublic static class Student {private Long id;@ExcelAttribute(sort = 0, value = "姓名")private String name;@ExcelAttribute(sort = 1, value = "年龄")private Integer age;@ExcelAttribute(sort = 2, value = "住址")private String address;@ExcelAttribute(sort = 3, value = "生日")private Date birthday;@ExcelAttribute(sort = 4, value = "身高")private Double height;@ExcelAttribute(sort = 5, value = "是否来自大陆")private Boolean isMainlandChina;}}

总结

上面的工具类封装具有一定难度,尽量理解即可。

网络上还有一种封装方式,需要调用者自己在外部组装好各个字段对应的Map传入Util。但个人认为POI本身效率就不高,所以这点性能提升可有可无,用起来还麻烦。

至此,对POI的学习告一段落,请不要在生产环境使用PoiExcelUtils(可能发生OOM),推荐使用EasyExcel。

 

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

相关文章:

  • GPT-Crawler一键爬虫构建GPTs知识库
  • 吉他初学者学习网站搭建系列(5)——如何做一个在线节拍器
  • Android:BackStackRecord
  • error转string
  • uniapp使用vue-i18n国际化多国语言
  • 记录 | CUDA编程中使用#ifdef指令控制生成CPU和GPU代码
  • [足式机器人]Part2 Dr. CAN学习笔记-数学基础Ch0-3线性化Linearization
  • CoreDNS实战(九)-性能压测
  • IDEA连接Redis注意事项
  • Arduino项目式编程教学第三章——红外遥控灯
  • CSS单位vmin、vmax
  • 【JMeter】BeanShell了解基础知识
  • C/C++---------------LeetCode第118. 杨辉三角
  • RT-Thread Studio文件消失不见或被排除构建
  • 【唐山海德教育】职称评审需要什么条件呢?
  • ----------
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • 【跃迁之路】【669天】程序员高效学习方法论探索系列(实验阶段426-2018.12.13)...
  • AHK 中 = 和 == 等比较运算符的用法
  • docker容器内的网络抓包
  • Elasticsearch 参考指南(升级前重新索引)
  • es6(二):字符串的扩展
  • GraphQL学习过程应该是这样的
  • Javascript弹出层-初探
  • Java超时控制的实现
  • Python socket服务器端、客户端传送信息
  • 从tcpdump抓包看TCP/IP协议
  • 类orAPI - 收藏集 - 掘金
  • 如何解决微信端直接跳WAP端
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 完善智慧办公建设,小熊U租获京东数千万元A+轮融资 ...
  • 新海诚画集[秒速5センチメートル:樱花抄·春]
  • ​批处理文件中的errorlevel用法
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #ubuntu# #git# repository git config --global --add safe.directory
  • #设计模式#4.6 Flyweight(享元) 对象结构型模式
  • (42)STM32——LCD显示屏实验笔记
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (k8s中)docker netty OOM问题记录
  • (教学思路 C#之类三)方法参数类型(ref、out、parmas)
  • (数据结构)顺序表的定义
  • (一)认识微服务
  • (转)VC++中ondraw在什么时候调用的
  • (转)清华学霸演讲稿:永远不要说你已经尽力了
  • (转载)Google Chrome调试JS
  • .dwp和.webpart的区别
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .vue文件怎么使用_vue调试工具vue-devtools的安装
  • @javax.ws.rs Webservice注解
  • @RequestMapping用法详解
  • @德人合科技——天锐绿盾 | 图纸加密软件有哪些功能呢?
  • [.net]官方水晶报表的使用以演示下载
  • [.NET]桃源网络硬盘 v7.4
  • [AutoSar]BSW_OS 01 priority ceiling protocol(PCP)
  • [bzoj 3124][sdoi 2013 省选] 直径