前言
关于导出 Excel 文件,可以说是大多数服务中都需要集成的功能。那么,要如何优雅快速地(偷懒地)去实现这个功能呢?
你可能第一想法是:这还不简单?用 Apache 开源框架 poi, 或者 jxl 都可以实现啊。面向百度编程,把代码模板 copy 下来,根据自己的业务再改改,能有多难?
这样你写了一堆代码很臃肿,建议使用阿里巴巴的easyExcel,高效快速能实现你的功能
Apache poi、jxl 的缺陷
在说如何实现之前,我们先来讨论一下传统 Excel 框架的不足!除了上面说的,Apache poi、jxl 都存在生成 excel 文件不够简单优雅快速外,它们都还存在一个严重的问题,那就是非常耗内存,严重时会导致内存溢出。
POI 虽然目前来说,是 excel 解析框架中被使用最广泛的,但这个框架并不完美。
为什么这么说呢?
开发者们大部分使用 POI,都是使用其 userModel 模式。而 userModel 的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,但是还是可控的。
然而 userModel 模式最大的问题是在于,对内存消耗非常大,一个几兆的文件解析甚至要用掉上百兆的内存。现实情况是,很多应用现在都在采用这种模式,之所以还正常在跑是因为并发不大,并发上来后,一定会OOM或者频繁的 full gc。
阿里出品的 EasyExcel
官方对其的简介是:
快速、简单避免OOM的java处理Excel工具!
EasyExcel 解决了什么
主要来说,有以下几点:
传统 Excel 框架,如 Apache poi、jxl 都存在内存溢出的问题;
传统 excel 开源框架使用复杂、繁琐;
EasyExcel 底层还是使用了 poi, 但是做了很多优化,如修复了并发情况下的一些 bug, 具体修复细节,可阅读官方文档https://github.com/alibaba/easyexcel;
easyPOI
插入图片
jar包
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.8</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.8</version>
</dependency>
<!--返回视图层模板,没有的话不行-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置yml文件,使用访问能跳转到该静态资源
静态html内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<form action="/springPoiDemo/upFile" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" title="提交" />
</form>
</body>
</html>
java代码
控制层
package com.jay.easyPoi;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @version 0.0.1
* @program: spring-poi-demo
* @description:
* 参考:https://blog.youkuaiyun.com/qq_16513911/article/details/90666265
* @author: huangzq
* @create: 2021-01-15 09:45
*/
@Controller
public class EasyPOIController {
/**
* 先请求访问该页面,在上传图片,然后调用下面接口进行下载
*
* @return
*/
@RequestMapping("/index")
public String index(){
return "index";
}
@Autowired
HttpServletRequest request;
/**
* 上传图片文件测试生成excel表格可以带图形式 postmane不好测试,下载到客户端postman做不到,采用网页版的
* http://localhost:8066/springPoiDemo/index
* @param file
* @param response
* @throws IOException
*/
@PostMapping("/upFile")
@ResponseBody
public void upFile(@RequestParam("file") MultipartFile file, HttpServletResponse response){
//文件名称
String filename = file.getOriginalFilename();
//文件后缀(后缀检查略过)
String prefix= filename.substring(filename.lastIndexOf("."));
File newFile = null;
try {
newFile = File.createTempFile(System.currentTimeMillis()+"",prefix);
file.transferTo(newFile);
} catch (IOException e) {
e.printStackTrace();
}
//转码BASE64可以存储数据库
//OSS
String imageToBase64 = ImageTools.ImageToBase64(newFile.getPath());
//String filepath = request.getSession().getServletContext().getRealPath("/") + "upload" + filename;
ImageTools.Base64ToImage(imageToBase64,newFile.getPath());
BufferedImage bufferImg ;//图片一
try {
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
//读图片
bufferImg = ImageIO.read(newFile);
ImageIO.write(bufferImg, "png", byteArrayOut);
// 创建一个工作薄
HSSFWorkbook wb = new HSSFWorkbook();
//创建一个sheet
HSSFSheet sheet = wb.createSheet("sheet1");
HSSFPatriarch patriarch = sheet.createDrawingPatriarch();
HSSFRow row = sheet.createRow(0);
HSSFCellStyle style = wb.createCellStyle();
//设置水平对齐的样式为居中对齐;
style.setAlignment(HorizontalAlignment.CENTER);
//设置垂直对齐的样式为居中对齐;
style.setVerticalAlignment(VerticalAlignment.CENTER);
String[] excelHeader = { "序号", "名字", "备注","图片"};
for (int i = 0; i < excelHeader.length; i++) {
HSSFCell cell = row.createCell(i);
cell.setCellValue(excelHeader[i]);
cell.setCellStyle(style);
sheet.autoSizeColumn(i);
//设置列宽
sheet.setColumnWidth(i, 256*30+184);
// sheet.SetColumnWidth(i, 100 * 256);
}
List<User> list = createModeList();
for (int i = 0; i < list.size(); i++) {
row = sheet.createRow(i + 1);
row.setHeight((short) (35.7*30));
User user = list.get(i);
//设置样式 每列都是水平垂直居中
HSSFCell cell = row.createCell(0);
cell.setCellValue(user.getId());
cell.setCellStyle(style);
HSSFCell cell1 = row.createCell(1);
cell1.setCellValue(user.getName());
cell1.setCellStyle(style);
HSSFCell cell2 = row.createCell(2);
cell2.setCellValue(user.getShortname());
cell2.setCellStyle(style);
/**
* 该构造函数有8个参数
* 前四个参数是控制图片在单元格的位置,分别是图片距离单元格left,top,right,bottom的像素距离
* 后四个参数,前两个表示图片左上角所在的cellNum和 rowNum,后天个参数对应的表示图片右下角所在的cellNum和 rowNum,
* excel中的cellNum和rowNum的index都是从0开始的
*/
HSSFClientAnchor anchor = new HSSFClientAnchor(0, 0, 0, 0,
(short) 3, (i + 1), (short) 4, (i+2));
//解决Excel无法在筛选时携带图片跟着筛选,毕竟图片都"飘"在了单元格上面
//ClientAnchor有三个值,我分别试了试根据效果添加注释
// int MOVE_AND_RESIZE = 0;//跟随单元格扩大或者缩小,就是你拖动单元格的时候,图片大小也在变
// int MOVE_DONT_RESIZE = 2;//图片固定在该单元格在左上角,并且随着单元格移动
// int DONT_MOVE_AND_RESIZE = 3;//固定在Excel某个位置,像牛皮广告一样不会动
// anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);//0 可以拖动,拖出那个单元格,改变单元格大小,图片大小不会改变,需要特地修改拖的拖拽框才能改变
// anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_DO_RESIZE);//1 同上
// anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_DONT_RESIZE);//2
anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE);//3
// 插入图片
//导出Excel中的图片比例缩放问题?
//解决:在这行插入图片最后加上resize,里面double类型的缩放比例倍数,我设置的是1,占据满了一个单元格,设置0.5就长宽各占据单元格的一半
//当然加了这个左上角可以固定你想要的单元格,右下角那边就不算了
patriarch.createPicture(anchor, wb.addPicture(byteArrayOut
.toByteArray(), HSSFWorkbook.PICTURE_TYPE_JPEG)).resize(1);
}
response.setContentType("application/vnd.ms-excel");
//设置文件名称
response.setHeader("Content-disposition", "attachment;filename=export.xls");
OutputStream outputStream = response.getOutputStream();
wb.write(outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
//try catch这些关闭流略
}
private List<User> createModeList(){
List<User> infArray = new ArrayList();
for(int i=0;i<20;i++) {
infArray.add(new User(i,i+"姓名","name"+i+i+i));
}
return infArray;
}
}
图片工具类
package com.jay.easyPoi;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.io.*;
/**
* @version 0.0.1
* @program: spring-poi-demo
* @description:
* @author: huangzq
* @create: 2021-01-15 09:50
*/
public class ImageTools {
public static String ImageToBase64(String imgPath) {
InputStream in=null;
byte[] data=null;
try{
in = new FileInputStream(imgPath);
data = new byte[in.available()];
in.read(data);
in.close();
}catch (IOException e){
e.printStackTrace();
}
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);
}
public static boolean Base64ToImage(String base64, String imgFilePath) {
// 对字节数组字符串进行Base64解码并生成图片
if (base64 == null) {
// 图像数据为空
return false;
}
BASE64Decoder decoder = new BASE64Decoder();
try {
// Base64解码
byte[] b = decoder.decodeBuffer(base64);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {// 调整异常数据
b[i] += 256;
}
}
OutputStream out = new FileOutputStream(imgFilePath);
out.write(b);
out.flush();
out.close();
return true;
} catch (Exception e) {
return false;
}
}
}
实体类行
package com.jay.easyPoi;
import lombok.Data;
/**
* @version 0.0.1
* @program: spring-poi-demo
* @description:
* @author: huangzq
* @create: 2021-01-15 09:45
*/
@Data
public class User {
private Integer id;
private String name;
private String more;
private String shortname;
public User(Integer id, String name, String more) {
this.id= id;
this.name= name;
this.more = more;
}
}
启动项目,就按照我的控制层上的注释说明操作就行了,下载后显示结果
easyExel
插入了图片
不同实体类方式导出图片
控制层
package com.jay.easyexcel.sampleWrite;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.util.FileUtils;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* @version 0.0.1
* @program: spring-poi-demo
* @description: 简单的导出
* @author: huangzq
* @create: 2021-01-14 15:33
*/
@RestController
@Slf4j
@RequestMapping("/pic")
public class PicController {
/**
* 网络地址访问:http://localhost:8066/springPoiDemo/pic/write
* 生成一个excel文件,没有本地存放临时的,直接返回响应流给客户端
* 有一个问题,导出的图片漂浮的指定的单元格上,你打开过后,放大缩小,图片还是在那个位置,这个是不行的
*
* <p>
* 这个链接是导出到服务器上,没有给客户端
* https://www.cnblogs.com/bingyang-py/p/12461944.html
*
* @param response
*/
@GetMapping("/write")
public void test1(HttpServletResponse response) {
//准备数据
List<Teacher> teachers = new ArrayList<>();
//以前的
// teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
// teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
// teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
// teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
//现在的
teachers.add(new Teacher(1, "hhh", "C:\\Users\\shinelon\\Pictures\\标记破损\\00_00_05_24.jpg", 1));
teachers.add(new Teacher(1, "hhh", "C:\\Users\\shinelon\\Pictures\\标记破损\\00_00_05_24.jpg", 1));
teachers.add(new Teacher(1, "hhh", "C:\\Users\\shinelon\\Pictures\\标记破损\\00_00_05_24.jpg", 1));
teachers.add(new Teacher(1, "hhh", "C:\\Users\\shinelon\\Pictures\\标记破损\\00_00_05_24.jpg", 1));
//不要带后缀,本地导出你带后缀,我这个是线上导出到客户端,那个输出流指定了格式
String fileName = "hhhh";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
// EasyExcel.write(fileName, Teacher.class).sheet("模板").doWrite(teachers);
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(getOutputStream(fileName, response)).build();
//导出某个sheet,指定sheet名:
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).build();
//指定sheet中的每个表(Table)的表头以及导出对应的实体类,序号0,1分别表示第几张表,head为指定表头以及表导出对应的实体类:
WriteTable writeTable2 = EasyExcel.writerTable(1).head(Teacher.class).needHead(true).build();
excelWriter.write(teachers, writeSheet, writeTable2);
} catch (Exception e) {
log.info("导出表失败:", e);
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
}
/**
* 直接通过Response输出流写文件,浏览器表现为下载文件
*
* @param fileName
* @param response
* @return
*/
public OutputStream getOutputStream(String fileName, HttpServletResponse response) {
OutputStream out = null;
try {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes("utf-8"), "ISO8859-1") + ".xls");
out = response.getOutputStream();
//向out中写入流
out.flush();
response.flushBuffer();
} catch (IOException e) {
e.printStackTrace();
}
return out;
}
/**
* 图片也是悬浮的,待优化
*
* 客户端网路访问路径:http://localhost:8066/springPoiDemo/pic/picWrite
*
* 参考:https://alibaba-easyexcel.github.io/quickstart/write.html#%E5%9B%BE%E7%89%87%E5%AF%BC%E5%87%BA
* @throws Exception
*/
@GetMapping("/picWrite")
public void imageWrite(HttpServletResponse response) throws Exception {
String fileName = "多种格式图片excel";
// 如果使用流 记得关闭
InputStream inputStream = null;
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(getOutputStream(fileName, response)).build();
List<ImageData> list = new ArrayList<ImageData>();
ImageData imageData = new ImageData();
list.add(imageData);
String imagePath = "C:\\Users\\shinelon\\Pictures\\Camera Roll\\微信图片_20210114192857.png";
// 放入五种类型的图片 实际使用只要选一种即可
imageData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
imageData.setFile(new File(imagePath));
imageData.setString(imagePath);
inputStream = FileUtils.openInputStream(new File(imagePath));
// imageData.setInputStream(inputStream);
imageData.setUrl(new URL("https://huangzouqiang-huangzouqiang.oss-cn-beijing.aliyuncs.com/api/2021-01-12/thum/2e8670fdc4d24fe59d2fed9a3181c7cc.jpg"));
//导出某个sheet,指定sheet名:
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).build();
//指定sheet中的每个表(Table)的表头以及导出对应的实体类,序号0,1分别表示第几张表,head为指定表头以及表导出对应的实体类:
WriteTable writeTable2 = EasyExcel.writerTable(1).head(Teacher.class).needHead(true).build();
excelWriter.write(ImmutableList.of(imageData), writeSheet, writeTable2);
} finally {
if (inputStream != null) {
inputStream.close();
}
if (excelWriter != null) {
excelWriter.finish();
}
}
}
}
实体类1
package com.jay.easyexcel.sampleWrite;
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.converters.string.StringImageConverter;
import lombok.Data;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
/**
* @version 0.0.1
* @program: spring-poi-demo
* @description:
* @author: huangzq
* @create: 2021-01-14 19:28
*/
@Data
@ContentRowHeight(100)
@ColumnWidth(100 / 8)
public class ImageData {
private File file;
private InputStream inputStream;
/**
* 如果string类型 必须指定转换器,string默认转换成string
*/
@ExcelProperty(converter = StringImageConverter.class)
private String string;
private byte[] byteArray;
/**
* 根据url导出
*
* @since 2.1.1
*/
private URL url;
}
实体类2
package com.jay.easyexcel.sampleWrite;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.converters.string.StringImageConverter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* @version 0.0.1
* @program: spring-poi-demo
* @description:
*
* EasyExcel也是注解式开发,常用注解如下:
*
* ExcelProperty 指定当前字段对应excel中的那一列
* ExcelIgnore 默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
* DateTimeFormat 日期转换,用String去接收excel日期格式的数据会调用这个注解。里面的value参照java.text.SimpleDateFormat
* NumberFormat 数字转换,用String去接收excel数字格式的数据会调用这个注解。里面的value参照java.text.DecimalFormat
*
* @author: huangzq
* @create: 2021-01-14 15:33
*/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
@ExcelIgnore
private Integer teacherId;
@ExcelProperty("老师名字")
private String name;
@ExcelProperty(value = "图片",converter = StringImageConverter.class)
private String image;
@ExcelProperty(value = "状态")
private Integer status;
}
说明:easyExcel关于图片的说明比较少,官方我也看了,有 但是不多,这里也会有些纰漏的,图片是悬浮那个位置的,设置参数来定位目前还没完善,后期在补充,如果需要导出带图片的话,还是建议使用poi的,出了问题,网上能找到方法去解决的
参考:https://mp.weixin.qq.com/s/TZYxyzt_FpXcWuJpxz_IZQ
https://blog.youkuaiyun.com/qq_16513911/article/details/90666265