一、简介
HSSFWorkbook 、 XSSFWorkbook 、SXSSFWorkbook三者的区别
- HSSFWorkbook:是操作Excel 2003以前(包括2003)的版本,扩展名是.xls,数据限制是65535,超过则内存溢出
- XSSFWorkbook:是操作Excel 2007后的版本,扩展名是.xlsx,数据限制是1048576行,16384列,超过则内存溢出
- SXSSFWorkbook:是操作Excel 2007后的版本,扩展名是.xlsx,从POI 3.8版本开始可以持久化到磁盘,减少内存溢出问题
我们写个工具不仅能直接把简单Java对象数据直接导出到Excel,还能把数据读取成我们需要的简单的Java对象,并且可以根据你的需要选择不同的 Workbook,不用纠结HSSFWorkbook 、 XSSFWorkbook 、SXSSFWorkbook分别怎么用,本文中 org.apache.poi 使用的版本是 5.0.0。
二、maven依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alian</groupId>
<artifactId>excel</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>excel</name>
<description>Java实现Excel文件读写</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.14</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
最重要的依赖就是下面两个
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.0.0</version>
</dependency>
三、工具类ExcelUtil
ExcelUtil.java
package com.alian.excel.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Slf4j
public class ExcelUtil {
/**
* 从Excel读取数据返回对象集合
*
* @param filePath 文件路径
* @param t java对象
* @param filterRowNumbers 过滤行数(比如数据的头部一行就不是数据)
* @param <T> 泛型
* @return List<T>
*/
public static <T> List<T> readDataFromExcel(String filePath, Class<T> t, int filterRowNumbers) {
FileInputStream fis = null;
ArrayList<T> list = new ArrayList<>();
try {
fis = new FileInputStream(filePath);
Workbook sheets = WorkbookFactory.create(fis);
//获取sheet第一页(根据自己需要)
Sheet sheet = sheets.getSheetAt(0);
//获取表格的行数
int totalRowNumber = sheet.getPhysicalNumberOfRows();
//获取对象的字段列表
Field[] fields = t.getDeclaredFields();
for (int i = filterRowNumbers; i < totalRowNumber; i++) {
//获取一行数据
Row row = sheet.getRow(i);
//实例对象放到循环内(这个bug是网友【starwenran】提到的,网址【https://blog.csdn.net/weixin_42345741】)
T obj = t.newInstance();
// //变量一行数据的每个单元格,row.getPhysicalNumberOfCells()是单元格的数量
for (int j = 0, jLen = row.getPhysicalNumberOfCells(); j < jLen; j++) {
Cell cell = row.getCell(j);
//获取字段
Field dataField = fields[j];
String startWord = dataField.getName().substring(0, 1);
//需要注意的是如果变量是is开头的Boolean类型,它的set方法不能用下面的,因为它的set方法是去掉is的方法
//比如isEnable,set方法是setEnable
String methodName = "set" + dataField.getName().replaceFirst(startWord, startWord.toUpperCase());
//获取字段的set方法,dataField.getType()是参数的类型
Method method = t.getMethod(methodName, dataField.getType());
//反射调用set方法,getValueFromCell是把表格的值转成对应的类型
method.invoke(obj, getValueFromCell(dataField, cell));
}
list.add(obj);
}
return list;
} catch (Exception e) {
log.error("从Excel读取数据异常", e);
return Collections.emptyList();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 把数据导入到Excel(支持HSSFWorkbook、XSSFWorkbook、SXSSFWorkbook)
*
* @param workbook 工作薄
* @param sheetName sheet名称
* @param headerArray 头部标签数据
* @param list 数据list
* @param filePath 数据导出路径
* @param <T> 泛型
*/
public static <T> void exportDataToExcel(Workbook workbook, String sheetName, String[] headerArray, List<T> list, String filePath) {
//生成一个表格,并命名
Sheet sheet = workbook.createSheet(sheetName);
//设置表格默认列宽15个字节
sheet.setDefaultColumnWidth(15);
//生成一个头部样式
CellStyle headerStyle = getCellStyle(workbook, true);
//生成表格标题
Row headerRow = sheet.createRow(0);
headerRow.setHeight((short) 300);
for (int i = 0, len = headerArray.length; i < len; i++) {
//创建头部行的一个小单元格
Cell headerRowCell = headerRow.createCell(i);
//设置头部单元格的样式
headerRowCell.setCellStyle(headerStyle);
//设置头部单元格的值
headerRowCell.setCellValue(headerArray[i]);
}
//获取数据域样式
CellStyle bodyStyle = getCellStyle(workbook, false);
FileOutputStream os = null;
try {
//将数据放入sheet中
for (int i = 0, iLen = list.size(); i < iLen; i++) {
//创建一行,因为头部已经占用一行故需要加1
Row dataRow = sheet.createRow(i + 1);
T t = list.get(i);
//利用反射,根据JavaBean属性的先后顺序,动态调用get方法得到属性的值
Field[] fields = t.getClass().getDeclaredFields();
try {
for (int j = 0, jLen = fields.length; j < jLen; j++) {
//获取单元格第值
Cell dataRowCell = dataRow.createCell(j);
//获取字段
Field dataField = fields[j];
String startWord = dataField.getName().substring(0, 1);
//需要注意的是如果变量是is开头的Boolean类型,它的get方法不能用下面的,因为它的get方法是去掉is的方法
//比如isEnable,get方法是getEnable
String methodName = "get" + dataField.getName().replaceFirst(startWord, startWord.toUpperCase());
//获取对象的get方法
Method getMethod = t.getClass().getMethod(methodName);
//反射调用get方法
Object value = getMethod.invoke(t);
//单元格值为String
dataRowCell.setCellValue(null == value ? "" : value.toString());
dataRowCell.setCellStyle(bodyStyle);
}
} catch (Exception e) {
log.error("第【{}】行数据生成异常(下标0开始)", i, e);
}
}
os = new FileOutputStream(filePath);
workbook.write(os);
os.flush();
} catch (Exception e) {
log.error("生成数据异常", e);
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
log.error("关闭文件异常", e);
}
}
}
/**
* 获取单元格样式
*
* @param workbook 工作薄
* @param isHeader 是否是头部标签
* @return CellStyle
*/
public static CellStyle getCellStyle(Workbook workbook, boolean isHeader) {
CellStyle style = workbook.createCellStyle();
//设置边框
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
//设置边框颜色
style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
style.setRightBorderColor(IndexedColors.BLACK.getIndex());
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
//水平对齐方式
style.setAlignment(HorizontalAlignment.CENTER);
//垂直对齐方式
style.setVerticalAlignment(VerticalAlignment.CENTER);
//设置字体样式
Font font = workbook.createFont();
font.setColor(IndexedColors.BLACK.getIndex());
font.setFontHeightInPoints((short) 12);
if (isHeader) {
//设置背景色
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
font.setFontHeightInPoints((short) 14);
font.setBold(true);
}
//把字体应用到当前样式
style.setFont(font);
return style;
}
/**
* 把数据转成对象字段相应的类型(待完善:其他类型的判断,默认值及为空判断)
*
* @param dataField
* @param cell
* @return
*/
public static Object getValueFromCell(Field dataField, Cell cell) {
String fieldTypeStr = dataField.getType().toString();
if (fieldTypeStr.contains("String")) {
return cell.getStringCellValue();
} else if (fieldTypeStr.contains("Integer") || fieldTypeStr.contains("int")) {
return Integer.parseInt(cell.getStringCellValue());
} else if (fieldTypeStr.contains("Boolean") || fieldTypeStr.contains("boolean")) {
return Boolean.getBoolean(cell.getStringCellValue());
} else if (fieldTypeStr.contains("Double") || fieldTypeStr.contains("double")) {
return Double.parseDouble(cell.getStringCellValue());
} else if (fieldTypeStr.contains("float")) {
return Float.parseFloat(cell.getStringCellValue());
} else if (fieldTypeStr.contains("Long") || fieldTypeStr.contains("long")) {
return Long.parseLong(cell.getStringCellValue());
} else if (fieldTypeStr.contains("char")) {
return cell.getStringCellValue().charAt(0);
} else if (fieldTypeStr.contains("LocalTime")) {
return LocalTime.parse(cell.getStringCellValue());
} else if (fieldTypeStr.contains("LocalDate")) {
return LocalDate.parse(cell.getStringCellValue());
} else if (fieldTypeStr.contains("LocalDateTime")) {
return LocalDateTime.parse(cell.getStringCellValue());
} else if (fieldTypeStr.contains("Date")) {
try {
return new SimpleDateFormat("yyyy-MM-dd").parse(cell.getStringCellValue());
} catch (ParseException e) {
log.error("时间转化异常", e);
}
}
return null;
}
}
工具类中使用到了反射的知识,来完成get和set方法的操作。最近太忙了,都没有更新博客了,这个是修订后的版本,多谢网友【starwenran】的留言提醒,因为存数据的时候,随意造了重复数据,没检查读出的结果,造成读出的数据是最后一行数据,【starwenran】的博客地址是:https://blog.csdn.net/weixin_42345741,当然这个工具类还有他的不足,比如对is开头的Boolean对象的处理(本文注释中有说明),以后有时间我再优化下。
四、测试
4.1、EmployeeVo
EmployeeVo.java
package com.alian.excel.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
@Data
public class EmployeeVo {
/**
* 员工编号
*/
private String id;
/**
* 员工姓名
*/
private String name;
/**
* 员工年龄
*/
private int age;
/**
* 工资
*/
private double salary;
/**
* 部门
*/
private String department;
/**
* 入职时间
*/
private LocalDate hireDate;
/**
* 无参构造函数不能少
*/
public EmployeeVo(){
}
public EmployeeVo(String id, String name, int age, double salary, String department, LocalDate hireDate) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
this.department = department;
this.hireDate = hireDate;
}
}
4.2、写入数据到Excel
我们写个测试类
@Test
public void write() {
List<EmployeeVo> dataList = new ArrayList<>();
//仅仅是模拟数据
for (int i = 10000; i < 10030; i++) {
EmployeeVo employeeVo = new EmployeeVo("BAT"+i,"梁南生",18,20000.0,"研发部",LocalDate.of(2020,2,13));
dataList.add(employeeVo);
}
String sheetName = "员工信息";
String[] headerArray = new String[]{"员工编号", "员工姓名", "员工年龄", "工资", "部门", "入职时间"};
String filePath = "C:\\myFile\\CSDN\\Excel\\Excel文件写入测试.xls";
ExcelUtil.exportDataToExcel(new HSSFWorkbook(), sheetName, headerArray, dataList, filePath);
//ExcelUtil.exportDataToExcel(new XSSFWorkbook(), sheetName, headerArray, dataList, filePath);
//ExcelUtil.exportDataToExcel(new SXSSFWorkbook(),sheetName, headerArray, dataList, filePath);
}
运行结果:
如果你用HSSFWorkbook 写入的数据大于65535则会报错
17:02:37.541 [main] ERROR com.alian.excel.utils.ExcelUtil - 生成数据异常
java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
4.3、从Excel读取数据
@Test
public void read() {
String filePath = "C:\\myFile\\CSDN\\Excel\\Excel文件写入测试.xls";
List<EmployeeVo> employeeVos = ExcelUtil.readDataFromExcel(filePath, EmployeeVo.class, 1);
log.info("读取的数据行数:{}",employeeVos.size());
for (EmployeeVo employeeVo:employeeVos){
log.info("{}",employeeVo);
}
}
运行结果:
19:54:37.313 [main] INFO com.alian.excel.service.TestExcelService - 读取的数据行数:30
19:54:37.316 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10000, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10001, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10002, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10003, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10004, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10005, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10006, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10007, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10008, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10009, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10010, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10011, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10012, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10013, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10014, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10015, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10016, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10017, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10018, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10019, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10020, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10021, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10022, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10023, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10024, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10025, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10026, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10027, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10028, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
19:54:37.317 [main] INFO com.alian.excel.service.TestExcelService - EmployeeVo(id=BAT10029, name=梁南生, age=18, salary=20000.0, department=研发部, hireDate=2020-02-13)
结语
本工具类是针对简单Java对象实现读写的,尽量不要使用继承之类的,假设你的对象实现了序列化接口,可能就要对字段 serialVersionUID进行排除。还有就是各种类型的转换并没有写全(只包括常见类型),包括数据空值处理等,大家可以根据需要进行调整。
本文是先写入再读取,因为写入时字段的类型是字符串,所以读取的时候会转为对象相应的类型,Excel里的格式本身就是很复杂的,所以使用本工具类时,最好是使用本工具写入的,或者先把数据的格式先转为文本格式再使用。
如果你对账文件处理,建议还是使用CSV文件,参考:Java实现CSV文件的读写(包含追加内容)