簡介
這篇文章主要記錄自己學習上傳和導出Excel時的一些心得,企業辦公系統的開發中,經常會收到這樣的需求:批量錄入數據、數據報表使用 Excel 打開,或者職能部門同事要打印 Excel 文件,而他們又沒有直接操作數據庫的能力,這時就需要在某些模塊中實現導入、導出 Excel 的功能。
接下來,我們就來看看借助哪些庫、工具可以實行上述這些需求。
POI 簡介
Apache POI 是 Apache 軟件基金會的開放源碼函式庫,POI 提供了 API,可以幫助 Java 程序實現對 Microsoft Office 格式檔案的讀寫功能。
首先需要了解下 Excel 的文件格式,目前主要有兩種格式,即 xls 和 xlsx 格式。 xlsx 是從 Office 2007 版開始使用的,使用新的基於 XML 的壓縮文件格式取代了當時專有的默認文件格式,在傳統文件擴展名后面添加了字母 x 使其占用空間更小,可以向下兼容 xls ,2007 版本后的 Excel 軟件都可以操作 xls 和 xlsx 格式文件,而之前的版本只能打開 xls 格式文件。
針對不同 Excel 文檔格式,POI 提供了不同的類來處理。
針對 xls 格式,相應的類有:
- HSSFWorkbook excel 文檔對象
- HSSFSheet excel 表格對象
- HSSFRow excel 表格行對象
- HSSFCell excel 單元格對象
- HSSFCellStyle excel 單元格格式
- ……
針對 xlsx 格式,相應的類有:
- XSSFWorkbook excel 文檔對象
- XSSFSheet excel 表格對象
- XSSFRow excel 表格行對象
- XSSFCell excel 單元格對象
- XSSFCellStyle excel 單元格格式
- ……
操作 Excel,POI 也提供了相應的方法。
讀取 Excel,相應的方法有:
//獲取文件流 InputStream is = new FileInputStream(file); //得到Excel工作簿對象 XSSFWorkbook xssfWorkbook = new XSSFWorkbook(is); //得到Excel工作表對象 XSSFSheet xssfSheet = xssfWorkbook.getSheetAt(0); //得到Excel工作表的指定行對象 XSSFRow xssfRow = xssfSheet.getRow(i); //得到Excel工作表指定行的單元格 XSSFCell xssfCell = xssfRow.getCell(i); //得到單元格樣式 XSSFCellStyle xssfCellStyle = xssfCell.getCellStyle();
創建 Excel,相應的方法有:
//創建工作薄
XSSFWorkbook wb = new XSSFWorkbook(); //創建工作表對象 XSSFSheet sheet = wb.createSheet("sheet1"); //創建Excel工作表的行對象 XSSFRow row = sheet.createRow(i); //創建單元格樣式 XSSFCellStyle style = wb.createCellStyle(); //創建Excel工作表指定行的單元格 XSSFCell cell = row.createCell(i); //設置Excel單元格的值 cell.setCellStyle(style);
前端實現
導入功能涉及到文件上傳,因此需要增加文件上傳插件,引入 ajaxupload.js,代碼如下:
<!-- ajax upload --> <script src="plugins/ajaxupload/ajaxupload.js"></script>
增加 “ 導入 ” 功能按鈕:
<button class="btn btn-file" id="importV1Button"> <i class="fa fa-upload"></i> 導入V1 </button>
導入功能的處理流程是,首先判斷上傳文件的格式,之后向后端發送請求,后端處理完成后返回結果,前端根據返回結果進行判斷,如果錯誤則給出錯誤提示,如果正確則提示導入了多少條數據。
new AjaxUpload('#importV1Button', { action: 'users/importV1', name: 'file', autoSubmit: true, responseType: 'json', onSubmit: function (file, extension) { //文件格式限制 if (!(extension && /^(xlsx)$/.test(extension.toLowerCase()))) { alert('只支持xlsx格式的文件!', { icon: "error", }); return false; } }, onComplete: function (file, r) { if (r.resultCode == 200) { //提示用戶 alert("成功導入" + r.data + "條記錄!"); //列表數據重新加載 reload(); return false; } else { alert(r.message); } } });
前端處理流程可總結為:選擇導入文件 -> 文件上傳 -> 處理回調信息 -> 重新加載列表數據。
后端邏輯
控制層
在 Controller 層處理文件流,並調用業務層方法進行導入:
/** * <p> * 批量導入用戶(直接導入) */ @RequestMapping(value = "/importV1", method = RequestMethod.POST) public Result saveByExcelFileV1(@RequestParam("file") MultipartFile multipartfile) { File file = FileUtil.convertMultipartFileToFile(multipartfile); if (file==null){ return ResultGenerator.genFailResult("導入失敗!"); } int i = adminUserService.importUsersByExcelFile(file); if (i > 0) { Result result = ResultGenerator.genSuccessResult(); result.setData(i); return result; } else { return ResultGenerator.genFailResult("導入失敗"); } }
FileUtil的工具類
import org.apache.commons.io.FileUtils; import org.springframework.web.multipart.MultipartFile; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.net.URL; import java.net.URLConnection; import java.util.UUID; public class FileUtil { /** * 轉換MultipartFile對象為java.io.File類型 * * @param multipartFile * @return */ public static File convertMultipartFileToFile(MultipartFile multipartFile) { File result = null; try { /** * UUID.randomUUID().toString()是javaJDK提供的一個自動生成主鍵的方法。 * UUID(Universally Unique Identifier)全局唯一標識符,是指在一台機器上生成的數字, * 它保證對在同一時空中的所有機器都是唯一的,是由一個十六位的數字組成,表現出來的形式。 * 由以下幾部分的組合:當前日期和時間(UUID的第一個部分與時間有關,如果你在生成一個UUID之后, * 過幾秒又生成一個UUID,則第一個部分不同,其余相同),時鍾序列, * 全局唯一的IEEE機器識別號(如果有網卡,從網卡獲得,沒有網卡以其他方式獲得), * UUID的唯一缺陷在於生成的結果串會比較長。 * * * File.createTempFile和File.createNewFile()的區別: * 后者只是創建文件,而前者可以給文件名加前綴和后綴 */ //這里對生成的文件名加了UUID隨機生成的前綴,后綴是null result = File.createTempFile(UUID.randomUUID().toString(), null); multipartFile.transferTo(result); result.deleteOnExit(); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 根據url獲取文件對象 * * @param fileUrl * @return */ public static File downloadFile(String fileUrl) { File result = null; try { result = File.createTempFile(UUID.randomUUID().toString(), null); URL url = new URL(fileUrl); URLConnection connection = url.openConnection(); connection.setConnectTimeout(3000); BufferedInputStream bis = new BufferedInputStream(connection.getInputStream()); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(result)); byte[] car = new byte[1024]; int l = 0; while ((l = bis.read(car)) != -1) { bos.write(car, 0, l); } bis.close(); bos.close(); } catch (Exception e) { e.printStackTrace(); } return result; } /** * @param request * @return */ public static String getRealPath(HttpServletRequest request) { ServletContext sc = request.getSession().getServletContext(); String uploadDir = sc.getRealPath("/upload"); return uploadDir; } public static boolean saveFile(String savePath, String fileFullName, MultipartFile file) throws IOException { File uploadFile = new File(savePath + fileFullName); FileUtils.writeByteArrayToFile(new File(savePath, fileFullName), file.getBytes()); return uploadFile.exists(); } public static String mergeFile(int chunksNumber, String ext, String uploadFolderPath, HttpServletRequest request) { //合並分片流 String mergePath = uploadFolderPath; String destPath = getRealPath(request);// 文件路徑 String newName = System.currentTimeMillis() + ext;// 文件新名稱 SequenceInputStream s; InputStream s1; try { s1 = new FileInputStream(mergePath + 0 + ext); String tempFilePath; InputStream s2 = new FileInputStream(mergePath + 1 + ext); s = new SequenceInputStream(s1, s2); for (int i = 2; i < chunksNumber; i++) { tempFilePath = mergePath + i + ext; InputStream s3 = new FileInputStream(tempFilePath); s = new SequenceInputStream(s, s3); } //分片文件存儲到/upload/chunked目錄下 StringBuilder filePath = new StringBuilder(); filePath.append(destPath).append(File.separator).append("chunked").append(File.separator); saveStreamToFile(s, filePath.toString(), newName); // 刪除保存分塊文件的文件夾 deleteFolder(mergePath); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return newName; } private static boolean deleteFolder(String mergePath) { File dir = new File(mergePath); File[] files = dir.listFiles(); if (files != null) { for (File file : files) { try { file.delete(); } catch (Exception e) { e.printStackTrace(); } } } return dir.delete(); } private static void saveStreamToFile(SequenceInputStream inputStream, String filePath, String newName) throws Exception { File fileDirectory = new File(filePath); synchronized (fileDirectory) { if (!fileDirectory.exists()) { if (!fileDirectory.mkdir()) { throw new Exception("文件夾創建失敗,路徑為:" + fileDirectory); } } if (!fileDirectory.exists()) { if (!fileDirectory.mkdir()) { throw new Exception("文件夾創建失敗,路徑為:" + fileDirectory); } } } OutputStream outputStream = new FileOutputStream(filePath + newName); byte[] buffer = new byte[1024]; int len = 0; try { while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); outputStream.flush(); } } catch (Exception e) { e.printStackTrace(); throw e; } finally { outputStream.close(); inputStream.close(); } } }
通過調用FileUtil.convertMultipartFileToFile()方法將MultipartFile類型轉換成File類型再進行操作.
然后判空操作. 調用業務層里的方法,將轉換完的File類型對象傳入
業務層
@Override public int importUsersByExcelFile(File file) { XSSFSheet xssfSheet = null; //讀取File對象並轉換成XSSFSheet類型對象進行處理 try { //表格對象 xssfSheet = PoiUtil.getXSSFSheet(file); } catch (Exception e) { e.printStackTrace(); return 0; } ArrayList<AdminUser> adminUsers = new ArrayList<>(); //第一行是表名稱,第二行才是數據,所以從第二行開始讀取 for (int i = 1; i <= xssfSheet.getLastRowNum(); i++) { //獲取Excel表格指定行的對象 XSSFRow row = xssfSheet.getRow(i); if (row != null) { AdminUser adminUser = new AdminUser(); //獲取用戶名 XSSFCell userName = row.getCell(0); //獲取密碼 XSSFCell password = row.getCell(1); //設置用戶名 if (!StringUtils.isEmpty(userName)) { adminUser.setUserName(PoiUtil.getValue(userName)); } if (!StringUtils.isEmpty(password)) { adminUser.setPassword(MD5Util.MD5Encode(PoiUtil.getValue(password), "utf-8")); } //用戶驗證 已存在或者為空則不進行insert操作 if (!StringUtils.isEmpty(adminUser.getUserName()) && !StringUtils.isEmpty(adminUser.getPassword()) && selectusername(adminUser.getUserName()) == null) { adminUsers.add(adminUser); } } } //判空 if (!CollectionUtils.isEmpty(adminUsers)) { //adminUsers用戶列表不為空則執行批量添加sql return adminUserDao.addExcel(adminUsers); } return 0; }
Excel類型的工具類
package com.ssm.demo.utils; import org.apache.poi.hssf.usermodel.HSSFDataFormat; import org.apache.poi.xssf.usermodel.*; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * author:13 * date:2018-07 */ public class PoiUtil { //excel默認寬度; private static int width = 512 * 14; //默認字體 private static String excelfont = "微軟雅黑"; /** * @param excelName 導出的EXCEL名字 * @param headers 導出的表格的表頭 * @param fileds 導出的數據 map.get(key) 對應的 key * @param formators 導出數據的樣式 * @param widths 表格的列寬度 默認為 512 * 14 * @param data 數據集 List<Map> * @param response * @throws IOException */ public static void exportFile(String excelName, String[] headers, String[] fileds, int[] formators, int[] widths, List<Map<String, Object>> data, HttpServletRequest request, HttpServletResponse response) throws IOException { if (widths == null) { widths = new int[fileds.length]; for (int i = 0; i < fileds.length; i++) { widths[i] = width; } } if (formators == null) { formators = new int[fileds.length]; for (int i = 0; i < fileds.length; i++) { formators[i] = 1; } } //設置文件名 String fileName = "導出數據"; if (!StringUtils.isEmpty(excelName)) { fileName = excelName; } //創建工作薄 XSSFWorkbook wb = new XSSFWorkbook(); //創建sheet XSSFSheet sheet = wb.createSheet("sheet1"); //創建表頭,沒有則跳過此步驟 int headerrow = 0; if (headers != null) { XSSFRow row = sheet.createRow(headerrow); //表頭樣式 XSSFCellStyle style = wb.createCellStyle(); XSSFFont font = wb.createFont(); font.setBoldweight(XSSFFont.BOLDWEIGHT_BOLD); font.setFontName(excelfont); font.setFontHeightInPoints((short) 11); style.setFont(font); style.setAlignment(XSSFCellStyle.ALIGN_CENTER); style.setBorderBottom(XSSFCellStyle.BORDER_THIN); style.setBorderLeft(XSSFCellStyle.BORDER_THIN); style.setBorderRight(XSSFCellStyle.BORDER_THIN); style.setBorderTop(XSSFCellStyle.BORDER_THIN); for (int i = 0; i < headers.length; i++) { sheet.setColumnWidth((short) i, (short) widths[i]); XSSFCell cell = row.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(style); } headerrow++; } //表格主體 if (data != null) { List styleList = new ArrayList(); //列數 for (int i = 0; i < fileds.length; i++) { XSSFCellStyle style = wb.createCellStyle(); XSSFFont font = wb.createFont(); font.setFontName(excelfont); font.setFontHeightInPoints((short) 10); style.setFont(font); style.setBorderBottom(XSSFCellStyle.BORDER_THIN); style.setBorderLeft(XSSFCellStyle.BORDER_THIN); style.setBorderRight(XSSFCellStyle.BORDER_THIN); style.setBorderTop(XSSFCellStyle.BORDER_THIN); if (formators[i] == 1) { style.setAlignment(XSSFCellStyle.ALIGN_LEFT); } else if (formators[i] == 2) { style.setAlignment(XSSFCellStyle.ALIGN_CENTER); } else if (formators[i] == 3) { style.setAlignment(XSSFCellStyle.ALIGN_RIGHT); } else if (formators[i] == 4) { //int類型 style.setAlignment(XSSFCellStyle.ALIGN_RIGHT); style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0")); } else if (formators[i] == 5) { //float類型 style.setAlignment(XSSFCellStyle.ALIGN_RIGHT); style.setDataFormat(HSSFDataFormat.getBuiltinFormat("#,##0.00")); } else if (formators[i] == 6) { //百分比類型 style.setAlignment(XSSFCellStyle.ALIGN_RIGHT); style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%")); } styleList.add(style); } for (int i = 0; i < data.size(); i++) { //行數 XSSFRow row = sheet.createRow(headerrow); Map map = data.get(i); for (int j = 0; j < fileds.length; j++) { //列數 XSSFCell cell = row.createCell(j); Object o = map.get(fileds[j]); if (o == null || "".equals(o)) { cell.setCellValue(""); } else if (formators[j] == 4) { //int cell.setCellValue((Long.valueOf((map.get(fileds[j])) + "")).longValue()); } else if (formators[j] == 5 || formators[j] == 6) { //float cell.setCellValue((Double.valueOf((map.get(fileds[j])) + "")).doubleValue()); } else { cell.setCellValue(map.get(fileds[j]) + ""); } cell.setCellStyle((XSSFCellStyle) styleList.get(j)); } headerrow++; } } //文件名+excel格式"xlsx" fileName = fileName + ".xlsx"; String filename = ""; try { filename = encodeChineseDownloadFileName(request, fileName); } catch (Exception e) { e.printStackTrace(); } response.setHeader("Content-disposition", filename); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-disposition", "attachment;filename=" + filename); response.setHeader("Pragma", "No-cache"); OutputStream ouputStream = response.getOutputStream(); wb.write(ouputStream); ouputStream.flush(); ouputStream.close(); } /** * 對文件流輸出下載的中文文件名進行編碼以屏蔽各種瀏覽器版本的差異性 * * @throws UnsupportedEncodingException */ public static String encodeChineseDownloadFileName( HttpServletRequest request, String pFileName) throws Exception { String filename = null; String agent = request.getHeader("USER-AGENT"); if (null != agent) { if (-1 != agent.indexOf("Firefox")) {//Firefox filename = "=?UTF-8?B?" + (new String(org.apache.commons.codec.binary.Base64.encodeBase64(pFileName.getBytes("UTF-8")))) + "?="; } else if (-1 != agent.indexOf("Chrome")) {//Chrome filename = new String(pFileName.getBytes(), "ISO8859-1"); } else {//IE7+ filename = java.net.URLEncoder.encode(pFileName, "UTF-8"); filename = filename.replace("+", "%20"); } } else { filename = pFileName; } return filename; } /** * 獲取sheet對象 * * @param file * @return */ public static XSSFSheet getXSSFSheet(File file) { InputStream is = null; XSSFWorkbook xssfWorkbook = null; try { is = new FileInputStream(file); xssfWorkbook = new XSSFWorkbook(is); } catch (IOException e) { return null; } //獲取工作表對象 XSSFSheet xssfSheet = xssfWorkbook.getSheetAt(0); return xssfSheet; } /** * 將單元格數據轉換為String * * @param cell * @return */ public static String getValue(XSSFCell cell) { String cellValue = ""; if (null != cell) { //判斷數據類型,防止報錯 switch (cell.getCellType()) { case XSSFCell.CELL_TYPE_NUMERIC: // 數字 DecimalFormat df = new DecimalFormat("0"); cellValue = df.format(cell.getNumericCellValue()); break; case XSSFCell.CELL_TYPE_STRING: // 字符串 cellValue = cell.getStringCellValue(); break; case XSSFCell.CELL_TYPE_BOOLEAN: // Boolean cellValue = cell.getBooleanCellValue() + ""; break; case XSSFCell.CELL_TYPE_FORMULA: // 公式 cellValue = cell.getCellFormula() + ""; break; case XSSFCell.CELL_TYPE_BLANK: // 空值 cellValue = ""; break; case XSSFCell.CELL_TYPE_ERROR: // 故障 cellValue = "非法字符"; break; default: cellValue = "未知類型"; break; } } return cellValue; } }
這里首先通過PoiUtil工具類中的getXSSFSheet()方法獲取XSSFSheet類型的表格對象。
然后創建一個集合,通過for循環遍歷,這里注意循環的時候i的初始值是1。
因為第一行是表名稱,第二行才是數據。
通過getRow方法獲取指定行的XSSFRow類型的對象。
再通過row.getCell()方法獲取第一列和第二列的用戶名和密碼,如果不為空,就通過PoiUtil.getValue()方法來獲取String類型的用戶名和密碼裝入AdminUser類型的對象再裝入創建的集合中。
還需要判斷一下添加的用戶名和數據庫是否重復,如果重復則不添加。
如果集合中的數據不為空,則通過持久層調用Mapper操作數據庫,將Excel表中的數據批量增加。
持久層
int addExcel(@RequestParam("adminUsers") List<AdminUser> adminUsers);
Mapper
<!--這樣批量插入可以返回成功插入的數量--> <insert id="addExcel" parameterType="AdminUser"> insert into tb_admin_user(user_name,password_md5) values <foreach collection="list" index="index" item="adminUser" open="" separator="," close=""> (#{adminUser.userName},#{adminUser.password}) </foreach> </insert>
如果返回的int類型大於0的話,就代表批量插入成功,我們把返回的插入數量返回給前端,在插入完成后給用戶一個插入多少條數據的提醒
效果