Apache POI
是Apache软件基金会的开源函式库,提供跨平台的Java API
实现Microsoft Office
格式档案读写。但是存在如下一些问题:
对POI有过深入了解的才知道原来POI还有SAX模式(Dom解析模式)。但SAX模式相对比较复杂,excel有03和07两种版本,两个版本数据存储方式截然不同,sax解析方式也各不一样。
想要了解清楚这两种解析方式,才去写代码测试,估计两天时间是需要的。再加上即使解析完,要转换到自己业务模型还要很多繁琐的代码。总体下来感觉至少需要三天,由于代码复杂,后续维护成本巨大。
POI的SAX模式的API可以一定程度的解决一些内存溢出的问题,但是POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大,一个3M的Excel用POI的SAX解析,依然需要100M左右内存。
大部分使用POI都是使用他的userModel模式。userModel的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而userModel模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。
总体上来说,简单写法重度依赖内存,复杂写法学习成本高。
功能强大
代码书写冗余繁杂
读写大文件耗费内存较大,容易OOM
EasyExcel重写了POI对07版Excel的解析,可以把内存消耗从100M左右降低到10M以内,并且再大的Excel不会出现内存溢出,03版仍依赖POI的SAX模式。
下图为64M内存1分钟内读取75M(46W行25列)的Excel(当然还有急速模式能更快,但是内存占用会在100M多一点)
在上层做了模型转换的封装,让使用者更加简单方便
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.1.6version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
/**
* 需求:单实体导入
* 导入Excel学员信息到系统。
* 包含如下列:姓名、出生日期、性别
* 模板:逐浪教育学员信息表.xls文件
*/
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 学生实体类
* lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
*
* @author 狐狸半面添
* @create 2023-02-26 14:56
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
/**
* 学生姓名
*/
private String name;
/**
* 学生出生日期
*/
private Date birthday;
/**
* 学生性别
*/
private String gender;
/**
* id
*/
private String id;
}
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.fox.easyexcel.domain.Student;
/**
* 读取文档的监听器类
*
* @author 狐狸半面添
* @create 2023-02-26 15:10
*/
public class StudentListener extends AnalysisEventListener<Student> {
/**
* 读监听器,每读一行内容,都会调用一次invoke,在invoke可以操作使用读取到的数据
*
* @param student 每次读取到的数据封装的对象
* @param analysisContext
*/
public void invoke(Student student, AnalysisContext analysisContext) {
System.out.println(student);
}
/**
* 读取完整个文档之后,调用的方法
*
* @param analysisContext
*/
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// todo
}
}
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.listener.StudentListener;
import org.junit.Test;
/**
* @author 狐狸半面添
* @create 2023-02-26 15:03
*/
public class ExcelTest {
/**
* 工作簿:一个excel文件就是一个工作簿
* 工作表:一个工作簿可以有多个工作表(sheet)
*/
@Test
public void test01() {
/*
1.获得一个工作簿对象
构建一个读的工作簿对象,参数说明:
- pathName:要读的文件的路径
- head:文件中每一行数据要存储到的实体的类型的class
- readListener:读监听器,每读一行内容,都会调用一次该对象的invoke,在invoke可以操作使用读取到的数据
*/
ExcelReaderBuilder readWorkBook = EasyExcel.read("逐浪教育学员信息表.xlsx", Student.class, new StudentListener());
// 2.获得一个工作表对象,默认读取第一个工作表
ExcelReaderSheetBuilder sheet = readWorkBook.sheet();
// 3.读取工作表中的内容
sheet.doRead();
}
}
/**
* 需求:单实体导出
* 导出多个学生对象到Excel表格
* 包含如下列:姓名、出生日期、性别
* 模板详见:逐浪教育学员信息表.xlsx
*/
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 学生实体类
* lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
* 注解 @ContentRowHeight():内容的行高
* 注解 @HeadRowHeight:表头的行高
*
* @author 狐狸半面添
* @create 2023-02-26 14:56
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
/**
* 学生姓名
*/
@ExcelProperty("学生姓名")
@ColumnWidth(20)
private String name;
/**
* 学生出生日期
*/
@ExcelProperty("出生日期")
@ColumnWidth(20)
private Date birthday;
/**
* index 从0开始
* 学生性别
*/
@ExcelProperty(value = "学生性别", index = 1)
private String gender;
/**
* id
*/
@ExcelIgnore
private String id;
}
@Test
public void test02() {
/*
1.构建一个写的工作簿对象
- pathName:要写入的文件路径
- head:封装写入的数据的实体类型
*/
ExcelWriterBuilder writeWorkBook = EasyExcel.write("逐浪教育学员信息表.xlsx", Student.class);
// 2.获取工作表对象,默认是第一个工作表
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 3.生成十个测试对象
ArrayList<Student> students = new ArrayList<Student>();
for (int i = 1; i <= 10; i++) {
students.add(new Student("逐浪者-" + i, new Date(), "男", null));
}
// 4.将数据写入工作表
sheet.doWrite(students);
}
基于SpringMVC的文件上传和下载
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.1.6version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.6.3version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
/**
* 学生姓名
*/
private String name;
/**
* 学生性别
*/
private String gender;
/**
* 学生出生日期
*/
private Date birthday;
/**
* id
*/
private String id;
}
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.service.StudentService;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
/**
* 读取文档的监听器类
*
* @author 狐狸半面添
* @create 2023-02-26 15:10
*/
@Component
@Scope("prototype") //作者要求每次读取都要使用新的Listener
public class StudentListener extends AnalysisEventListener<Student> {
@Resource
private StudentService studentService;
public static final ThreadLocal<ArrayList<Student>> threadLocal = new ThreadLocal<ArrayList<Student>>();
/**
* 读监听器,每读一行内容,都会调用一次invoke,在invoke可以操作使用读取到的数据
*
* @param student 每次读取到的数据封装的对象
* @param analysisContext
*/
public void invoke(Student student, AnalysisContext analysisContext) {
ArrayList<Student> students = threadLocal.get();
if (students == null) {
threadLocal.set(new ArrayList<Student>());
students = threadLocal.get();
}
students.add(student);
if (students.size() == 5) {
studentService.save(students);
students.clear();
}
}
/**
* 读取完整个文档之后,调用的方法
*
* @param analysisContext
*/
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// todo
}
}
import com.fox.easyexcel.domain.Student;
import java.util.ArrayList;
/**
* @author 狐狸半面添
* @create 2023-02-26 16:27
*/
public interface StudentService {
/**
* 保存学生信息
*
* @param students 信息列表
*/
void save(ArrayList<Student> students);
}
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.service.StudentService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
/**
* @author 狐狸半面添
* @create 2023-02-26 16:27
*/
@Service
public class StudentServiceImpl implements StudentService {
@Override
public void save(ArrayList<Student> students) {
System.out.println("save to database = " + students);
}
}
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.listener.StudentListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
* @author 狐狸半面添
* @create 2023-02-26 16:29
*/
@RestController
@RequestMapping("/student")
@Slf4j
public class StudentController {
@Resource
private StudentListener studentListener;
@PostMapping("/read")
public String readExcel(MultipartFile uploadExcel) {
try {
// 1.获取工作簿
ExcelReaderBuilder readWorkBook = EasyExcel.read(uploadExcel.getInputStream(), Student.class, studentListener);
// 2.获取工作表
ExcelReaderSheetBuilder sheet = readWorkBook.sheet();
// 3.读取数据
sheet.doRead();
// 4.释放线程
StudentListener.threadLocal.remove();
return "success";
} catch (Exception e) {
log.error("读取文件失败:{}", e.getMessage());
return "fail";
}
}
}
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 学生实体类
* lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
* 注解 @ContentRowHeight():内容的行高
* 注解 @HeadRowHeight:表头的行高
*
* @author 狐狸半面添
* @create 2023-02-26 14:56
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
/**
* id
*/
@ExcelIgnore
private String id;
/**
* 学生姓名
*/
@ExcelProperty("学生姓名")
private String name;
/**
* 学生性别
*/
@ExcelProperty("学生性别")
private String gender;
/**
* 学生出生日期
*/
@ExcelProperty("学生出生日期")
private Date birthday;
}
/**
* 文件下载
* 1. 编写实体类并创建对象以便写入表格
* 2. 设置响应参数:文件的ContentType和文件名,同时设置编码避免乱码
* 3. 直接写,内部会调用finish方法自动关闭OutputStream
*/
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 防止中文乱码:import java.net.URLEncoder
String fileName = URLEncoder.encode("测试", "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
// 创建文件簿
ExcelWriterBuilder workBook = EasyExcel.write(response.getOutputStream(), Student.class);
// 创建一个文件表
ExcelWriterSheetBuilder sheet = workBook.sheet("模板");
// 生成测试数据
ArrayList<Student> students = new ArrayList<Student>();
for (int i = 1; i <= 10; i++) {
students.add(new Student(null, "逐浪者-" + i, "男", new Date()));
}
// 写入,并且会关闭流
sheet.doWrite(students);
}
访问:http://localhost:8080/student/download
EasyExcel支持调整行高、列宽、背景色、字体大小等内容,但是控制方式与使用原生POI无异,比较繁琐,不建议使用。
但是可以使用模板填充的方式,向预设样式的表格中直接写入数据,写入数据的时候会保持原有样式。
Excel表格中用{} 来表示包裹要填充的变量,如果单元格文本中本来就有{
、}
左右大括号,需要在括号前面使用斜杠转义\{
、\}
。
代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。
编写封装填充数据的类或选用Map
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 使用实体类封装填充数据
* 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
*
* @author 狐狸半面添
* @create 2023-02-26 17:32
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
}
准备数据并填充到文件
/**
* 单组数据填充:封装类的方式
*/
@Test
public void test03() {
// 1.准备模板
String templatePath = FillData.class.getResource("/fill_data_template1.xlsx").getPath();
// 2.创建一个工作簿对象
ExcelWriterBuilder writeWorkBook = EasyExcel.write("excel填充-单组数据.xlsx", FillData.class)
.withTemplate(templatePath);
// 3.创建工作表对象
ExcelWriterSheetBuilder sheet1 = writeWorkBook.sheet("sheet1");
// 4.准备数据
FillData fillData = new FillData("逐浪者", 19);
// 5.填充数据,并关闭流
sheet1.doFill(fillData);
}
/**
* 单组数据填充:map的方式
*/
@Test
public void test04() {
// 1.准备模板
String templatePath = FillData.class.getResource("/fill_data_template1.xlsx").getPath();
// 2.创建一个工作簿对象
ExcelWriterBuilder writeWorkBook = EasyExcel.write("excel填充-单组数据.xlsx", FillData.class)
.withTemplate(templatePath);
// 3.创建工作表对象
ExcelWriterSheetBuilder sheet1 = writeWorkBook.sheet("sheet1");
// 4.准备数据:使用Map数据填充
HashMap<String, Object> mapFillData = new HashMap<>();
mapFillData.put("name", "狐狸半面添");
mapFillData.put("age", 18);
// 5.填充数据,并关闭流
sheet1.doFill(mapFillData);
}
Excel表格中用{.}
来表示包裹要填充的变量,如果单元格文本中本来就有{
、}
左右大括号,需要在括号前面使用斜杠转义\{
、\}
。
代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。
编写封装填充数据的类或选用Map
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 使用实体类封装填充数据
* 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
*
* @author 狐狸半面添
* @create 2023-02-26 17:32
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
}
准备数据并填充到文件
/**
* 多组数据填充:封装类的方式
*/
@Test
public void test05() {
// 1.准备模板
String templatePath = FillData.class.getResource("/fill_data_template2.xlsx").getPath();
// 2.创建一个工作簿对象
ExcelWriterBuilder writeWorkBook = EasyExcel.write("excel填充-多组数据.xlsx", FillData.class)
.withTemplate(templatePath);
// 3.创建工作表对象
ExcelWriterSheetBuilder sheet1 = writeWorkBook.sheet("sheet1");
// 4.准备数据
ArrayList<FillData> data = new ArrayList<>();
for (int i = 15; i <= 30; i++) {
data.add(new FillData("逐浪者-" + i, i));
}
// 5.填充数据,并关闭流
sheet1.doFill(data);
}
即有多组数据填充,又有单一数据填充,为了避免两者数据出现冲突覆盖的情况,在多组填充时需要通过FillConfig
对象设置换行。
编写封装填充数据的类或选用Map
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 使用实体类封装填充数据
* 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
*
* @author 狐狸半面添
* @create 2023-02-26 17:32
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
}
准备数据并填充到文件
/**
* 组合数据填充
*/
@Test
public void test06() {
// 1.准备模板
String templatePath = FillData.class.getResource("/fill_data_template3.xlsx").getPath();
// 2.创建一个工作簿对象
ExcelWriter workBook = EasyExcel.write("excel填充-组合数据.xlsx", FillData.class)
.withTemplate(templatePath).build();
// 3.创建工作表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 4.准备数据
ArrayList<FillData> data = new ArrayList<>();
for (int i = 15; i <= 30; i++) {
data.add(new FillData("逐浪者-" + i, i));
}
HashMap<String, String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date", "2023-02-26");
dateAndTotal.put("total", "10000");
// 5.填充数据,会一并关闭流
// sheet1.doFill(data);
// 多组填充
workBook.fill(data, fillConfig, sheet);
// 单组填充
workBook.fill(dateAndTotal, sheet);
// 关闭流
workBook.finish();
}
水平填充和多组填充模板一样,不一样的地方在于,填充时需要通过FillConfig
对象设置水平填充。
编写封装填充数据的类或选用Map
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 使用实体类封装填充数据
* 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
*
* @author 狐狸半面添
* @create 2023-02-26 17:32
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
}
准备数据并填充到文件
/**
* 水平填充
*/
@Test
public void test07() {
// 1.准备模板
String templatePath = FillData.class.getResource("/fill_data_template4.xlsx").getPath();
// 2.创建一个工作簿对象
ExcelWriter workBook = EasyExcel.write("excel填充-水平填充.xlsx", FillData.class)
.withTemplate(templatePath).build();
// 3.创建工作表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// 4.数据准备
ArrayList<FillData> data = new ArrayList<>();
for (int i = 15; i <= 30; i++) {
data.add(new FillData("逐浪者-" + i, i));
}
// 换行
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 多组填充
workBook.fill(data, fillConfig, sheet);
// 关闭流
workBook.finish();
}
为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而是采用的是一行一行写入的方式,不能实现删除和移动行,也不支持备注写入。多组数据写入的时候,如果需要新增行,只能在最后一行增加,不能在中间位置添加。
/**
* 报表导出
*/
@Test
public void test08() {
// 1.准备模板
String templatePath = FillData.class.getResource("/report_template.xlsx").getPath();
// 2.创建一个工作簿对象
ExcelWriter workBook = EasyExcel.write("报表.xlsx", FillData.class)
.withTemplate(templatePath).build();
// 3.创建工作表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// ****** 准备数据 *******
// 日期
HashMap<String, String> dateMap = new HashMap<>();
dateMap.put("date", "2020-03-16");
// 总会员数
HashMap<String, String> totalCountMap = new HashMap<>();
dateMap.put("totalCount", "1000");
// 新增员数
HashMap<String, String> increaseCountMap = new HashMap<>();
dateMap.put("increaseCount", "100");
// 本周新增会员数
HashMap<String, String> increaseCountWeekMap = new HashMap<>();
dateMap.put("increaseCountWeek", "50");
// 本月新增会员数
HashMap<String, String> increaseCountMonthMap = new HashMap<>();
dateMap.put("increaseCountMonth", "100");
// 新增会员数据
List<Student> students = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
students.add(new Student(null, "逐浪者-" + i, "男", new Date()));
}
// **** 准备数据结束****
// 写入统计数据
workBook.fill(dateMap, sheet);
workBook.fill(totalCountMap, sheet);
workBook.fill(increaseCountMap, sheet);
workBook.fill(increaseCountWeekMap, sheet);
workBook.fill(increaseCountMonthMap, sheet);
// 写入新增会员
workBook.fill(students, sheet);
// 关闭流
workBook.finish();
}
invoke
方法内部;所有配置都是继承的。Workbook的配置会被Sheet继承。所以在用EasyExcel设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个sheet,之后针对单个sheet。
使用位置:标准作用在成员变量上,吧实体类中属性和excel表中列关联起来。
可选属性:
属性名 | 含义 | 说明 |
---|---|---|
index | 对应Excel表中的列数 | 默认-1,建议指定时从0开始 |
value | 对应Excel表中的列头 | |
converter | 成员变量转换器 | 自定义转换器需要实Converter接口 |
使用效果:index属性可以指定当前字段对应excel中的哪一列,可以根据列名value去匹配,也可以不写。
如果不使用@ExcelProperty注解,成员变量从上到下的顺序,对应表格中从左到右的顺序;
**使用建议:**要么全部不写,要么全部用index,要么全部用名字去匹配,尽量不要三个混着用。
标注在成员变量上,默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
标注在成员变量上,日期转换,代码中用String类型的成员变量
去接收excel中日期格式的数据
会调用这个注解。里面的value
参照java.text.SimpleDateFormat
/**
* 学生出生日期
*/
@ExcelProperty("出生日期")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
标注在成员变量上,数字转换,代码中用String类型的成员变量
去接收excel数字格式的数据
会调用这个注解。里面的value
参照java.text.DecimalFormat
标注在类上。
不标注该注解时,默认类中所有成员变量都会参与读写,无论是否在成员变量上加了@ExcelProperty
的注解。
标注该注解后,类中的成员变量如果没有标注 @ExcelProperty
注解将不会参与读写。
ReadWorkbook
,ReadSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。
readListener
监听器,在读取数据的过程中会不断的调用监听器。
headRowNumber
指定需要读表格的 列头行数。默认有一行头,也就是认为第二行开始起为数据。
head
与 clazz
二选一。读取文件头对应的列表,会根据列表匹配数据。建议使用class,就是文件中每一行数据对应的代码中的实体类型。
clazz
与 head
二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据。
autoTrim
字符串、表头等数据自动trim
password
读的时候是否需要使用密码
excelType
当前excel的类型,读取时会自动判断,无需设置。inputStream
与file
二选一。建议使用file。file
与inputStream
二选一。读取文件的文件。autoCloseStream
自动关闭流。readCache
默认小于5M用 内存,超过5M会使用 EhCache
,不建议使用这个参数。useDefaultListener
@since 2.1.4
默认会加入ModelBuildEventListener
来帮忙转换成传入class
的对象,设置成false
后将不会协助转换对象,自定义的监听器会接收到Map
对象,如果还想继续接听到class
对象,请调用readListener
方法,加入自定义的beforeListener
、 ModelBuildEventListener
、 自定义的afterListener
即可。sheetNo
需要读取Sheet的编号,建议使用这个来指定读取哪个SheetsheetName
根据名字去匹配Sheet,excel 2003不支持根据名字去匹配使用位置:标准作用在成员变量上
可选属性:
属性名 | 含义 | 说明 |
---|---|---|
index | 对应Excel表中的列数 | 默认-1,指定时建议从0开始 |
value | 对应Excel表中的列头 | |
converter | 成员变量转换器 | 自定义转换器需要实Converter接口 |
使用效果:
index
指定写到第几列,如果不指定则根据成员变量位置排序;
value
指定写入的列头,如果不指定则使用成员变量的名字作为列头;
如果要设置复杂的头,可以为value指定多个值。
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 学生实体类
* lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
* 注解 @ContentRowHeight():内容的行高
* 注解 @HeadRowHeight:表头的行高
*
* @author 狐狸半面添
* @create 2023-02-26 14:56
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
/**
* 学生姓名
*/
@ExcelProperty(value = {"学员信息表", "学生姓名"})
@ColumnWidth(20)
private String name;
/**
* 学生出生日期
*/
@ExcelProperty(value = {"学员信息表", "出生日期"})
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
/**
* index 从0开始
* 学生性别
*/
@ExcelProperty(value = {"学员信息表", "学员性别"})
@ColumnWidth(20)
private String gender;
/**
* id
*/
@ExcelIgnore
private String id;
}
基本和读取时一致
@ContentRowHeight()
:标注在类上或属性上,指定内容行高
@HeadRowHeight()
:标注在类上或属性上,指定列头行高
@ColumnWidth()
:标注在类上或属性上,指定列宽
@ExcelIgnore
:默认所有字段都会写入excel,这个注解会忽略这个字段
DateTimeFormat
:日期转换,将Date
写到excel会调用这个注解。里面的value
参照java.text.SimpleDateFormat
NumberFormat
:数字转换,用Number
写excel会调用这个注解。里面的value
参照java.text.DecimalFormat
ExcelIgnoreUnannotated
:默认不加 ExcelProperty
的注解的都会参与读写,加了不会参与
WriteWorkbook
、WriteSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。
writeHandler
写的处理器。可以实现WorkbookWriteHandler
,SheetWriteHandler
,RowWriteHandler
,CellWriteHandler
,在写入excel的不同阶段会调用,对使用者透明不可见。
relativeHeadRowIndex
距离多少行后开始。也就是开头空几行
needHead
是否导出头
head
与clazz
二选一。写入文件的头列表,建议使用class。
clazz
与head
二选一。写入文件的头对应的class,也可以使用注解。
autoTrim
字符串、表头等数据自动trim
excelType
当前excel的类型,默认为xlsx
outputStream
与file
二选一。写入文件的流
file
与outputStream
二选一。写入的文件
templateInputStream
模板的文件流
templateFile
模板文件
autoCloseStream
自动关闭流。
password
写的时候是否需要使用密码
useDefaultStyle
写的时候是否是使用默认头
sheetNo
需要写入的编号。默认0
sheetName
需要些的Sheet名称,默认同sheetNo
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* excel通用读取监听类
*
* @author 狐狸半面添
* @create 2023-02-26 15:10
*/
@Slf4j
@Getter
@NoArgsConstructor
public class ExcelListener<T> extends AnalysisEventListener<T> {
/**
* 自定义用于暂时存储data 可以通过实例获取该值
*/
private final List<T> dataList = new ArrayList<>();
/**
* 每解析一行都会回调invoke()方法
*
* @param data 每一行的数据
*/
@Override
public void invoke(T data, AnalysisContext context) {
dataList.add(data);
log.info("读取的一条信息:{}", data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("{}条数据,解析完成", dataList.size());
}
}
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.fox.easyexcel.listener.ExcelListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Set;
/**
* @author 狐狸半面添
* @create 2023-02-26 21:50
*/
public class EasyExcelUtils {
private static final Log log = LogFactory.getLog(EasyExcelUtils.class);
/**
* 单sheet版本Excel读取
* 从Excel中读取文件,读取的文件是一个DTO类
*
* @param inputStream 文件流
* @param clazz 行数据类型
*/
public static <T> List<T> readExcelOneSheet(InputStream inputStream, final Class<?> clazz) {
// 1.创建监听类
ExcelListener<T> listener = new ExcelListener<>();
// 2.构建工作簿对象的输入流
ExcelReader excelReader = EasyExcel.read(inputStream, clazz, listener).build();
// 3.构建工作表对象的输入流,默认是第一张工作表
ReadSheet readSheet = EasyExcel.readSheet(0).build();
// 4.读取信息,每读取一行都会调用监听类的 invoke 方法
excelReader.read(readSheet);
// 5.关闭流,如果不关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.finish();
return listener.getDataList();
}
/**
* 多sheet版本Excel读取
*
* @param 行数据的类型
* @param filePath 文件路径
* @param clazz 行数据的类型
* @return 所有信息
*/
public static <T> List<T> readExcelAllSheet(String filePath, final Class<?> clazz) {
ExcelListener<T> listener = new ExcelListener<>();
// 读取全部sheet
// 这里需要注意 ExcelListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个DemoDataListener里面写
EasyExcel.read(filePath, clazz, listener).doReadAll();
return listener.getDataList();
}
/**
* 网页上的下载导出,只有一个工作表
*
* @param fileName 文件名
* @param clazz 类的字节码文件,行数据的类型
* @param dataList 导出的数据
* @param sheetName 工作表名
* @param response 响应体
* @throws IOException 异常对象
*/
public static void writeWeb(String fileName, final Class<?> clazz, List<?> dataList, String sheetName, HttpServletResponse response) throws IOException {
// 1.指定响应体内容类型
response.setContentType("application/vnd.ms-excel");
// 2.指定编码方式
response.setCharacterEncoding("utf-8");
// 3.URLEncoder.encode可以防止中文乱码:import java.net.URLEncoder
fileName = URLEncoder.encode(fileName, "UTF-8");
// 4.指定响应标头
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
// 5.获取工作簿对象的输出流
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// 6.设置工作表的名称
if (!StringUtils.hasText(sheetName)) {
sheetName = "sheet1";
}
// 7.指定写用哪个class去写
WriteSheet writeSheet = EasyExcel.writerSheet(0, sheetName).head(clazz).build();
// 8.将 dataList 中的数据逐行写入工作表中
excelWriter.write(dataList, writeSheet);
// 9.finish关闭流
excelWriter.finish();
// 10.关闭流
response.getOutputStream().close();
}
public static <T> void writeExcelList(HttpServletResponse response, List<List<T>> data, String fileName, Class<?> clazz, String sheetName) throws Exception {
OutputStream out = getOutputStream(fileName, response);
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(out, clazz).excelType(ExcelTypeEnum.XLSX).registerWriteHandler(getDefaultHorizontalCellStyleStrategy());
ExcelWriter excelWriter = excelWriterBuilder.build();
ExcelWriterSheetBuilder excelWriterSheetBuilder;
WriteSheet writeSheet;
for (int i = 1; i <= data.size(); i++) {
excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
excelWriterSheetBuilder.sheetNo(i);
excelWriterSheetBuilder.sheetName(sheetName + i);
writeSheet = excelWriterSheetBuilder.build();
excelWriter.write(data.get(i - 1), writeSheet);
}
excelWriter.finish();
out.close();
}
private static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
fileName = URLEncoder.encode(fileName, "UTF-8");
// response.setContentType("application/vnd.ms-excel"); // .xls
// .xlsx
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf8");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
return response.getOutputStream();
}
/**
* 获取默认表头内容的样式
*
* @return
*/
private static HorizontalCellStyleStrategy getDefaultHorizontalCellStyleStrategy() {
/** 表头样式 **/
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景色(浅灰色)
// 可以参考:https://www.cnblogs.com/vofill/p/11230387.html
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
// 字体大小
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 10);
headWriteCellStyle.setWriteFont(headWriteFont);
//设置表头居中对齐
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
/** 内容样式 **/
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 内容字体样式(名称、大小)
WriteFont contentWriteFont = new WriteFont();
contentWriteFont.setFontName("宋体");
contentWriteFont.setFontHeightInPoints((short) 10);
contentWriteCellStyle.setWriteFont(contentWriteFont);
// //设置内容垂直居中对齐
// contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// //设置内容水平居中对齐
// contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 设置边框样式
contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
// 头样式与内容样式合并
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}
/**
* 导出 Excel到指定目录 :单个 sheet,带表头,
*
* @param tableData
* @param fileName 导出的路径+文件名 例如: file/test.xlsx
* @param sheetName 导入文件的 sheet 名
* @throws Exception
*/
public static void writeExcelAutoColumnWidth(String fileName, List<?> tableData, String sheetName, Class<?> clazz) throws Exception {
// 根据用户传入字段 假设我们要忽略 date
EasyExcel.write(fileName, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.doWrite(tableData);
}
/**
* 导出 Excel到指定目录 :单个 sheet,带表头,
*
* @param fileName 导出的路径+文件名 例如: file/test.xlsx
* @param tableData
*/
public static void writeExcelWithOneSheet1(String fileName, List<?> tableData, String sheetName, Class<?> clazz, Set<String> excludeColumnFiledNames) {
// 根据用户传入字段 假设我们要忽略 date
EasyExcel.write(fileName, clazz)
.excludeColumnFiledNames(excludeColumnFiledNames)
.sheet(sheetName)
.registerWriteHandler(styleWrite(false))
.doWrite(tableData);
}
public static HorizontalCellStyleStrategy styleWrite(boolean isWrapped) {
// 头的策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景设置为红色
// headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 18);
headWriteCellStyle.setWriteFont(headWriteFont);
// 内容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
//contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 背景绿色
//contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
WriteFont contentWriteFont = new WriteFont();
// 字体大小
contentWriteFont.setFontHeightInPoints((short) 11);
//设置 自动换行
contentWriteCellStyle.setWrapped(isWrapped);
//设置 垂直居中
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
//EasyExcel.write(fileName, DemoData.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板")
// .doWrite(data());
}
}