EasyPOI 导出网址图片到Excel解决方案
0、需求背景
在最近写的一个项目中,后台用户管理界面需要添加一个导出表格数据的功能,然后我就想到了使用EasyPOI这个技术来实现,有关它的使用可以看我之前记录的这两篇文章:
使用EasyPOI来实现导入导出表格数据我是有学习使用过的,但是这次却不同,在我之前写的一个项目中,用户信息中的用户头像是以文件的形式直接上传到服务端进行保存的,然后在数据库表字段中保存的是图片在本机的地址路径,然后在使用EasyPOI进行导出时,只需要在对应的属性上加如下注解:
@Excel(name = "用户头像", type = 2 ,width = 40 , height = 20, imageType = 1)
private String avatar;
type =2 该字段类型为图片
- imageType=1 (默认可以不填)表示从file读取,字段类型是个字符串类型,可以用相对路径也可以用绝对路径、绝对路径优先依次获取。
- imageType=2 表示从数据库或者已经读取完毕,字段类型是个字节数组,直接使用,同时image 类型的cell最好设置好宽和高,会百分百缩放到cell那么大,不是原尺寸。
显然,在之前项目的这种情况下,只需要给注解添加属性imageType=1
即可,然而这次在表中保存的是图片的URL地址,这种方式应该是更常用,毕竟把太多的图片保存到服务器是不现实的,大多都会使用第三方提供的对象存储,比如阿里的OSS、腾讯的COS等。
总之需求简述就是:已知图片的URL,需要将其保存到Excel表中进行导出。
通过强大的搜索引擎技术,最终解决了这个问题,然后在这里记录下。
1、解决方案
导出实体类
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ExcelTarget("UserVO")
public class UserVO {
// 省略其他字段属性...
@Excel(name = "用户ID")
private Long uid;
// 省略其他字段属性...
@Excel(name = "用户性别", replace = {"男_0","女_1", "保密_2"}, suffix = "生")
private Integer gender;
// 省略其他字段属性...
// 重点关注以下两个字段
// 在数据库表中记录的是avatar URL
@Excel(name = "头像图片路径", width = 40)
private String avatar;
// 需要在额外添加一个专门用于存储从网络下载的图片二进制数据的byte数组
@JsonIgnore
@Excel(name = "用户头像", type = 2, width = 40, height = 20, imageType = 2)
private byte[] imgFile;
}
那么重点就是去获取网络图片的二进制数据了,可以通过Java 网络编程、IO相关技术来实现,在这里可以使用网络图片下载工具类:
package cn.imyjs.haibao.utils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
/**
* @author YJS
* @classname HttpImgUtils
* @description 网络图片下载工具
* @date 2023/2/21 10:19
* @webSite www.imyjs.cn
*/
public class HttpImgUtils {
/**
* 获取网络图片转成字节流
* @param strUrl 完整图片地址
* @return 图片资源数组
*/
public static byte[] getNetImgByUrl(String strUrl) {
try {
URL url = new URL(strUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(2 * 1000);
// 通过输入流获取图片数据
InputStream inStream = conn.getInputStream();
// 得到图片的二进制数据
byte[] btImg = readInputStream(inStream);
return btImg;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 从输入流中获取字节流数据
* @param inStream 输入流
* @return 图片流
*/
private static byte[] readInputStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
// 设置每次读取缓存区大小
byte[] buffer = new byte[1024*10];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
inStream.close();
return outStream.toByteArray();
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
String url = "https://www.baidu.com/img/flexible/logo/pc/result.png";
System.out.println(Arrays.toString(getNetImgByUrl(url)));
}
}
控制层代码
那么下面就是进行编写接口来实现功能了。
@ApiOperation("导出用户信息数据")
@GetMapping("/export")
public void exportRecords(HttpServletResponse response) throws IOException {
// 首先通过service层去获取到需要导出的用户信息数据列表
List<UserVO> userVOS = userService.getAllUserInfo();
// 重点:下面是需要对于图片URL字段进行处理
for (UserVO img : userVOS) {
try {
// 通过工具类获取到对应URL图片的二进制数据,再保存到 ImgFile 的byte数组中
img.setImgFile(HttpImgUtils.getNetImgByUrl(img.getAvatar()));
// 路径无效则无法下载图片资源,导致该字段为NULL
if (img.getImgFile() == null) {
// 如果该图片资源不存在,则给默认的图片资源
img.setImgFile(HttpImgUtils.getNetImgByUrl("http://imyjs.cn/corepress_avatar/1.jpg"));
}
} catch (Exception e) {
log.error("导出文件_图片资源获取失败 url = " + img.getAvatar());
continue;
}
}
// 下面就是使用EasyPOI技术进行导出了
String fileName = "用户信息.xls";
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams("用户信息", "用户信息"), UserVO.class, userVOS);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
response.setCharacterEncoding("UTF-8");
response.setHeader("content-Type", "application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
workbook.write(outputStream);
} catch (IOException e) {
log.error("导出图片失败");
throw new RuntimeException(e);
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
然而此时又出现了新的问题,Easypoi导出报文件扩展名与文件格式或文件扩展名无效问题,
就是下载出来的文件打不开,提示文件格式错误什么的,然后我又开始再网上搜索各种解决方案,其中有一个是这么解决的:
try (OutputStream out = response.getOutputStream()) {
response.setCharacterEncoding("UTF-8");
response.setHeader("content-Type", "application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
workbook.write(baos);
response.setHeader("Content-Length", String.valueOf(baos.size()));
out.write(baos.toByteArray());
} catch (IOException e) {
try {
throw new Exception(e.getMessage());
} catch (Exception e1) {
// TODO Auto-generated catch block
log.error("导出图片失败");
e1.printStackTrace();
}
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
说原因是:Debug发现请求头的Content-Length,在未设置的情况在是-1,下载时需重新定义Content-Length。
然而在我这里并没有解决问题,没什么卵用。
继续....
然后通过一篇文章,简单看了下源码,感觉是文件格式的问题,
于是我就在这方面想,忽略了前端代码,就是前端下载时指定的文件名后缀要和后端保持一致为.xls
。
2、前端代码
// import fileDownload from 'js-file-download'
// 导出用户数据
exportData() {
exportUserData().then((resp) => {
// 可以使用js-file-download工具
// fileDownload(resp, '用户信息.xlsx')
// 这里resp.data是返回的blob对象
const blob = new Blob([resp.data],
{ type: 'application/vnd.openxmlformats- officedocument.spreadsheetml.sheet;charset=utf-8' })
// application/vnd.openxmlformats-officedocument.spreadsheetml.sheet这里表示xlsx类型
const downloadElement = document.createElement('a')
const href = window.URL.createObjectURL(blob) // 创建下载的链接
downloadElement.href = href
downloadElement.download = '用户信息.xls' // 下载后文件名
document.body.appendChild(downloadElement)
downloadElement.click() // 点击下载
document.body.removeChild(downloadElement) // 下载完成移除元素
window.URL.revokeObjectURL(href) // 释放掉blob对象
}).catch((r) => {
console.log(r)
this.$message.error('文件导出异常!')
})
}
最终解决了问题,效果如下: