Apache POI
是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。
结构:
03版本的Excel,即后缀名为.xls的。07版本的Excel,即后缀名为.xlsx的。
Excel03:只支持xls类型的文档,最多65536行、256列。
Excel07:除了支来持xls类型文档,还支持xlsx类型的文档,最多1048576行、6384列。
工作簿:一个Excel文件
工作表:一个Excel文件中的表
行、列:每一行、每一列
<!-- xls(Excel 03版本) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.9</version>
</dependency>
<!-- xlsx(Excel 07版本) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
<!-- 日期格式化工具 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
1、03版本
public class ExcelWriteTest {
// 文件输入位置
public static String Path = "D:\\";
@Test
public void testWrite03() throws IOException {
// 创建一个工作簿
Workbook workbook = new HSSFWorkbook();
// 根据工作簿创建一个工作表
Sheet sheet = workbook.createSheet("柳成荫");
// 根据工作表创建一行
Row row0 = sheet.createRow(0);
// 根据行创建一个单元格
Cell cell00 = row0.createCell(0);
cell00.setCellValue("今日听歌数"); // 即Excel里的坐标(0,0),第一行第一列的格子
Row row1 = sheet.createRow(1);
Cell cell10 = row1.createCell(0);
cell10.setCellValue(10); // 即Excel里的坐标(1,0),第二行第一列的格子
Cell cell01 = row0.createCell(1);
cell01.setCellValue("统计时间"); // (0,1)
Cell cell11 = row1.createCell(1);
cell11.setCellValue(new DateTime().toString("yyyy-MM-dd HH:mm:ss")); // (1,1)
// 生成一张表
OutputStream outputStream = new FileOutputStream(Path + "03版本测试.xls");
workbook.write(outputStream);
// 关闭流
outputStream.close();
System.out.println("03版本Excel生成完毕!");
}
}
2、07版本
相对上面03版本,代码没有什么变化,就是创建工作簿的对象(XSSFWorkbook)变了和文件后缀(.xlsx)变了
@Test
public void testWrite07() throws IOException {
// 创建一个工作簿 - 07
Workbook workbook = new XSSFWorkbook();
// 根据工作簿创建一个工作表
Sheet sheet = workbook.createSheet("柳成荫");
// 根据工作表创建一行
Row row0 = sheet.createRow(0);
// 根据行创建一个单元格
Cell cell00 = row0.createCell(0);
cell00.setCellValue("今日听歌数"); // 即Excel里的坐标(0,0),第一行第一列的格子
Row row1 = sheet.createRow(1);
Cell cell10 = row1.createCell(0);
cell10.setCellValue(10); // 即Excel里的坐标(1,0),第二行第一列的格子
Cell cell01 = row0.createCell(1);
cell01.setCellValue("统计时间"); // (0,1)
Cell cell11 = row1.createCell(1);
cell11.setCellValue(new DateTime().toString("yyyy-MM-dd HH:mm:ss")); // (1,1)
// 生成一张表
OutputStream outputStream = new FileOutputStream(Path + "07版本测试.xlsx");
workbook.write(outputStream);
// 关闭流
outputStream.close();
System.out.println("07版本Excel生成完毕!");
}
1、03版本
@Test
public void testWrite03BigData() throws IOException{
// 开始写入时间
long begin = System.currentTimeMillis();
// 创建工作簿
Workbook workbook = new HSSFWorkbook();
// 创建表
Sheet sheet = workbook.createSheet("九月清晨");
for (int rowNum = 0; rowNum < 65536; rowNum++){
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 256; cellNum++){
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum);
}
}
OutputStream outputStream = new FileOutputStream(Path + "03bigData.xls");
workbook.write(outputStream);
outputStream.close();
// 结束写入时间
long end = System.currentTimeMillis();
System.out.println("写入完毕,花费时间:");
System.out.println((double) (end - begin)/1000);
}
2、07版本
在03版本基础上,只需修改对象(XSSFWorkbook)和文件后缀名(.xlsx)即可。
相对03版本,写入相同的数据花费时间更长,但是07能写入的数据更多。
3、07版本升级版 - SXSSFWorkbook
它可以写入非常大的数据量,如100万条甚至更多,写数据速度极快,占用内存更少。
但是它会在写入过程中产生临时文件,默认是100条数据被保存在内存中,如果超过这个数量,则最前面的数据被写入临时文件,如果想要自定义内存中数据的数量,可以使用SXSSFWorkbook(数量)
。
@Test
public void testWrite07BigDataPlus() throws IOException{
// 开始写入时间
long begin = System.currentTimeMillis();
// 创建工作簿
Workbook workbook = new SXSSFWorkbook();
// 创建表
Sheet sheet = workbook.createSheet("九月清晨");
for (int rowNum = 0; rowNum < 65536; rowNum++){
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 256; cellNum++){
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum);
}
}
OutputStream outputStream = new FileOutputStream(Path + "07bigDataPlus.xlsx");
workbook.write(outputStream);
outputStream.close();
((SXSSFWorkbook) workbook).dispose(); // 删除生成的临时文件
// 结束写入时间
long end = System.currentTimeMillis();
System.out.println("写入完毕,花费时间:");
System.out.println((double) (end - begin)/1000);
}
我这个电脑有点捞,花费时间比较长,上面那个低配版本写了很久都没写完所以才没截图的。
读操作,需要根据类型来读,也就是需要判断类型。这里就以07版为例:
下面代码可以单独封装出来,根据类型判断。Excel表里的每一行内容实际也就是一个对象,也可以读出来之后封装到对象里。
@Test
public void testCellType() throws Exception{
// 获取文件流
InputStream inputStream = new FileInputStream("D:\\test.xlsx");
// 创建工作簿
Workbook workbook = new XSSFWorkbook(inputStream);
// 创建工作表 - 下标为0
Sheet sheet = workbook.getSheetAt(0);
// 获取标题内容 - 第一行
Row rowTitle = sheet.getRow(0);
if (rowTitle != null){
// 获取列数
int cellCount = rowTitle.getPhysicalNumberOfCells();
// 遍历列数取出每一个单元格
for(int cellNum = 0; cellNum < cellCount; cellNum++){
// 获取列
Cell cell = rowTitle.getCell(cellNum);
if(cell != null){
String cellValue = cell.getStringCellValue();
System.out.print(cellValue + "|");
}
}
System.out.println();
}
// 获取行数
int rowCount = sheet.getPhysicalNumberOfRows();
for (int rowNum = 1; rowNum < rowCount; rowNum++){
// 获取行
Row row = sheet.getRow(rowNum);
if (row != null){
// 获取列数
int cellCount = row.getPhysicalNumberOfCells();
for (int cellNum = 0; cellNum < cellCount; cellNum++){ // 遍历列
Cell cell = row.getCell(cellNum);
String value = "";
if (cell != null){
int cellType = cell.getCellType(); // 获取单元格数据类型
switch (cellType){
case HSSFCell.CELL_TYPE_STRING: // 字符串类型
System.out.print("【String类型】");
value = cell.getStringCellValue();
break;
case HSSFCell.CELL_TYPE_BOOLEAN: // 布尔类型
System.out.println("【boolean类型】");
value = String.valueOf(cell.getBooleanCellValue());
break;
case HSSFCell.CELL_TYPE_BLANK: // 为空
break;
case HSSFCell.CELL_TYPE_NUMERIC: // 数字(日期、普通数字)
if (HSSFDateUtil.isCellDateFormatted(cell)){ // 日期
System.out.println("【Date日期】");
value = new DateTime(cell.getDateCellValue()).toString("yyyy-MM-dd");
}else{ // 数字
System.out.println("【纯数字】");
cell.setCellType(HSSFCell.CELL_TYPE_STRING);
value = String.valueOf(cell.toString());
}
break;
case HSSFCell.CELL_TYPE_ERROR:
System.out.println("【数据类型错误】");
break;
}
System.out.println(value);
}
}
}
}
}
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便。
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。
先创建一个实体类
@Data
public class User {
@ExcelProperty("学生编号")
private Long id;
@ExcelProperty("学生姓名")
private String name;
@ExcelProperty("入学时间")
private Date date;
@ExcelProperty("入学成绩")
private Double grade;
@ExcelProperty("是否入学")
private Boolean isOk;
@ExcelIgnore // 忽略
private String sex;
}
创建写的测试类
@Test
public void testWrite(){
String fileName = "D:\\write.xlsx";
EasyExcel.write(fileName, User.class).sheet("学生列表").doWrite(getDate());
}
/**
* 获取数据
*/
public static List<User> getDate(){
List<User> list = new ArrayList<User>();
User user;
for (int i = 1; i <= 10; i++){
user = new User();
user.setId((long)i);
user.setName("学生" + i + "号");
user.setDate(new Date());
user.setGrade(99.99);
user.setIsOk(i%2==0);
user.setSex((i%2==0)?"男":"女");
list.add(user);
System.out.println(user);
}
return list;
}
先创建一个监听器
public class ExcelListener extends AnalysisEventListener<User> {
private List<User> users = new ArrayList<User>();
// 让外界可以调用
public List<User> getUser() {
return users;
}
// 一行一行读取Excel内容
@Override
public void invoke(User user, AnalysisContext analysisContext) {
System.out.println("读取中...");
users.add(user); // 加入到集合
}
// 读取表头内容
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头:" + headMap);
}
// 读取完成之后做什么事
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("读完了");
}
}
测试代码
@Test
public void testRead() {
// 实现Excel读操作
String fileName = "D:\\write.xlsx";
ExcelListener excelListener = new ExcelListener();
EasyExcel.read(fileName,User.class,excelListener).sheet().doRead();
List<User> users = excelListener.getUser();
for (User user : users) {
System.out.println(user);
}
}
因为在实际开发中,我们通常要调用某个服务模块,把从Excel里读取的数据存入到数据库。因此,修改监听器代码如下:
public class ExcelListener extends AnalysisEventListener<User> {
// 用于控制存储数据库
private static int BATCH_COUNT = 100;
// 用于存储数据
private List<User> users = new ArrayList<User>();
// 在这个监听器类没有办法通过注解的方式注入Service,只能通过参数传递
private UserService userService;
public ExcelListener(){}
public ExcelListener(UserService userService){
this.userService = userService;
}
// 一行一行读取Excel内容
@Override
public void invoke(User user, AnalysisContext analysisContext) {
users.add(user);
// 达到BATCH_COUNT了,需要存入一次数据,防止数据太多在内存,导致OOM
if(users.size() >= BATCH_COUNT){
userService.saveUsers(users); // 保存一次
users.clear(); // 清空List集合
}
}
// 读取表头内容
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头:" + headMap);
}
// 读取完成之后做什么事
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("读完了");
}
}