前言

关于导出 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.csdn.net/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.csdn.net/qq_16513911/article/details/90666265

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐