代码仓库:ExcelAttach.java
配套视频:https://www.bilibili.com/video/BV1wT41197W8
需求
需求要做一个小小的员工信息收集系统,员工登录页面,填写一些信息,然后上传附件,比如身份证、证书照片、其他什么电子档之类的。
系统管理后台会提供列表和详情页来查看,同时,一般都会配备导出功能。
这篇博客就是实现导出这个小功能。
功能点:除了导出excel,还需要与附件一起打包zip,excel中每条记录要用超链接关联附件目录。
效果图
前置知识:excel 设置超链接到本地目录
方式1:单元格上 右键–>超链接 或者 CTRL+K 快捷键 调出超链接编辑窗口
- Excel小知识:超链接3种常用的使用方法
- 选择左侧“原有文件或网页”图标,可以选择文件夹或者文件,然后点击确定按钮即可。
- 打开效果:txt文件是直接记事本打开,图片会用浏览器打开。
方式2:输入文件协议 file:// ,可以打开文件夹或者文件,但是只支持绝对路径
- 相对路径会提示 无法打开指定的文件
方式3:超链接函数 HYPERLINK
- WPS学堂–表格系列课–查找和引用函数–HYPERLINK函数
- HYPERLINK(链接位置, [显示文本]]) 第2个参数可省
- 可以文件夹或文件,可以相对路径或绝对路径
- =HYPERLINK(“.”, “打开当前目录”)
java实现
1.主要代码
- 使用poi生成excel
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.spire.ms.System.Collections.ArrayList;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import util.ZipUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 导出excel,并与附件打包zip,每条记录用超链接关联附件目录
*/
public class ExcelAttach {
// zip文件生成根目录
private static String rootDirLinux = "/opt/export_tmp"; // linux
private static String rootDirWin = "D:\\export_tmp"; // windows
public static void main(String[] args) throws Exception {
exportZipByExcelAndAttach();
}
/**
* 导出zip,将生成的excel和附件一起打包,每条记录的附件目录以超链接(相对路径)跳转
*/
private static void exportZipByExcelAndAttach() throws Exception {
// 准备数据
List<Map<String, Object>> list = new ArrayList();
Map<String, Object> map = ImmutableMap.of("gh", "1", "name", "姓名-001", "deptName", "部门-001",
"attachUrls", Arrays.asList("https://i2.hdslb.com/bfs/archive/c268dd8a28a29ee6872375c9a4385c35d789a5db.jpg@320w_200h"));
// 不可变,一旦创建就不能再往里添加键值对了
// 转为可变
map = Maps.newHashMap(map);
list.add(map);
list.add(ImmutableMap.of("gh", "2", "name", "姓名-002", "deptName", "部门-002",
"attachUrls", Arrays.asList("https://i2.hdslb.com/bfs/archive/c268dd8a28a29ee6872375c9a4385c35d789a5db.jpg@320w_200h")));
list.add(ImmutableMap.of("gh", "3", "name", "姓名-003", "deptName", "部门-003",
"attachUrls", Arrays.asList("https://i2.hdslb.com/bfs/archive/c268dd8a28a29ee6872375c9a4385c35d789a5db.jpg@320w_200h")));
list.add(ImmutableMap.of("gh", "4", "name", "姓名-004", "deptName", "部门-004",
"attachUrls", Arrays.asList("https://i2.hdslb.com/bfs/archive/c268dd8a28a29ee6872375c9a4385c35d789a5db.jpg@320w_200h")));
// 创建工作薄
// xlsx格式 XSSF
Workbook workbook = new XSSFWorkbook();
// 创建工作表
Sheet sheet = workbook.createSheet("用户数据");
// 设置列宽 设置默认宽度
sheet.setDefaultColumnWidth(25);
// 设置表头 简单点,不设置样式了
String[] headers = new String[]{"工号", "姓名", "部门", "附件"};
// 创建第一行
Row titleRow = sheet.createRow(0);
Cell cell = null;
for (int i = 0; i < headers.length; i++) {
// 创建单元格
cell = titleRow.createCell(i);
cell.setCellValue(headers[i]);
}
long currentTimeMillis = System.currentTimeMillis();
// 遍历集合数据,生成数据行
if (CollectionUtils.isNotEmpty(list)) {
// 从第2行开始
int rowIndex = 1;
Row row = null;
for (Map<String, Object> item : list) {
String gh = (String) item.get("gh");
String name = (String) item.get("name");
String deptName = (String) item.get("deptName");
List<String> attachUrls = (List<String>) item.get("attachUrls");
row = sheet.createRow(rowIndex);
cell = row.createCell(0);
cell.setCellValue(gh);
cell = row.createCell(1);
cell.setCellValue(name);
cell = row.createCell(2);
cell.setCellValue(deptName);
cell = row.createCell(3);
if (CollectionUtils.isNotEmpty(attachUrls)) {
for (int i = 0; i < attachUrls.size(); i++) {
String attachUrl = attachUrls.get(i);
// 附件目录 相对目录 和生成的excel同级
// 因为附件不止一个,所以直接打开附件目录即可
String codeLink = getFilePath(deptName, name, gh, null, currentTimeMillis, 3);
// 超链接描述
String code = "打开附件目录";
// 生成的超链接不带蓝色下划线样式
String formula = MessageFormat.format("HYPERLINK(\"{0}\",\"{1}\")", codeLink, code);
cell.setCellFormula(formula);
// 手动给超链接添加样式
// 创建单元格样式
CellStyle cellStyle = workbook.createCellStyle();
// 不直接使用getCellStyle(),用cloneStyleFrom就能实现保持原有样式
cellStyle.cloneStyleFrom(cell.getCellStyle());
// 设置字体
Font font = workbook.createFont();
font.setColor(IndexedColors.BLUE.getIndex());
font.setUnderline((byte) 1);
cellStyle.setFont(font);
// 设置单元格样式
cell.setCellStyle(cellStyle);
String fileName = "附件_" + (i+1) + ".jpg";
// 获取附件保存地址
String filePath = getFilePath(deptName, name, gh, fileName, currentTimeMillis, 2);
// 要注意的点就是,如果附件太多,下载附件可能需要很久很久
// 不能实现前端立即下载,需要后台生成,然后消息通知之类的
// 如果把附件直接保存在本地服务器之上,速度会快一点
saveFile(attachUrl, filePath);
}
}
rowIndex++;
}
}
// 获取excel保存地址
String excelFilePath = getFilePath(null, null, null,
"用户数据.xlsx", currentTimeMillis, 1);
// 生成excel
ExcelUtil.saveExcelFile(workbook, excelFilePath);
workbook.close();
// 打成zip
File desc = new File(excelFilePath);
// excel保存临时目录
File dir = desc.getParentFile();
SimpleDateFormat dateFormat = new SimpleDateFormat("用户数据(yyyy-MM-dd_HH-mm-ss)");
String zipFilePath = dir.getParentFile().getParentFile().getAbsolutePath() +
File.separator + dateFormat.format(new Date()) + ".zip";
try (FileOutputStream out = new FileOutputStream(zipFilePath)) {
ZipUtils.toZip(dir, out, true);
// 删除临时文件夹
ZipUtils.delFile(dir.getParentFile());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取文件路径
*
* @param deptName
* @param name
* @param gh
* @param currentTimeMillis
* @param fileName 文件名
* @param fileType 1-excel,2-附件,3-附件保存目录(相对路径)
*/
private static String getFilePath(String deptName, String name, String gh, String fileName, long currentTimeMillis, int fileType) {
StringBuilder filePath = new StringBuilder();
if (fileType != 3) {
// 绝对路径
String osName = System.getProperties().getProperty("os.name");
if (osName.contains("Linux")) {
filePath.append(rootDirLinux);
} else {
filePath.append(rootDirWin);
}
filePath.append(File.separator + "temp" + currentTimeMillis);
filePath.append(File.separator + "用户数据");
// 下方相对路径不需要 / 开头
filePath.append(File.separator);
}
if (fileType == 1) {
// 获取excel生成路径
filePath.append(fileName);
return filePath.toString();
}
// 获取当前用户附件保存路径
filePath.append("附件");
filePath.append(File.separator + deptName);
filePath.append(File.separator + name + "(" + gh + ")");
if (fileType == 3) {
// 附件保存目录(相对路径)
return filePath.toString();
}
filePath.append(File.separator + fileName);
return filePath.toString();
}
/**
* 下载网络附件,保存到临时目录
*
* @param urlString
* @param filePath
*/
private static String saveFile(String urlString, String filePath) {
File desc = new File(filePath);
File dir = desc.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
boolean rs = false;
if (!desc.exists()) {
ReadableByteChannel rbc = null;
FileOutputStream fos = null;
try {
URL website = new URL(urlString);
HttpURLConnection urlCon = (HttpURLConnection) website.openConnection();
// 指定超时时间,不指定可能会无限等待
urlCon.setConnectTimeout(180000);
urlCon.setReadTimeout(180000);
rbc = Channels.newChannel(urlCon.getInputStream());
fos = new FileOutputStream(desc);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
rs = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (rbc != null) {
try {
rbc.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
} else {
rs = true;
}
// 输出绝对路径
return rs ? desc.getAbsolutePath() : null;
}
}
2.pom
<!--引入poi-ooxml,就会引入poi-->
<!--<dependency>-->
<!-- <groupId>org.apache.poi</groupId>-->
<!-- <artifactId>poi</artifactId>-->
<!-- <version>4.1.0</version>-->
<!--</dependency>-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
3.zip工具类
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 压缩文件夹成zip
*
*/
public class ZipUtils {
private static final int BUFFER_SIZE = 2 * 1024;
/**
* 压缩成ZIP 方法1
*
* @param sourceFile 压缩文件夹
* @param out 压缩文件输出流
* @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
* @throws RuntimeException 压缩失败会抛出运行时异常
*/
public static void toZip(File sourceFile, OutputStream out, boolean KeepDirStructure)
throws RuntimeException {
try (ZipOutputStream zos = new ZipOutputStream(out)) {
compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 递归压缩方法
*
* @param sourceFile 源文件
* @param zos zip输出流
* @param name 压缩后的名称
* @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
* @throws Exception
*/
private static void compress(File sourceFile, ZipOutputStream zos, String name,
boolean KeepDirStructure) throws Exception {
byte[] buf = new byte[BUFFER_SIZE];
if (sourceFile.isFile()) {
// 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
zos.putNextEntry(new ZipEntry(name));
// copy文件到zip输出流中
int len;
FileInputStream in = new FileInputStream(sourceFile);
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
zos.closeEntry();
in.close();
} else {
File[] listFiles = sourceFile.listFiles();
if (listFiles == null || listFiles.length == 0) {
// 需要保留原来的文件结构时,需要对空文件夹进行处理
if (KeepDirStructure) {
// 空文件夹的处理
zos.putNextEntry(new ZipEntry(name + "/"));
// 没有文件,不需要文件的copy
zos.closeEntry();
}
} else {
for (File file : listFiles) {
// 判断是否需要保留原来的文件结构
if (KeepDirStructure) {
// 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
// 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
} else {
compress(file, zos, file.getName(), KeepDirStructure);
}
}
}
}
}
/**
* 删除本地文件及目录
*/
public static boolean delFile(File file) {
if (!file.exists()) {
return false;
}
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
delFile(f);
}
}
return file.delete();
}
}