easypoi功能如同名字easy,主打的功能就是容易,让一个没见接触过poi的人员
就可以方便的写出Excel导出,Excel模板导出,Excel导入,Word模板导出,通过简单的注解和模板
语言(熟悉的表达式语法),完成以前复杂的写法。
具体使用和注解使用等可以查看http://easypoi.mydoc.io/#text_202982
demo源码地址https://github.com/li469791221/easypoidemo
建议去码云下载新版,老版本有部分bug:https://gitee.com/lemur/easypoi
文中版本,已知开启校验错误数据的时候导出有问题,新版本中修改了源数据删除错误数据获取正确的workbook,以及源数据删除正确数据获取错误的workbook的bug。
本demo使用fastdfs资源服务器保存图片,也有使用到httpclient请求网络资源。
1,添加maven依赖。
<!-- easypoi依赖 -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>3.2.0</version>
</dependency>
<!-- fastdfs -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.1-RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.4.1</version>
</dependency>
2,配置application.properties。
fdfs.connect-timeout=600
fdfs.so-timeout=1500
#fastdfs服务器的地址
fdfs.tracker-list[0]=192.168.25.133:22122
fdfs.thumbImage.height=150
fdfs.thumbImage.width=150
IMAGE_SERVER_URL=http://192.168.25.133/
spring.jmx.enabled=false
server.port=8080
server.servlet.path=/
3,编写一个excel实体类。
public class Member {
@Excel(name = "用户id") //表明这个字段要导出到excel或从excel导入
private Long id;
@Excel(name = "用户姓名")
private String name;
@Excel(name = "生日", format = "yyyy-MM-dd HH:mm:ss")
//format指定字段导入导出的日期格式化类型
private Date birthday;
@Excel(name = "性别", replace = {"男_0", "女_1"}) //replace使用的时候直接"实际含义_数据库实际内容"即可,导入导出均适用
private String sex;
@Excel(name = "用户年龄")
private String age;
@Excel(name = "电话", width = 16)
private String phone;
@Excel(name = "用户账号", width = 16)
private String loginName;
@Excel(name = "用户头像", width = 32, height = 32, type = 2)
//type等于2表示导出的类型为图片。导入的也是一样。
private String pic;
public Member() {}
public Member(Long id, String name, String sex, Date birthday, String age, String phone, String loginName, String pic) {
this.id = id;
this.name = name;
this.sex = sex;
this.birthday = birthday;
this.age = age;
this.phone = phone;
this.loginName = loginName;
this.pic = pic;
}//getset省略
}
4,准备一个fastdfs服务器服务(资源服务器根据自己需要设定。也可以不要,看自己的图片准备保存到哪里)。
@Service
public class ExcelService {
@Autowired
private FastFileStorageClient storageClient;
@Value("${IMAGE_SERVER_URL}")
private String url;
/**
* 上传资源到fastdfs资源服务器
* @param file
* @return
* @throws IOException
*/
public String uploadFile(MultipartFile file) throws IOException {
StorePath storePath = storageClient.uploadFile(file.getInputStream()
, file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()), null);
return url + storePath.getFullPath();
}
}
这样准备工作就做好了。更多的标签介绍可以去上面提供的网址查看,也可以点进@Excel查看这个里面每个属性对应的注释。
@RestController
public class ExcelController {
@Autowired
private ExcelService excelService;
//准备一些图片以供测试
public static final String[] imgs = new String[]{
// "http://192.168.25.133/group1/M00/00/01/wKgZhVvC78SAfpWaAAAsAp7EzlE763.jpg",
"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=108228188,2741176027&fm=27&gp=0.jpg",
"http://192.168.25.133/group1/M00/00/01/wKgZhVvC8OCAJRuiAABeY6xwxBQ960.jpg",
"http://192.168.25.133/group1/M00/00/01/wKgZhVvC8PGAIo8LAABP3bR1ZnU861.jpg",
"http://192.168.25.133/group1/M00/00/01/wKgZhVvC8P6AFx5KAAA22wMuH2A112.jpg"
};
/**
* 上传文件到fastdfs
* @param file
* @return
* @throws IOException
*/
@PostMapping("/uploadFile")
public String uploadFile(MultipartFile file) throws IOException {
String path = excelService.uploadFile(file);
return path;
}
/**
* 下载excel
*/
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
List<Member> list = new ArrayList<>();
for (int i = 0; i < 4; i++) {
list.add(new Member((long)i, "张三" + i, i % 2 + "", new Date(), 20 + i + "",
"15219873928", "123456" + i, imgs[i]));
}
//ExportParams可以通过构造的两个参数设置导出的sheet名字和excel的title列名
ExportParams params = new ExportParams();
Workbook workbook = ExcelExportUtil.exportExcel(params, Member.class, list);
// 告诉浏览器用什么软件可以打开此文件
response.setHeader("content-Type", "application/vnd.ms-excel");
// 下载文件的默认名称
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("用户数据表","UTF-8") + ".xls");
//编码
response.setCharacterEncoding("UTF-8");
workbook.write(response.getOutputStream());
}
}
直接在浏览器上面输入http://localhost:8080/export下载excel。
1,导出图片的时候报错:
java.lang.ArrayIndexOutOfBoundsException: 0
at cn.afterturn.easypoi.util.PoiPublicUtil.getFileExtendName(PoiPublicUtil.java:147)
问题描述是导出图片的时候下标越界。让我们debug来看下问题原因
可以看到是从ImageCache中获取的data为空造成。
继续到ImageCache中查找原因。
可以看到是在ImageIO.write()我们图片url的时候得到了一个空的字节数组。
那么问题出来了,这里为什么返回了一个空的字节数组。从write方法的三个参数中的第二个参数我们可以看到这里将图片路径进行了字符串截取,截取的是第一个点以后的字符串,显然这是在截取图片的后缀名。
因为我们使用的图片路径为http://192.168.25.133/group1/M00/00/01/wKgZhVvC78SAfpWaAAAsAp7EzlE763.jpg。随意第一个点之后的字符串显然有问题,所以这里应该截取最后一个点后面的字符串。
发现了问题,那么我们就需要解决问题:
Easypoi的ImageCache为我们提供了setLoadingCache()方法,使得我们可以设置自己想要的loadingCache,所以,我们只需要自己重写一个loadingCache并赋值给ImageCache。
我们可以在Spring容器启动后把这个自定义的loadingCache赋值给ImageCache。
只需要修改这个截取的地方即可。
在导出图片的时候,部分图片显示不出来。
这个问题的原因肯定就要从导出图片的时候获取图片的流信息上面查找。
从上面的问题我们发现图片的流是在loadingCache里面通过POICacheManager.getFile()获取的,所以我们需要到这个类里面去找到获取图片流的地方。
这里显示这个fileLoader是默认实现,通过这个类里面的setFileLoader()我们可以知道。我们可以重写这个fileLoader。
从这里我们看到easypoi是使用URLConnection去获取网络图片流的。
那么问题肯定就是URLConnection获取图片流的时候出了问题。
所以我换成了httpclient来获取图片,可能还会有其他问题,但是我这边目前没有发现问题。
具体解决方法:自己写一个fileloader实现IFileLoader接口,我们只需要重写上面获取网络图片的这一块地方,然后赋值给POICacheManager。
简单导入只需要一行代码。
/**
* 简单导入
* @param file
* @throws Exception
*/
@PostMapping("/easyImport")
public void easyImport(MultipartFile file) throws Exception {
List<Member> list = ExcelImportUtil.importExcel(file.getInputStream(), Member.class, new ImportParams());
for (Member member : list) {
System.out.println(member);
}
//Member{id=0, name='张三0', birthday=Sun Oct 14 16:22:16 CST 2018, sex='0', age='20', phone='15219873928', loginName='1234560', pic='/excel/upload/img\Member\pic3571587826.JPG'}
//Member{id=1, name='张三1', birthday=Sun Oct 14 16:22:16 CST 2018, sex='1', age='21', phone='15219873928', loginName='1234561', pic='/excel/upload/img\Member\pic20436614652.JPG'}
//Member{id=2, name='张三2', birthday=Sun Oct 14 16:22:16 CST 2018, sex='0', age='22', phone='15219873928', loginName='1234562', pic='/excel/upload/img\Member\pic88937536202.JPG'}
//Member{id=3, name='张三3', birthday=Sun Oct 14 16:22:16 CST 2018, sex='1', age='23', phone='15219873928', loginName='1234563', pic='/excel/upload/img\Member\pic57721772929.JPG'}
}
结果如上面的注释,可以看到图片是保存到了项目目录下面的。这个可以通过@Excel里面的savePath属性改写保存到项目下的哪个目录。
查看easypoi源码。导入的ExcelImportUtil.importExcel()方法里面使用的是new ExcelImportService().importExcelByIs(inputstream, pojoClass, params, false).getList();
发现上传图片的逻辑在ExcelImportService.saveImage()方法里:
发现只有两种保存图片方式:一种保存到服务器目录下,一种保存到数据库,这也对应了@Excel里面的注释描述。
解决方法:自己重写ExcellImportService.java(实际上是重写他的saveImage里面保存图片的方法)。
然后直接使用这个自定义的ExcellImportService进行导入,不能在使用之前easypoi提供的导入入口。
结果也在上面的注释里面,可以看到pic的路径已经是我们fastdfs资源服务器里面的资源的路径了。
我们在导入的时候经常需要做数据校验。
easypoi可以帮助我们完成这件事情,我们只需要在excel实体类上面添加校验规则和校验失败的提示信息即可,同时也可以自定义校验规则
1.添加校验规则注解。
public class Member implements IExcelModel {
@Excel(name = "用户id")
@NotNull //基本数据类型使用,都是javax.validation.constraints包里的
private Long id;
@Excel(name = "用户姓名")
@NotEmpty //字符串类型使用
private String name;
@Excel(name = "生日", format = "yyyy-MM-dd HH:mm:ss")
private Date birthday;
@Excel(name = "性别", replace = {"男_0", "女_1"}) //replace使用的时候直接"实际含义_数据库实际内容"即可,导入导出均适用
@Pattern(regexp = "^[0-1]$", message = "性别输入有误") //字符串类型使用
private String sex;
@Excel(name = "用户年龄")
@Pattern(regexp = "^\\d{1,3}$", message = "年龄输入有误")
private String age;
@Excel(name = "电话", width = 16)
@Pattern(regexp = "^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$", message = "电话输入有误")
private String phone;
@Excel(name = "用户账号", width = 16)
private String loginName;
@Excel(name = "用户头像", width = 32, height = 32, type = 2)
private String pic;
public Member() {}
public Member(Long id, String name, String sex, Date birthday, String age, String phone, String loginName, String pic) {
this.id = id;
this.name = name;
this.sex = sex;
this.birthday = birthday;
this.age = age;
this.phone = phone;
this.loginName = loginName;
this.pic = pic;
}
}
添加自定义校验规则需要继承IExcelVerifyHandler,然后给importParam设置这个校验器
/**
* 自定义的导入校验器
*/
public class MyVerifyHandler implements IExcelVerifyHandler<Member> {
@Override
public ExcelVerifyHandlerResult verifyHandler(Member member) {
ExcelVerifyHandlerResult result = new ExcelVerifyHandlerResult();
//假设我们要添加用户,
//现在去数据库查询loginName,如果存在则表示校验不通过。
//假设现在数据库中有个loginName 1234560
if ("1234560".equals(member.getLoginName())) {
result.setMsg("该用户已存在");
result.setSuccess(false);
return result;
}
result.setSuccess(true);
return result;
}
}
ImportParams params = new ImportParams();
params.setNeedVerfiy(true);
private String errorMsg; //自定义一个errorMsg接受下面重写IExcelModel接口的get和setErrorMsg方法。
@Override
public String getErrorMsg() {
return errorMsg;
}
@Override
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
**
* excel导入错误信息实体类
*/
public class MemberFailed extends Member {
@Excel(name = "错误信息")
private String errorMsg;
@Override
public String getErrorMsg() {
return errorMsg;
}
@Override
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public static MemberFailed member2MemberFailed(Member member) {
MemberFailed failed = new MemberFailed();
failed.setErrorMsg(member.getErrorMsg());
failed.setAge(member.getAge());
failed.setBirthday(member.getBirthday());
failed.setId(member.getId());
failed.setLoginName(member.getLoginName());
failed.setName(member.getName());
failed.setPhone(member.getPhone());
failed.setPic(member.getPic());
failed.setSex(member.getSex());
return failed;
}
public static List<MemberFailed> members2MemberFaileds(List<Member> members) {
List<MemberFailed> list = new ArrayList<>();
for (Member member : members) {
list.add(member2MemberFailed(member));
}
return list;
}
}
@PostMapping("/complexImport")
public void complexImport(MultipartFile file, HttpServletResponse response) throws Exception {
ImportParams params = new ImportParams();
params.setNeedVerfiy(true);
params.setVerifyHandler(new MyVerifyHandler());
ExcelImportResult importResult = new ExcelImportService().importExcelByIs(file.getInputStream(),
Member.class, params, true);
//getList()里面的就是所有校验成功的excel数据
List<Member> list = importResult.getList();
if (list != null) {
for (Member member : list) {
System.out.println(member);
}
}
System.out.println("-----------------------");
//getFailWorkbook()和getFailList()里面的就是所有校验失败的excel数据
Workbook failWorkbook = importResult.getFailWorkbook();
List<Member> failList = importResult.getFailList();
List<MemberFailed> faileds = MemberFailed.members2MemberFaileds(failList);
// response.setHeader("content-Type", "application/vnd.ms-excel");
// response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("用户数据表","UTF-8") + ".xls");
// response.setCharacterEncoding("UTF-8");
// failWorkbook.write(response.getOutputStream());
//将错误excel信息返回给客户端
ExportParams exportParams = new ExportParams();
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, MemberFailed.class, faileds);
response.setHeader("content-Type", "application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("用户数据表","UTF-8") + ".xls");
response.setCharacterEncoding("UTF-8");
workbook.write(response.getOutputStream());
}
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
List<Member> list = new ArrayList<>();
for (int i = 0; i < 4; i++) {
list.add(new Member((long)i, "张三" + i, i % 2 + "", new Date(), 20 + i + "",
"15219873928", "123456" + i, imgs[i]));
}
//ExportParams params = new ExportParams();
//导出的时候指明标题列名和sheet名字
ExportParams params = new ExportParams("用户数据", "用户数据");
Workbook workbook = ExcelExportUtil.exportExcel(params, Member.class, list);
// 告诉浏览器用什么软件可以打开此文件
response.setHeader("content-Type", "application/vnd.ms-excel");
// 下载文件的默认名称
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("用户数据表","UTF-8") + ".xls");
//编码
response.setCharacterEncoding("UTF-8");
workbook.write(response.getOutputStream());
}
如果按照之前那么导入会获取不到数据或者报错。
原因:这里多了一个标题列,导出的时候还是从第一列开始读取,结果就会出问题。我们因该从第二列开始读取。
解决:指定标题列params.setTitleRows(1);
@PostMapping("/complexImport")
public void complexImport(MultipartFile file, HttpServletResponse response) throws Exception {
ImportParams params = new ImportParams();
//在有标题列的时候,需要指明标题列在1,默认是0
params.setTitleRows(1);
params.setNeedVerfiy(true);
params.setVerifyHandler(new MyVerifyHandler());
ExcelImportResult importResult = new ExcelImportService().importExcelByIs(file.getInputStream(),
Member.class, params, true);
List<Member> list = importResult.getList();
if (list != null) {
for (Member member : list) {
System.out.println(member);
}
}
System.out.println("-----------------------");
Workbook failWorkbook = importResult.getFailWorkbook();
List<Member> failList = importResult.getFailList();
List<MemberFailed> faileds = MemberFailed.members2MemberFaileds(failList);
// response.setHeader("content-Type", "application/vnd.ms-excel");
// response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("用户数据表","UTF-8") + ".xls");
// response.setCharacterEncoding("UTF-8");
// failWorkbook.write(response.getOutputStream());
ExportParams exportParams = new ExportParams();
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, MemberFailed.class, faileds);
// 告诉浏览器用什么软件可以打开此文件
response.setHeader("content-Type", "application/vnd.ms-excel");
// 下载文件的默认名称
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("用户数据表","UTF-8") + ".xls");
//编码
response.setCharacterEncoding("UTF-8");
workbook.write(response.getOutputStream());
}