首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
231 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
202 阅读
3
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
91 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
66 阅读
5
解决 Mac 版 PicGo 无法打开问题:“已损坏,无法打开” 报错处理指南
37 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
java虚拟机
JVM
保姆级教程
Java
Spring AI
SpringBoot
Nginx
WebFlux
Spring
cdn
https
dcdn
网站加速
Tool
图片导出
服务部署
源码解析
单点登录
google
sso
Luca Ju
累计撰写
35
篇文章
累计收到
1
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
1
篇与
的结果
2025-11-21
EasyExcel 实战:导出带图片的 Excel 完整方案
在实际开发中,可能会遇到导出 Excel 时需要包含图片的场景。EasyExcel 作为阿里开源的高效 Excel 处理工具,虽然原生支持图片导出,但在图片格式适配、单元格样式调整等细节上需要自定义扩展。本文将详细讲解如何基于 EasyExcel 实现带图片的 Excel 导出,包含完整代码示例和关键细节说明。一、最终效果展示导出的 Excel 中,图片将自适应单元格大小,无图片时显示"无图"提示,整体格式整洁规范:二、实现步骤详解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导出带图片文件的所有内容了,希望对大家有所帮助~
2025年11月21日
17 阅读
0 评论
2 点赞