1、 准备Excel
- 如下图所示,需要支持在Excel中导入图片,并上传至指定文件中。
- Excel中的图片不能嵌入单元格,否则无法实现该功能。
2、 实现思路
- 使用在Apache POI中,XSSFDrawing和HSSFPatriarch类,进行获取Excel对应sheet页中的所有图片。
- 将图片流式存储至服务器的临时文件夹中。
- 业务处理结束后,将临时文件夹中的数据转移至目标文件夹。
- 删除临时文件夹。
3、使用工具
导入Apache POI的包解析Excel
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
4、读取Excel文件
Java 有多种读取文件的方法。可参考使用
5、创建临时文件夹
在服务器中创建临时文件夹,用于存储当前Excel中的图片,操作结束后无论是否成功,均要删除该文件夹。
/**
*
* 获取历史文件夹目录
*
* @param workerId
* 导入唯一标识,not null
* @return 文件路径
*/
public static String genTempSubFolder(String workerId) {
Date d = new Date();
String currentFileFolder = new SimpleDateFormat("yyyyMMdd").format(d) + "-" + workerId;
String path = "tmp".replace("\\", "/");
String fileId = currentFileFolder.replace("\\", "/");
if (!path.endsWith("/") && !fileId.startsWith("/")) {
path += "/";
}
return path + fileId;
}
6、读取Excel中某个Sheet中的图片
- 首先判断导入唯一标识workerId是否为空,如果为空则直接返回空的HashMap。
- WorkerId的作用是标记每一次的唯一标识,用于生成临时文件夹
- 创建一个HashMap用于保存行号和文件名集合的映射关系。
- 根据workerId生成一个临时子文件夹。
- 遍历表格中的每一行,通过判断工作簿类型(XSSFWorkbook或HSSFWorkbook),分别处理图片的读取和保存。
- 对于XSSFWorkbook类型的工作簿,通过XSSFDrawing获取所有形状对象,判断形状是否为XSSFPicture类型,如果是则调用getPicture方法保存图片,并将文件名添加到结果集中。
- 对于HSSFWorkbook类型的工作簿,通过HSSFPatriarch获取所有形状对象,判断形状是否为HSSFPicture类型,如果是则调用getPicture方法保存图片,并将文件名添加到结果集中。
- 判断是否是图片类型的数据
- 写入图片到本地
- 返回结果
- 返回结果集。
/**
*
* 获取单元格图片内容
*
* @param sheet
* 标签页,not null
* @param workerId
* 导入唯一标识,not null
* @param nullable
* 是否允许为空,not null
*
* @return 行号 - 文件名集合
*/
public static Map<Integer, List<String>> readImagesUrl(Sheet sheet, String workerId, boolean nullable)
throws Exception {
if (StringUtil.isNullOrBlank(workerId)) {
return new HashMap<>();
}
Map<Integer, List<String>> result = new HashMap<>();
String tmpDir = genTempSubFolder(workerId);
for (int rowIdx = 1; rowIdx <= sheet.getLastRowNum(); rowIdx++) {
Workbook workbook = sheet.getWorkbook();
int index = 0;
if (workbook instanceof XSSFWorkbook) {
XSSFDrawing drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
for (XSSFShape shape : drawing.getShapes()) {
if (shape instanceof XSSFPicture) {
XSSFPicture picture = (XSSFPicture) shape;
index++;
getPicture(picture, tmpDir, workerId, index, result);
}
}
} else if (workbook instanceof HSSFWorkbook) {
HSSFPatriarch drawing = (HSSFPatriarch) sheet.createDrawingPatriarch();
List<HSSFShape> shapes = drawing.getChildren();
for (HSSFShape shape : shapes) {
if (shape instanceof HSSFPicture) {
HSSFPicture picture = (HSSFPicture) shape;
index++;
getPicture(picture, tmpDir, workerId, index, result);
}
}
}
}
return result;
}
private static void getPicture(Picture picture, String tmpDir, String workerId, Integer index, Map<Integer, List<String>> result) throws Exception {
PictureData pictureData = picture.getPictureData();
ClientAnchor clientAnchor = picture.getClientAnchor();
byte[] data = null;
// 起始行
int startRow = clientAnchor.getRow1();
// 截止行
int endRow = clientAnchor.getRow2();
// 起始列
int startCol = clientAnchor.getCol1();
// 截止列
int endCol = clientAnchor.getCol2();
if (startRow != endRow) {
return;
}
// 获取图片
data = pictureData.getData();
// 获取图片格式
String ext = pictureData.suggestFileExtension();
if (!isImageFile(ext)) {
throw new Exception("非图片类型,不支持excel导入。");
}
String fileId = MessageFormat.format("import_{0}_{1}_{2}.{3}", workerId, startRow, index, ext);
// 写入临时文件夹
writeFile(tmpDir, fileId, new ByteArrayInputStream(data));
List<String> rowImages = result.get(startRow);
if (rowImages == null) {
rowImages = new ArrayList<>();
result.put(startRow, rowImages);
}
rowImages.add(fileId);
}
// 判断是否是图片类型的数据
private static String[] videoTypes = new String[] {
"avi", "wmv", "mpg", "mpeg", "mov", "rm", "ram", "swf", "flv", "mp4" };
public static boolean isImageFile(String fileUrl) {
Assert.assertArgumentNotNull(fileUrl, "fileUrl");
String type = fileUrl.substring(fileUrl.lastIndexOf(".") + 1);
for (String str : imageTypes) {
if (str.equalsIgnoreCase(type)) {
return true;
}
}
return false;
}
/**
* 将文件写入到本地。
*
* @param uploadRootDir
* 上传根目录,not null。
* @param fileId
* 上传子路径,含文件名,not null。
* @param sourceStream
* 文件输入流,not null。
* @throws IOException
*/
public static void writeFile(String uploadRootDir, String fileId, InputStream sourceStream)
throws IOException {
String serverFile = FileUtil.concat(uploadRootDir, fileId);
String filePath = FileUtil.getFilePath(serverFile);
File dir = new File(filePath);
if (!dir.exists()) {
dir.mkdirs();
}
try (BufferedInputStream bis = new BufferedInputStream(sourceStream);
FileOutputStream fos = new FileOutputStream(serverFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[4096];
int readBytes;
while ((readBytes = bis.read(buffer)) != -1) {
bos.write(buffer, 0, readBytes);
}
bos.flush();
}
}
7、获取临时文件中的图片,并移动至其他文件夹
- 使用new FileInputStream(path)读取文件;
- 使用上述代码中的writeFile()方法写文件;
8、 注意点
在Java中,调用pictureData.getData()方法时,会返回一个byte[]数组,该数组包含了图片的二进制数据。这个数组会占用内存,直到它被垃圾回收器回收。
当你使用多个XSSFPictureData对象调用getData()方法时,每个方法调用都会返回一个新的byte[]数组,并且每个数组都会占用内存,直到它们被垃圾回收器回收。
如果你不再需要这些图片数据,你可以设置它们为null,这样它们将会被垃圾回收器回收,并释放占用的内存。
方案:
- 及时释放资源:一旦图片数据不再需要,应确保相关的 byte[] 数组不再被引用,以便垃圾回收器能够回收内存。你可以在处理完图片数据后,将 data 数组设置为 null。
- 避免不必要的数据加载:如果可能,避免重复加载同一张图片的数据。例如,如果需要多次使用同一张图片的数据,可以将其存储在一个缓存中,避免多次调用 getData()。
- 使用流式处理:如果图片数据非常大,可以考虑使用流式处理技术,如读取图片数据时使用输入流,这样可以避免一次性将整个图片加载到内存中。
- 图片预处理:在加载图片到Excel之前,可以先进行预处理,比如压缩图片或调整其尺寸,以减少内存占用。
- 内存监控:定期监控应用程序的内存使用情况,特别是在处理包含大量图片的Excel文件时,确保内存使用在可接受范围内。
- 提高效率的话可以使用多线程进行图片的处理。