1.入门案例
EasyPoi 是一个基于 Java 的 Excel/Word 导入导出工具库,封装了 Apache POI 的复杂操作,提供简洁的注解式 API,支持 Excel 和 Word 的快速读写、模板导出、大数据量导出等功能。
核心功能
- 注解式操作
通过注解简化实体类与 Excel/Word 的映射关系,无需手动编写复杂代码。 - 模板导出
支持 Freemarker 模板引擎,实现复杂格式的 Excel/Word 导出。 - 大数据处理
通过 SXSSF 技术优化内存占用,支持百万级数据导出。 - 导入校验
提供字段校验功能,如非空、格式、长度等。
添加依赖如下:
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.4.0</version>
</dependency>
在实体上添加注解
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelIgnore;
import java.util.Date;
public class Student {
@Excel(name = "学生编号")
private String id;
@Excel(name = "学生姓名")
private String name;
@Excel(name = "学生密码", desensitizationRule = "1_1")
private String password;
@Excel(name = "学生性别", replace = {"男_1", "女_0"}, suffix = "生")
private int sex;
@Excel(name = "出生日期", format = "yyyy-MM-dd")
private Date birthday;
@ExcelIgnore
private int age;
}
其中学生姓名定义了我们的列的行高,学生性别因为我们基本上都是存在数据库都是数字所以我们转换下,两个日期我们都是进行了格式化输出了,这样我们就完成了业务对我们Excel的样式需求,后面只有把这个学生列表输出就可以了
生成Excel代码如下
测试类如下
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import org.apache.poi.ss.usermodel.Workbook;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class MainTest {
public static void main(String[] args) throws IOException {
write();
read();
}
private static void write() throws IOException {
List<Student> list = new ArrayList<>();
list.add(new Student("1", "张三", "123", 1, new Date()));
list.add(new Student("2", "李四", "123", 1, new Date()));
list.add(new Student("3", "王二", "123", 1, new Date()));
list.add(new Student("4", "小花", "123", 0, new Date()));
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams("计算机一班学生", "学生信息"), Student.class, list);
OutputStream outputStream = new FileOutputStream("D:/学生统计.xls");
workbook.write(outputStream);
outputStream.close();
}
private static void read() {
ImportParams params = new ImportParams();
params.setTitleRows(1);
params.setHeadRows(1);
List<Student> list = ExcelImportUtil.importExcel(new File("D:/学生统计.xls"), Student.class, params);
System.out.println(list);
}
}
2.常用注解
easypoi起因就是Excel的导入导出,最初的模板是实体和Excel的对应,model--row,filed--col 这样利用注解我们可以和容易做到excel到导入导出 经过一段时间发展,现在注解有5个类分别是
- @Excel 作用到filed上面,是对Excel一列的一个描述
- @ExcelCollection 表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示
- @ExcelEntity 表示一个继续深入导出的实体,但他没有太多的实际意义,只是告诉系统这个对象里面同样有导出的字段
- @ExcelIgnore 和名字一样表示这个字段被忽略跳过这个导导出
- @ExcelTarget 这个是作用于最外层的对象,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理
@Excel
这个是必须使用的注解,如果需求简单只使用这一个注解也是可以的,涵盖了常用的Excel需求,需要大家熟悉这个功能,主要分为基础,图片处理,时间处理,合并处理几块,name_id是上面讲的id用法,这里就不累言了
属性 类型 默认值 功能
name String null 列名,支持name_id
needMerge boolean fasle 纵向合并单元格
orderNum String "0" 列的排序,支持name_id
replace String[] {} 值得替换 导出是{a_id,b_id} 导入反过来
savePath String "upload"导入文件保存路径,如果是图片可以填写,默认是upload/className/ IconEntity这个类对应的就是upload/Icon/
type int 1 导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本
width double 10 列宽
height double 10 列高,后期打算统一使用@ExcelTarget的height,这个会被废弃,注意
isStatistics boolean fasle 自动统计数据,在追加一行统计,把所有数据都和输出 这个处理会吞没异常,请注意这一点
isHyperlink boolean false 超链接,如果是需要实现接口返回对象
isImportField boolean true 校验字段,看看这个字段是不是导入的Excel中有,如果没有说明是错误的Excel,读取失败,支持name_id
exportFormat String "" 导出的时间格式,以这个是否为空来判断是否需要格式化日期
importFormat String "" 导入的时间格式,以这个是否为空来判断是否需要格式化日期
format String "" 时间格式,相当于同时设置了exportFormat 和 importFormat
databaseFormat String "yyyyMMddHHmmss" 导出时间设置,如果字段是Date类型则不需要设置 数据库如果是string 类型,这个需要设置这个数据库格式,用以转换时间格式输出
numFormat String "" 数字格式化,参数是Pattern,使用的对象是DecimalFormat
imageType int 1 导出类型 1 从file读取 2 是从数据库中读取 默认是文件 同样导入也是一样的
suffix String "" 文字后缀,如% 90 变成90%
isWrap boolean true 是否换行 即支持\n
mergeRely int[] {} 合并单元格依赖关系,比如第二列合并是基于第一列 则{0}就可以了
mergeVertical boolean fasle 纵向合并内容相同的单元格
fixedIndex int -1 对应excel的列,忽略名字
isColumnHidden boolean false 导出隐藏列
@ExcelTarget
限定一个到处实体的注解,以及一些通用设置,作用于最外面的实体
属性 类型 默认值 功能
value String null 定义ID
height double 10 设置行高
fontSize short 11 设置文字大小
注解中的ID的用法 这个ID算是一个比较独特的例子,比如
@ExcelTarget("teacherEntity")
public class TeacherEntity implements java.io.Serializable {
/** name */
@Excel(name = "主讲老师_teacherEntity,代课老师_absent", orderNum = "1", mergeVertical = true,needMerge=true,isImportField = "true_major,true_absent")
private String name;
这里的@ExcelTarget 表示使用teacherEntity这个对象是可以针对不同字段做不同处理 同样的ExcelEntity 和ExcelCollection 都支持这种方式 当导出这对象时,name这一列对应的是主讲老师,而不是代课老师还有很多字段都支持这种做法
@ExcelEntity
标记是不是导出excel 标记为实体类,一遍是一个内部属性类,标记是否继续穿透,可以自定义内部id
属性 类型 默认值 功能
id String null 定义ID
@ExcelCollection
一对多的集合注解,用以标记集合是否被数据以及集合的整体排序
属性 类型 默认值 功能
id String null 定义ID
name String null 定义集合列名,支持nanm_id
orderNum int 0 排序,支持name_id
type Class<?> ArrayList.class 导入时创建对象使用
@ExcelIgnore
忽略这个属性,多使用需循环引用中,无需多解释吧^^
3.级联关系
一个课程对应一个老师
一个课程对应N个学生
教师的实体:
public class StudentEntity implements Serializable{
private String id;
@Excel(name = "学生姓名", height = 20, width = 30, isImportField = "true_st")
private String name;
@Excel(name = "学生性别", replace = {"男_1", "女_2"}, suffix = "生", isImportField = "true_st")
private int sex;
@Excel(name = "出生日期", databaseFormat = "yyyyMMddHHmmss", format = "yyyy-MM-dd", isImportField = "true_st", width = 20)
private Date birthday;
@Excel(name = "进校日期", databaseFormat = "yyyyMMddHHmmss", format = "yyyy-MM-dd")
private Date registrationDate;
}
课程的实体:
@ExcelTarget("courseEntity")
public class CourseEntity implements java.io.Serializable {
/** 主键 */
private String id;
/** 课程名称 */
@Excel(name = "课程名称", orderNum = "1", width = 25)
private String name;
/** 老师主键 */
@ExcelEntity(id = "absent")
private TeacherEntity mathTeacher;
@ExcelCollection(name = "学生", orderNum = "4")
private List<StudentEntity> students;
}
教师的实体:
@ExcelTarget("teacherEntity")
public class TeacherEntity implements java.io.Serializable {
private String id;
/** name */
@Excel(name = "主讲老师_major,代课老师_absent", orderNum = "1", isImportField = "true_major,true_absent")
private String name;
这里在课程这个实体里面就完成了一对多的导出,达到了我们基础需求
同时使用了orderNum对我们的列进行了排序,满足老师的需求,导出代码如下
public static void main(String[] args) throws IOException {
List<StudentEntity> studentEntities1 = new ArrayList<StudentEntity>();
studentEntities1.add( new StudentEntity( "1", "张三", 1, new Date(), new Date() ) );
studentEntities1.add( new StudentEntity( "2", "李四", 1, new Date(), new Date() ) );
List<StudentEntity> studentEntities2 = new ArrayList<StudentEntity>();
studentEntities2.add( new StudentEntity( "3", "王二", 1, new Date(), new Date() ) );
studentEntities2.add( new StudentEntity( "4", "码子", 1, new Date(), new Date() ) );
TeacherEntity teacherEntity1 = new TeacherEntity( "1", "张老师" );
TeacherEntity teacherEntity2 = new TeacherEntity( "2", "李老师" );
List<CourseEntity> courseEntities = new ArrayList<CourseEntity>();
courseEntities.add( new CourseEntity( "1", "java", teacherEntity1, studentEntities1 ) );
courseEntities.add( new CourseEntity( "2", "UI", teacherEntity2, studentEntities2 ) );
Workbook workbook = ExcelExportUtil.exportExcel( new ExportParams( "课程", "sheet1" ),
CourseEntity.class, courseEntities );
OutputStream outputStream = new FileOutputStream( "D:/课程统计.xls" );
workbook.write( outputStream );
}
这样我们就完成了老师的需求,但是课程名和代课老师没有合并,不太美观
所以给课程名称和代课老师加了needMerge = true的属性,就可以完成单元格的合并
@Excel(name = "课程名称", orderNum = "1", width = 25,needMerge = true)
private String name;
@Excel(name = "主讲老师_major,代课老师_absent", orderNum = "1",needMerge = true, isImportField = "true_major,true_absent")
这样我们就完成了级联关系的导出
4.图片的导出
在日常运作中不可避免的会遇到图片的导入导出
这里提供了两种类型的图片导出方式
1.路径式
@Excel(name = "公司LOGO", type = 2 ,width = 40 , height = 20,imageType = 1)
private String companyLogo;
imageType=1 (默认可以不填),表示从file读取,字段类型是个字符串类型 可以用相对路径也可以用绝对路径,绝对路径优先依次获取
imageType=2 ,表示从数据库或者已经读取完毕,字段类型是个字节数组 直接使用 同时
image 类型的cell最好设置好宽和高,否则会百分百缩放到cell那么大,不是原尺寸,这里注意下
public class CompanyHasImgModel implements Serializable {
private String id;
@Excel(name = "公司LOGO", type = 2, width = 40, height = 20)
private String companyLogo;
@Excel(name = "姓名", height = 20, width = 30, isImportField = "true_st")
private String name;
}
执行程序
public static void main(String[] args) throws IOException {
List<CompanyHasImgModel> list = new ArrayList<CompanyHasImgModel>();
list.add( new CompanyHasImgModel( "百度", "D:/壁纸/1.jpg", "北京市海淀区西北旺东路10号院百度科技园1号楼" ) );
list.add( new CompanyHasImgModel( "阿里巴巴", "D:/壁纸/2.jpg", "北京市海淀区西北旺东路10号院百度科技园1号楼" ) );
list.add( new CompanyHasImgModel( "Lemur", "D:/壁纸/3.jpg", "亚马逊热带雨林" ) );
list.add( new CompanyHasImgModel( "一众", "D:/壁纸/4.jpg","山东济宁俺家" ) );
Workbook workbook = ExcelExportUtil.exportExcel( new ExportParams("aaa","s1"), CompanyHasImgModel.class, list );
FileOutputStream fos = new FileOutputStream( "D:/ExcelExportHasImgTest.exportCompanyImg.xls" );
workbook.write( fos );
fos.close();
}
2.字节流式
@Excel(name = "公司LOGO", type = 2 ,width = 40 , height = 20,imageType = 2)
private byte[] companyLogo;
执行程序
public static void main(String[] args) throws IOException {
List<CompanyHasImgModel> list = new ArrayList<CompanyHasImgModel>();
list.add( new CompanyHasImgModel( "百度", InputStream2ByteArray( "D:/壁纸/1.jpg" ), "北京市海淀区西北旺东路10号院百度科技园1号楼" ) );
list.add( new CompanyHasImgModel( "阿里巴巴", InputStream2ByteArray( "D:/壁纸/2.jpg" ), "北京市海淀区西北旺东路10号院百度科技园1号楼" ) );
list.add( new CompanyHasImgModel( "Lemur", InputStream2ByteArray( "D:/壁纸/3.jpg" ), "亚马逊热带雨林" ) );
list.add( new CompanyHasImgModel( "一众", InputStream2ByteArray( "D:/壁纸/4.jpg" ), "山东济宁俺家" ) );
Workbook workbook = ExcelExportUtil.exportExcel( new ExportParams( "aaa", "s1" ), CompanyHasImgModel.class, list );
FileOutputStream fos = new FileOutputStream( "D:/ExcelExportHasImgTest.exportCompanyImg.xls" );
workbook.write( fos );
fos.close();
}
private static byte[] InputStream2ByteArray(String filePath) throws IOException {
InputStream in = new FileInputStream( filePath );
byte[] data = toByteArray( in );
in.close();
return data;
}
private static byte[] toByteArray(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int n = 0;
while ((n = in.read( buffer )) != -1) {
out.write( buffer, 0, n );
}
return out.toByteArray();
}
这样就完成字节数组图片的导入
5.数据导入
有导出就有导入,基于注解的导入导出,配置配置上是一样的,只是方式反过来而已,比如类型的替换
导出的时候是1替换成男,2替换成女,导入的时候则反过来,男变成1 ,女变成2,时间也是类似
导出的时候date被格式化成 2017-8-25 ,导入的时候2017-8-25被格式成date类型
下面说下导入的基本代码,注解啥的都是上面讲过了,这里就不累赘了
public static void main(String[] args) throws IOException {
ImportParams params = new ImportParams();
params.setTitleRows( 1 );
params.setHeadRows( 1 );
long start = new Date().getTime();
List<StudentEntity> list = ExcelImportUtil.importExcel(
new File( "D:/学生统计.xls" ),
StudentEntity.class, params );
System.out.println( new Date().getTime() - start );
System.out.println( list.size() );
System.out.println( ReflectionToStringBuilder.toString( list.get( 0 ) ) );
}
基本是写法也很简单,
ImportParams 参数介绍下
属性 类型 默认值 功能
titleRows int 0 表格标题行数,默认0
headRows int 1 表头行数,默认1
startRows int 0 字段真正值和列标题之间的距离 默认0
keyIndex int 0 主键设置,如何这个cell没有值,就跳过 或者认为这个是list的下面的值 这一列必须有值,不然认为这列为无效数据
startSheetIndex int 0 开始读取的sheet位置,默认为0
sheetNum int 1 上传表格需要读取的sheet 数量,默认为1
needSave boolean false 是否需要保存上传的Excel
needVerfiy boolean false 是否需要校验上传的Excel
saveUrl String "upload/excelUpload" 保存上传的Excel目录,默认是 如 TestEntity这个类保存路径就是 upload/excelUpload/Test/yyyyMMddHHmss* 保存名称上传时间五位随机数
verifyHanlder IExcelVerifyHandler null 校验处理接口,自定义校验
lastOfInvalidRow int 0 最后的无效行数,不读的行数
readRows int 0 手动控制读取的行数
importFields String[] null 导入时校验数据模板,是不是正确的Excel
keyMark String ":" Key-Value 读取标记,以这个为Key,后面一个Cell 为Value,多个改为ArrayList
readSingleCell boolean false 按照Key-Value 规则读取全局扫描Excel,但是跳过List读取范围提升性能 仅仅支持titleRows + headRows + startRows 以及 lastOfInvalidRow
dataHanlder IExcelDataHandler null 数据处理接口,以此为主,replace,format都在这后面
如果要使用项目根路径,可以使用PoiPublicUtil的静态方法获取
new File(PoiPublicUtil.getWebRootPath("import/ExcelExportMsgClient.xlsx"))
Excel导入提示
-
读取指定的sheet 比如要读取上传的第二个sheet 那么需要把startSheetIndex = 1 就可以了
-
读取几个sheet 比如读取前2个sheet,那么 sheetNum=2 就可以了
-
读取第二个到第五个sheet 设置 startSheetIndex = 1 然后sheetNum = 4
-
读取全部的sheet sheetNum 设置大点就可以了
-
保存Excel 设置 needVerfiy = true,默认保存的路径为upload/excelUpload/Test/yyyyMMddHHmss* 保存名称上传时间五位随机数 如果自定义路径 修改下saveUrl 就可以了,同时saveUrl也是图片上传时候的保存的路径
-
判断一个Excel是不是合法的Excel importFields 设置下值,就是表示表头必须至少包含的字段,如果缺一个就是不合法的excel,不导入
6.大数据量导入
大数据导出是当我们的导出数量在几万,到上百万的数据时,一次从数据库查询这么多数据加载到内存然后写入会对我们的内存和CPU都产生压力,这个时候需要我们像分页一样处理导出分段写入Excel缓解Excel的压力
EasyPoi提供的是两个方法
- exportBigExcel
- closeExportBigExcel
添加数据和关闭服务,关闭服务不是必须的,可以调也可以不掉
public static void main(String[] args) throws IOException {
List<StudentEntity> list = new ArrayList<StudentEntity>();
Workbook workbook = null;
Date start = new Date();
ExportParams params = new ExportParams( "大数据测试", "测试" );
for (int i = 0; i < 1000000; i++) { //一百万数据量
StudentEntity client = new StudentEntity();
client.setBirthday( new Date() );
client.setId( "id" + i );
client.setName( "name" + i );
client.setRegistrationDate( new Date() );
list.add( client );
if (list.size() == 10000) {
workbook = ExcelExportUtil.exportBigExcel( params, StudentEntity.class, list );
list.clear();
}
}
ExcelExportUtil.closeExportBigExcel();
System.out.println( new Date().getTime() - start.getTime() );
FileOutputStream fos = new FileOutputStream( "D:/ExcelExportBigData.bigDataExport.xlsx" );
workbook.write( fos );
fos.close();
}
7.spring MVC导出
easypoi view 项目是为了更简单的方便搭建在导出时候的操作,利用spring mvc 的view 封装,更加符合spring mvc的风格 view下面包括多个 view的实现
- EasypoiBigExcelExportView 大数据量导出
- EasypoiMapExcelView map 列表导出
- EasypoiPDFTemplateView pdf导出
- EasypoiSingleExcelView 注解导出
- EasypoiTemplateExcelView 模板导出
- EasypoiTemplateWordView word模板导出
- MapGraphExcelView 图表导出
view的是使用方法大同小异,都有一个对应的bean,里面保护指定的参数常量 同意用modelmap.put(‘常量参数名’,‘值’)就可以,最后返回这个view名字
注解目录扫描的时候加上 cn.afterturn.easypoi.view 就可以使用了
实体上加注解
@RequestMapping(value = "/excel")
public void downloadByPoiBaseView(ModelMap map, HttpServletRequest request,
HttpServletResponse response) {
List<User> list = excelExportServer.list();
ExportParams params = new ExportParams("2412312", "测试", ExcelType.XSSF);
params.setFreezeCol(2);
map.put( NormalExcelConstants.DATA_LIST, list);
map.put(NormalExcelConstants.CLASS, User.class);
map.put(NormalExcelConstants.PARAMS, params);
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
大数据导出View的用法
EasypoiBigExcelExportView 是针对大数据量导出特定的View
在跳转到这个View的时候不需要查询数据,而且这个View自己去查询数据,用户只要实现IExcelExportServer接口就可以了
对应的常量类BigExcelConstants
public interface IExcelExportServer {
/**
* 查询数据接口
* @param obj 查询条件
* @param page 当前页数
* @return
*/
public List<Object> selectListForExcelExport(Object obj, int page);
}
EasypoiBigExcelExportView 判断是否还有下一页的条件是,如果selectListForExcelExport
返回null就认为是最后一页了,如果返回有数据这page+1继续查询
@Service
public class ExcelExportServer implements IExcelExportServer {
@Autowired
private UserMapper userMapper;
static int count=1;
public List<Object> selectListForExcelExport(Object o, int i) {
if (count>0){
count--;
return userMapper.selectByExample( new UserExample() );
}else {
return null;
}
}
}
在我们自己的controller中
@RequestMapping("load")
public void downloadByPoiBaseView(ModelMap map, HttpServletRequest request,
HttpServletResponse response) {
ExportParams params = new ExportParams("2412312", "测试", ExcelType.XSSF);
params.setFreezeCol(2);
map.put(BigExcelConstants.CLASS, MsgClient.class);
map.put(BigExcelConstants.PARAMS, params);
//就是我们的查询参数,会带到接口中,供接口查询使用
map.put(BigExcelConstants.DATA_PARAMS, new HashMap<String,String>());
map.put(BigExcelConstants.DATA_INTER,excelExportServer);
PoiBaseView.render(map, request, response, BigExcelConstants.EASYPOI_BIG_EXCEL_VIEW);
}
我们需要把参数条件封装成map或者其他类型,上面的obj可以把参数自己转回来 参数名字 BigExcelConstants.DATA_PARAM 然后把实现查询的接口注入进来就可以了 map.put(BigExcelConstants.DATA_INTER,excelExportServer); 后面就和其他View一样了。