问题
之前在项目中导出数据都是用的excel,但是随着生产数据量的增大,excel已经无法满足实际业务的需要。主要问题是如果一次性导出的数据过于庞大,excel格式导出速度慢,且存在内存被撑爆的可能。综合考虑采用csv格式导出,动态设定每个csv文件的数据条数,多个csv文件打成zip包。
/** 采用apache commons cvs
*导出csv文件,设定每个文件的最大数量,多个csv文件打成zip包
* 类说明
*/
public class CsvUtils {
private static Logger logger = LoggerFactory.getLogger(CsvUtils.class);
private static final int BUFFER_SIZE = 8192;//缓冲区大小
/**
* 将内容写入csv文件
* @param downloadTempPath 参考:request.getServletContext().getRealPath("download")
* @param relativePath 举例:${sessionId}/${productId}
* @param fileName 不带后缀名,举例:多个文件返回文件的后缀是zip,单个是csv
* @param csvCharset 读写csv文件的编码字符集
* @param header 表头
* @param records 内容
* @param csvMaxRowNum 写csv文件时单文件的最大行数(超过的分多个文件并压缩成zip包)
* @return 服务器磁盘上的文件
* @throws IOException 写入失败
*/
protected File writeToCsv(String downloadTempPath, String relativePath, String fileName, String csvCharset, List<String> header, List<List<String>> records, int csvMaxRowNum) throws IOException {
if(StringUtils.isBlank(fileName)) {
throw new IOException("文件名为空,生成csv文件失败");
}
if(header == null || header.isEmpty()) {
throw new IOException("表头为空,生成csv文件[" + fileName + "]失败");
}
String tempPath = downloadTempPath;
if(StringUtils.isBlank(tempPath)) {
tempPath = "/salemanagement/temp/";
}
if(!StringUtils.endsWith(tempPath, "/")) {
tempPath += "/";
}
if(relativePath == null) {
relativePath = "";
}
relativePath = StringUtils.removeStart(relativePath, "/");//如果以"/"开头,则去掉"/"字符
String absolutePath = tempPath + relativePath;
int recordsNum = records == null ? 0 : records.size();
if(recordsNum <= csvMaxRowNum) {//如果数据没有超过csvMaxRowNum,则返回csv文件
List<List<String>> csvData = new ArrayList<List<String>>();
csvData.add(header);
if(recordsNum > 0) {//如果有数据则添加,否则将返回一个只有表头的csv文件
csvData.addAll(records);
}
return writeRecords(csvData, absolutePath, fileName + ".csv", false, csvCharset);
} else {
List<File> files = new ArrayList<File>();
int fullFileNum = recordsNum / csvMaxRowNum;//能够写满(数据正好等于csvMaxRowNum)的文件的个数
for (int i = 0; i < fullFileNum; i++) {
List<List<String>> csvData = new ArrayList<List<String>>();
csvData.add(header);
csvData.addAll(records.subList(csvMaxRowNum * i, csvMaxRowNum * (i + 1)));
files.add(writeRecords(csvData, absolutePath, fileName + "_" + (i + 1) + ".csv", false, csvCharset));//文件序号从1开始
}
int leftNum = recordsNum % csvMaxRowNum;//不能写满一个文件时,剩余记录数。如果大于0,则需要单独写一个文件
if(leftNum > 0) {
List<List<String>> csvData = new ArrayList<List<String>>();
csvData.add(header);
csvData.addAll(records.subList(csvMaxRowNum * fullFileNum, recordsNum));
files.add(writeRecords(csvData, absolutePath, fileName + "_" + (fullFileNum + 1) + ".csv", false, csvCharset));//文件序号从1开始
}
return ZipFileUtil.zip(fileName + ".zip", absolutePath, files.toArray(new File[files.size()]));
}
}
/**
* 将字符串List存到文件中,每个List<String>一行
* @param records 字符串List
* @param filePath 目标全路径
* @param fileName 目标文件名
* @param append true追加,false覆盖
* @param charset 编码字符集
* @return 目标文件
* @throws IOException 写文件出错(包括字符集错误)
*/
public static File writeRecords(List<List<String>> records, String filePath, String fileName, boolean append, String charset) throws IOException {
File parentFile = new File(filePath);
if(!parentFile.exists()) {
parentFile.mkdirs();
}
OutputStreamWriter osw = null;
CSVPrinter csvPrinter = null;
try {
File file = new File(parentFile, fileName);
if(StringUtils.isBlank(charset)) {
osw = new OutputStreamWriter(new FileOutputStream(file, append));
} else {
osw = new OutputStreamWriter(new FileOutputStream(file, append), charset);
}
csvPrinter = new CSVPrinter(osw, CSVFormat.DEFAULT);
for (int i = 0; i < records.size(); i++) {
csvPrinter.printRecord(records.get(i));
}
return file;
} finally {
if(csvPrinter!=null) {
try {
csvPrinter.close();
} catch (Exception e) {
logger.warn("关闭CSVPrinter失败", e);
}
}
if(osw!=null) {
try {
osw.close();
} catch (Exception e) {
logger.warn("关闭csv文件[" + fileName + "]失败", e);
}
}
}
}
/**
* 压缩文件(保留文件的修改时间,不保留文件夹的)
* @param targetFileName 不包含路径,以zip结尾
* @param absRootPath 源文件必须在这个目录或子目录下,目标文件生成在这个目录下
*/
public static File zip(String targetFileName, String absRootPath, File... sourceFiles) throws IOException {
File targetFile = new File(absRootPath, targetFileName);
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new FileOutputStream(targetFile));
compress(zos, absRootPath, sourceFiles);
} finally {
if(zos != null) {
try {
zos.close();
} catch (IOException e) {
logger.warn("关闭目标文件[" + targetFile + "]的输入流失败", e);
}
}
}
return targetFile;
}
private static void compress(ZipOutputStream zipOutputStream, String absRootPath, File... sourceFiles) throws IOException {
absRootPath = StringUtils.replace(absRootPath, "\\", "/");
if(!absRootPath.endsWith("/")) {
absRootPath += "/";
}
for (File sourceFile : sourceFiles) {
if (sourceFile.isFile()) {
String absPath = sourceFile.getAbsolutePath();
absPath = StringUtils.replace(absPath, "\\", "/");
String zipEntryName = StringUtils.removeStart(absPath, absRootPath);
if(absPath.equals(zipEntryName)) {
throw new RuntimeException("压缩出错,absRootPath["+absRootPath+"]与absPath["+absPath+"]不匹配");
}
ZipEntry zipEntry = new ZipEntry(zipEntryName);
// zipEntry.setSize(sourceFile.length());
zipEntry.setTime(sourceFile.lastModified());
zipOutputStream.putNextEntry(zipEntry);
InputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(sourceFile));
int readLength = -1;
byte[] buffer = new byte[BUFFER_SIZE];
while ((readLength = bis.read(buffer, 0, BUFFER_SIZE)) != -1) {
zipOutputStream.write(buffer, 0, readLength);
}
} finally {
if(bis != null) {
try {
bis.close();
} catch (IOException e) {
logger.warn("关闭文件[" + sourceFile + "]的输入流失败", e);
}
}
try {
zipOutputStream.closeEntry();
} catch (Exception e) {
logger.warn("关闭ZipEntry[" + zipEntryName + "]失败", e);
}
}
} else {
compress(zipOutputStream, absRootPath, sourceFile.listFiles());
}
}
}
/**
* 解压文件(保留文件的修改时间,不保留文件夹的)
* @param targetDir 目标目录的绝对路径
*/
public static void unzip(File zipFile, String targetDir) throws IOException {
targetDir = StringUtils.replace(targetDir, "\\", "/");
if(!targetDir.endsWith("/")) {
targetDir += "/";
}
ZipFile zip = null;
try {
zip = new ZipFile(zipFile);
Enumeration<? extends ZipEntry> entries = zip.entries();
while(entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if(!entry.isDirectory()) {//加压文件会自动创建父目录,文件夹全部忽略
File entryFile = new File(targetDir + entry.getName());
if (entryFile.exists()) {
entryFile.delete();
}
File parent = entryFile.getParentFile();
if(!parent.exists()) {
parent.mkdirs();
}
InputStream bis = null;
OutputStream bos = null;
try {
bis = new BufferedInputStream(zip.getInputStream(entry));
bos = new BufferedOutputStream(new FileOutputStream(entryFile));
int readLength = -1;
byte[] buffer = new byte[BUFFER_SIZE];
while ((readLength = bis.read(buffer, 0, BUFFER_SIZE)) != -1) {
bos.write(buffer, 0, readLength);
}
bos.flush();
} finally {
if(bis != null) {
try {
bis.close();
} catch (IOException e) {
logger.warn("关闭entry[" + entry.getName() + "]的输入流失败", e);
}
}
if(bos != null) {
try {
bos.close();
} catch (IOException e) {
logger.warn("关闭文件[" + entryFile.getPath() + "]的输出流失败", e);
}
}
}
entryFile.setLastModified(entry.getTime());
}
}
} finally {
if(zip != null) {
try {
zip.close();
} catch (IOException e) {
logger.warn("关闭压缩文件[" + zipFile.getPath() + "]失败", e);
}
}
}
}
}
测试代码
/**
* 导出CSV文件
*/
@RequestMapping("/downloadFile")
public ResponseEntity<byte[]> downloadFile(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
List<String> header = new ArrayList<>();
header.add("编号");
header.add("姓名");
header.add("手机号");
List<List<String>> lists = new ArrayList<>();
for (int i = 0;i < 100;i++) {
List<String> dataList = new ArrayList<>();
dataList.add(i + "");
dataList.add("Jack" + i);
dataList.add("18815764728");
lists.add(dataList);
}
//导出CSV文件
File file = writeToCsv(request.getServletContext().getRealPath("download"), session.getId(), "note","GBK", header, lists,20);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment;filename=" + file.getName());
return new ResponseEntity<>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK);
}
小结:导出csv格式的文件效率比excel大大提高,但导出的csv文件如果用excel打开存在一定的问题:如导出的身份证,手机号和时间显示结果并不是自己想要的结果【手机号会转变成科学记数法,时间会用###代替】,需要手动去调整。目前网上有2种解决方案,一种是加上制表符“\t”,一种是“=数据”,(具体使用自行上网查阅)。但这两种解决方法都存在问题,即打开时没问题,但复制文本时会带上空格或“=”,两种效果不一样。
如果谁有完美的解决方法,希望留言告知,感谢!
版权声明:本文为gufenghua0222原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。