EasyExcel 实战:导出带图片的 Excel 完整方案

Luca Ju
2025-11-21 / 0 评论 / 17 阅读 / 正在检测是否收录...

在实际开发中,可能会遇到导出 Excel 时需要包含图片的场景。EasyExcel 作为阿里开源的高效 Excel 处理工具,虽然原生支持图片导出,但在图片格式适配、单元格样式调整等细节上需要自定义扩展。本文将详细讲解如何基于 EasyExcel 实现带图片的 Excel 导出,包含完整代码示例和关键细节说明。

一、最终效果展示

导出的 Excel 中,图片将自适应单元格大小,无图片时显示"无图"提示,整体格式整洁规范:
image-20251121103832192

二、实现步骤详解

1. 引入依赖

首先在 pom.xml 中添加 EasyExcel 核心依赖(本文使用 4.0.3 版本,适配大部分场景):

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>4.0.3</version>
</dependency>

2. 定义导出实体类

创建 RecordExcelVO 作为 Excel 导出的数据载体,核心关注图片字段的配置:

package com.retail.ocr.model.vo;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.retail.ocr.excel.converter.ImageConverter;
import lombok.Data;

/**
 * Excel 导出实体(识别记录)
 *
 * @author jucunqi
 * @since 2025/11/20
 */
@Data
public class RecordExcelVO {

    @ColumnWidth(20)  // 列宽设置
    @ExcelProperty("手机号")  // Excel 表头名称
    private String phoneNumber;

    @ColumnWidth(25)
    @ExcelProperty("识别时间")
    private String createTime;

    @ColumnWidth(20)
    @ExcelProperty("识别读数")
    private String number1;

    @ColumnWidth(20)
    @ExcelProperty("识别条码")
    private String number2;

    // 图片字段:使用自定义转换器处理 byte[] 类型
    @ExcelProperty(value = "识别照片", converter = ImageConverter.class)
    private byte[] imageBytes;

    @ColumnWidth(20)
    @ExcelProperty("标记内容")
    private String markDesc;

    @ColumnWidth(20)
    @ExcelProperty("备注")
    private String remark;
}

关键说明:

  • 图片字段类型为 byte[]:便于存储图片二进制数据(可从文件、数据库 BLOB 字段、网络图片转换获取)。
  • @ExcelProperty(converter = ImageConverter.class):指定自定义转换器,将 byte[] 转换为 Excel 可识别的图片格式。
  • @ColumnWidth:统一设置列宽,优化 Excel 显示效果。

3. 实现图片转换器(核心)

自定义 ImageConverter 实现 Converter<byte[]> 接口,负责将图片二进制数据转换为 EasyExcel 支持的 ImageData 格式:

package com.retail.ocr.excel.converter;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ImageData;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.ListUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.util.List;

@Slf4j
public class ImageConverter implements Converter<byte[]> {

    @Override
    public Class<?> supportJavaTypeKey() {
        return List.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.EMPTY;
    }

    @Override
    public byte[] convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }

    @Override
    public WriteCellData<?> convertToExcelData(byte[] value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        // 这里进行对数据实体类的byte[]进行处理
        List<ImageData> data = ListUtils.newArrayList();
        ImageData imageData;
        try {
            imageData = new ImageData();
            imageData.setImage(value);
            data.add(imageData);
        } catch (Exception e) {
            log.error("导出临时记录图片异常:", e);
        }
        WriteCellData<?> cellData = new WriteCellData<>();
        if (!CollectionUtils.isEmpty(data)) {
            // 图片返回图片列表
            cellData.setImageDataList(data);
            cellData.setType(CellDataTypeEnum.EMPTY);
        } else {
            // 没有图片使用汉字表示
            cellData.setStringValue("无图");
            cellData.setType(CellDataTypeEnum.STRING);
        }
        return cellData;
    }
}

核心逻辑:

  • 校验图片二进制数据有效性,避免空指针异常。
  • 有图片时:封装 ImageData 列表,指定 Excel 单元格类型为 IMAGE
  • 无图片时:设置文本"无图",单元格类型为 STRING,提升用户体验。

4. 自定义图片写入处理器

由于 EasyExcel 原生图片导出不支持自适应单元格、多图片排列等需求,需自定义 CellWriteHandler 处理图片的位置、大小和样式:

package com.retail.ocr.excel.handler;

import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.ImageData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFPicture;
import org.apache.poi.xssf.usermodel.XSSFShape;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;


public class ImageWriteHandler implements CellWriteHandler {
    /**
     * 已经处理的Cell
     */
    private final CopyOnWriteArrayList<String> REPEATS = new CopyOnWriteArrayList<>();
    /**
     * 单元格的图片最大张数(每列的单元格图片张数不确定,单元格宽度需按照张数最多的长度来设置)
     */
    private final AtomicReference<Integer> MAX_IMAGE_SIZE = new AtomicReference<>(0);

    /**
     * 标记手动添加的图片,用于排除EasyExcel自动添加的图片
     */
    private final CopyOnWriteArrayList<Integer> CREATE_PIC_INDEX = new CopyOnWriteArrayList<>();

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        //  在 数据转换成功后 不是头就把类型设置成空
        if (isHead) {
            return;
        }
        // 将要插入图片的单元格的type设置为空,下面再填充图片
        if (CollectionUtils.isNotEmpty(cellData.getImageDataList())) {
            cellData.setType(CellDataTypeEnum.EMPTY);
        }
    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        //  在 单元格写入完毕后 ,自己填充图片
        if (isHead || CollectionUtils.isEmpty(cellDataList)) {
            return;
        }
        boolean listFlag = false;
        Sheet sheet = cell.getSheet();
        // 此处为ExcelUrlConverterUtil的返回值
        List<ImageData> imageDataList = cellDataList.get(0).getImageDataList();
        if (CollectionUtils.isNotEmpty(imageDataList)) {
            listFlag = true;
        }
        if (!listFlag && imageDataList == null) {
            return;
        }
        String key = cell.getRowIndex() + "_" + cell.getColumnIndex();
        if (REPEATS.contains(key)) {
            return;
        }
        REPEATS.add(key);
        if (imageDataList.size() > MAX_IMAGE_SIZE.get()) {
            MAX_IMAGE_SIZE.set(imageDataList.size());
        }
        // 默认要导出的图片大小为60*60px,60px的行高大约是900,60px列宽大概是248*8
        sheet.getRow(cell.getRowIndex()).setHeight((short) 900);
        sheet.setColumnWidth(cell.getColumnIndex(), listFlag ? 240 * 8 * MAX_IMAGE_SIZE.get() : 240 * 8);

        if (listFlag) {
            for (int i = 0; i < imageDataList.size(); i++) {
                ImageData imageData = imageDataList.get(i);
                if (imageData == null) {
                    continue;
                }
                byte[] image = imageData.getImage();
                this.insertImage(sheet, cell, image, i);
            }
        } else {
            this.insertImage(sheet, cell, imageDataList.get(0).getImage(), 0);
        }

        // 清除EasyExcel自动添加的没有格式的图片
        XSSFDrawing drawingPatriarch = (XSSFDrawing) sheet.getDrawingPatriarch();
        List<XSSFShape> shapes = drawingPatriarch.getShapes();
        for (int i = 0; i < shapes.size(); i++) {
            XSSFShape shape = shapes.get(i);
            if (shape instanceof XSSFPicture && !CREATE_PIC_INDEX.contains(i)) {
                CREATE_PIC_INDEX.add(i);
                XSSFPicture picture = (XSSFPicture) shape;
                picture.resize(0);
            }
        }
    }

    /**
     * 重新插入一个图片
     *
     * @param sheet       Excel页面
     * @param cell        表格元素
     * @param pictureData 图片数据
     * @param i           图片顺序
     */
    private void insertImage(Sheet sheet, Cell cell, byte[] pictureData, int i) {
        int picWidth = Units.pixelToEMU(60);
        int index = sheet.getWorkbook().addPicture(pictureData, HSSFWorkbook.PICTURE_TYPE_PNG);
        CREATE_PIC_INDEX.add(index);
        Drawing<?> drawing = sheet.getDrawingPatriarch();
        if (drawing == null) {
            drawing = sheet.createDrawingPatriarch();
        }
        CreationHelper helper = sheet.getWorkbook().getCreationHelper();
        ClientAnchor anchor = helper.createClientAnchor();
        // 设置图片坐标
        anchor.setDx1(picWidth * i);
        anchor.setDx2(picWidth + picWidth * i);
        anchor.setDy1(0);
        anchor.setDy2(0);
        // 设置图片位置
        int columnIndex = cell.getColumnIndex();
        anchor.setCol1(columnIndex);
        anchor.setCol2(columnIndex);
        int rowIndex = cell.getRowIndex();
        anchor.setRow1(rowIndex);
        anchor.setRow2(rowIndex + 1);
        // ClientAnchor.AnchorType里有多种类型可选,从网上看的之前是不移动,直接悬浮在单元格上了,现在这个是随着单元格移动
        anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
        drawing.createPicture(anchor, index);
    }
}

核心功能:

  • 自适应行列宽:根据图片数量动态调整列宽,固定行高适配 60*60px 图片。
  • 多图片横向排列:支持单元格内多图片横向显示,间距 10px,避免重叠。
  • 图片随单元格移动:设置锚点类型为 MOVE_AND_RESIZE,图片会随单元格位置变化而同步移动。

5. 业务层导出实现

最后在 Controller 中编写导出接口,整合上述组件完成 Excel 导出:

@GetMapping("/recognizeRecordExport")
public void recognizeRecordExport(RecognizeRecordPageReqVo reqVo, HttpServletResponse response) throws IOException {

    // 获取数据逻辑省略
    List<RecordExcelVO> excelVOList = getRecordExcelVOS(reqVo);
    String filename = "识别数据.xlsx";
    // 输出 Excel
    EasyExcel.write(response.getOutputStream(), RecordExcelVO.class)
            .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
            .registerWriteHandler(new ImageWriteHandler())
            .sheet(filename).doWrite(excelVOList);
    // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
    response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename));
    response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}

关键注意事项:

  • 中文文件名乱码:使用 URLEncoder.encode 编码文件名,并指定 filename*=UTF-8'' 格式,兼容主流浏览器。
  • 响应头设置contentType 对于 xlsx 格式需设置为 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,xls 格式需改为 application/vnd.ms-excel
  • 流关闭策略:设置 autoCloseStream(false),避免 EasyExcel 提前关闭响应流导致下载失败。

以上就是使用EasyExcel导出带图片文件的所有内容了,希望对大家有所帮助~

2

评论 (0)

取消