簡介
- 本篇講述
EasyPOI
的最基本使用,即導出和導入Excel
文檔。
關於概念
- 關於
EasyPOI
的概念不再累贅,我們需要知道的所有東西就是,它可以幫助我們把集合對象pojo
轉換為表格Excel
。
- 本章采用的是客戶端上傳和下載
Excel
文檔作為范例,這也是最為頻繁的使用場景或需求。
主要步驟
- 數據庫表結構與
Excel
表結構是一致的,對於導入的情況,EasyPOI
所需要做的事情就讀取表格,將表格轉為pojo
的集合類對象,之后調用Service
層中的方法寫入數據庫即可。
- 對於導出的情況,則是先從
Service
層中讀取數據,之后通過EasyPOI
寫出到文件即可。
- 相對於導出操作而言,導入操作的要求實際上會更為嚴苛,其中涉及到數據合法性問題。
- 務必使用
JSR-303
或其實現Hibernate Validator
中的注解,來校驗導入數據的合法性。否則寫入數據庫的數據可能會出現各種問題,本文不會對校驗數據合法性進行測試。
核心代碼
- 其中的核心代碼是
Controller
中的上傳寫入數據庫,以及下載從數據庫取出數據轉換為Excel
。
- 我們將分兩部分快速講解。
package cn.dylanphang.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.dylanphang.pojo.Student;
import cn.dylanphang.service.StudentService;
import cn.dylanphang.util.ReturnClassUtils;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
/**
* @author dylan
*/
@Controller
@RequestMapping("/file")
@SessionAttributes("lastRetrieve")
public class StudentController {
private final StudentService studentService;
@Autowired
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@RequestMapping("/upload")
public String upload(MultipartFile upload) throws Exception {
// 1.解析文檔需要用到ImportParams對象
final ImportParams importParams = new ImportParams();
importParams.setHeadRows(1);
// 2.使用ExcelImportUtil工具類獲取Excel表格中的內容,並轉為List<T>,實際情況下我們需要知道上傳的表格是什么類型的
// *.因為所有的pojo都是按照用戶需求創建的,擁有模板,上傳的數據必須符合這些模板
// *.那么實際開發中,可以通過訪問的url判斷本次上傳的文件是屬於什么類型的pojo
final List<Student> students = ExcelImportUtil.importExcel(
upload.getInputStream(),
Student.class,
importParams
);
// 3.寫入數據庫,其中如果數據格式不符合,在上一步轉換的時候會報錯
this.studentService.saveAll(students);
return "upload-success";
}
@SuppressWarnings("all")
@RequestMapping("/download")
public ResponseEntity<byte[]> download(ModelMap modelMap, HttpServletRequest request) throws IOException {
// 1.獲取用戶的最后一次篩選查詢的結果集
final Object lastRetrieve = modelMap.getAttribute("lastRetrieve");
// *.判斷是否存在最后一次查詢
if (lastRetrieve != null && lastRetrieve instanceof List) {
// 2.pojo是根據用戶需求去創建的,使用自定義工具類返回集合中字節碼的文件信息
final Collection<?> retrieve = (Collection<? extends Object>) lastRetrieve;
final Class<?> targetClass = ReturnClassUtils.getReturnClass(retrieve);
// 3.獲取存儲文件的目錄路徑
String path = request.getSession().getServletContext().getRealPath("/files");
// 4.根據路徑創建file對象
File file = new File(path);
// 5.如果當前路徑下不存在/files目錄,則創建一個/files目錄
if (!file.exists()) {
final boolean mkdir = file.mkdir();
if (!mkdir) {
throw new RuntimeException("Create new folder failure.");
}
}
// 6.easypoi部分,獲取Workbook對象,創建UUID別名並寫入硬盤
final Workbook sheets = ExcelExportUtil.exportExcel(new ExportParams(), targetClass, retrieve);
final String name = UUID.randomUUID().toString().replaceAll("-", "") + ".xls";
final FileOutputStream fs = new FileOutputStream(new File(file, name));
sheets.write(fs);
fs.close();
// 7.獲取該.xls文件,並獲得它的輸入流
file = new File(path + "/" + name);
final FileInputStream is = new FileInputStream(file);
// 8.使用輸入流創建指定大小的byte[]數組,並將字節碼存入byte[]數組中
byte[] body = new byte[is.available()];
is.read(body);
is.close();
// 9.需要指定相應體中指定頭的值是attchement
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-Disposition", "attchement;filename=" + file.getName());
// 10.最后使用ResponseEntity封裝字節碼、響應體即狀態碼信息,並返回此對象引用
return new ResponseEntity<>(body, httpHeaders, HttpStatus.OK);
} else {
return null;
}
}
@RequestMapping("/query")
public ModelAndView hello() {
final ModelAndView mv = new ModelAndView("test");
final List<Student> students = this.studentService.findAll();
// *.將用戶的最后一次篩選查詢存入當前用戶的session域中
mv.addObject("lastRetrieve", students);
return mv;
}
}
- 對應的
pojo
需要添加注解@Excel
,將作為表格的字段名:
package cn.dylanphang.pojo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author dylan
*/
@Data
public class Student {
@Excel(name = "id")
private Integer id;
@Excel(name = "name")
private String name;
@Excel(name = "age")
private Integer age;
@Excel(name = "birthday", format = "yyyy-MM-dd")
private Date birthday;
public String getDisplayDate() {
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
return simpleDateFormat.format(this.birthday);
}
}
上傳文件
- 上傳文件會比較通俗簡單,甚至比
Spring MVC
原生上傳文件還要簡單,因為我們不需要將獲取到的字節碼文件存儲在本地,而是直接可用Excel
的字節碼文件作為轉換工具的參數,將Excel
表格變成一個List<T>
集合。
- 以下是上次文件的
@RequestMapping
方法:
@RequestMapping("/upload")
public String upload(MultipartFile upload) throws Exception {
// 1.解析文檔需要用到ImportParams對象
final ImportParams importParams = new ImportParams();
importParams.setHeadRows(1);
// 2.使用ExcelImportUtil工具類獲取Excel表格中的內容,並轉為List<T>,實際情況下我們需要知道上傳的表格是什么類型的
// *.因為所有的pojo都是按照用戶需求創建的,擁有模板,上傳的數據必須符合這些模板
// *.那么實際開發中,可以通過訪問的url判斷本次上傳的文件是屬於什么類型的pojo
final List<Student> students = ExcelImportUtil.importExcel(
upload.getInputStream(),
Student.class,
importParams
);
// 3.寫入數據庫,其中如果數據格式不符合,在上一步轉換的時候會報錯
this.studentService.saveAll(students);
return "upload-success";
}
- 使用
EasyPOI
中的兩個對象即可完成將Excel
文檔轉換為List<T>
集合的過程。而通常情況下,List<T>
也經常作為Mapper
方法中的參數,因此可以無縫銜接數據庫寫入。
- 其中最重要的是
ExcelImportUtil
工具類,它接收三個參數:
InputStream
:直接通過MultipartFile
對象獲取;
Class<T>
:根據實際上傳表格對應的pojo
類進行輸入,可以在前端隱式入參該表格的類名;
ImportParams
:自行創建並設置,一般表格模板是固定的,那么也就是說ImportParams
也是固定的。
- 過多的關於上傳文件的代碼不再贅述,下面講一下我認為的可能遇到的設計難點和解決思路:
- 關於當前上傳表格的類型識別,你幾乎無法得知當前上傳的表格是屬於哪一個
pojo
類的,只有在ExcelImportUtil
報錯的時候,你才會發現傳入的Class<T>
有問題:
- 因此實際開發中,你必須有效地識別用戶上傳的文件歸屬於哪一個
pojo
,其中一個方法是通過url
識別,用戶上傳文件的鏈接通過設置不同的url
,可以定向訪問指定的@RequestMapping
方法;
- 或者通過
RestFul
風格,這兩種方法都是通過前端參數的設置來給后端傳遞信息的。
- 關於導入參數
ImportParams
的設置:
- 理論上,你需要保證用戶上傳的文件是符合項目中所設置的
pojo
類的,因此你需要為你的表格提供模板;
- 用戶需要通過模板進行信息的填寫,對數據有效性的驗證大多數可以在
ImportParams
中完成,不再贅述。
下載文件
- 下載文件的測試,將假設下載文件是會緊跟在查詢某一個數據之后發生的,那么可以很輕松地通過
Session
的方式,保存用戶最后一次查詢的數據。一旦用戶點擊下載,那么下載的數據一定是用戶最后一次查詢的數據,從Session
取數據即可。
- 當然實際項目中,最好不要進行如下的判斷。你無法避免用戶在同一個瀏覽器中進行多頁面打開並查詢數據,此時就沒有辦法保證用戶需要下載的文件是不是最后一次查詢的文件,如果沿用以上邏輯,服務器將始終提供用戶最后一次查詢數據的下載。
EasyPOI
關於下載文件的操作,稍微比較復雜一點。
- 需要首先將從數據庫獲取到的
List<T>
轉換為本地的Excel
文件導出到本地;
- 然后將本地文件作為
attchement
返回給客戶端。
@RequestMapping("/query")
public ModelAndView hello() {
final ModelAndView mv = new ModelAndView("test");
final List<Student> students = this.studentService.findAll();
// *.將用戶的最后一次篩選查詢存入當前用戶的session域中
mv.addObject("lastRetrieve", students);
return mv;
}
@SuppressWarnings("all")
@RequestMapping("/download")
public ResponseEntity<byte[]> download(ModelMap modelMap, HttpServletRequest request) throws IOException {
// 1.獲取用戶的最后一次篩選查詢的結果集
final Object lastRetrieve = modelMap.getAttribute("lastRetrieve");
// *.判斷是否存在最后一次查詢
if (lastRetrieve != null && lastRetrieve instanceof List) {
// 2.pojo是根據用戶需求去創建的,使用自定義工具類返回集合中字節碼的文件信息
final Collection<?> retrieve = (Collection<? extends Object>) lastRetrieve;
final Class<?> targetClass = ReturnClassUtils.getReturnClass(retrieve);
// 3.獲取存儲文件的目錄路徑
String path = request.getSession().getServletContext().getRealPath("/files");
// 4.根據路徑創建file對象
File file = new File(path);
// 5.如果當前路徑下不存在/files目錄,則創建一個/files目錄
if (!file.exists()) {
final boolean mkdir = file.mkdir();
if (!mkdir) {
throw new RuntimeException("Create new folder failure.");
}
}
// 6.easypoi部分,獲取Workbook對象,創建UUID別名並寫入硬盤
final Workbook sheets = ExcelExportUtil.exportExcel(new ExportParams(), targetClass, retrieve);
final String name = UUID.randomUUID().toString().replaceAll("-", "") + ".xls";
final FileOutputStream fs = new FileOutputStream(new File(file, name));
sheets.write(fs);
fs.close();
// 7.獲取該.xls文件,並獲得它的輸入流
file = new File(path + "/" + name);
final FileInputStream is = new FileInputStream(file);
// 8.使用輸入流創建指定大小的byte[]數組,並將字節碼存入byte[]數組中
byte[] body = new byte[is.available()];
is.read(body);
is.close();
// 9.需要指定相應體中指定頭的值是attchement
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-Disposition", "attchement;filename=" + file.getName());
// 10.最后使用ResponseEntity封裝字節碼、響應體即狀態碼信息,並返回此對象引用
return new ResponseEntity<>(body, httpHeaders, HttpStatus.OK);
} else {
return null;
}
}
- 其中的難點其實是獲取文件存放路徑上,我們使用以下的代碼獲取目錄路徑,並獲取它的
File
對象,如果目錄不存在,則創建該目錄:
String path = request.getSession().getServletContext().getRealPath("/files");
File file = new File(path);
if (!file.exists()) {
final boolean mkdir = file.mkdir();
if (!mkdir) {
throw new RuntimeException("Create new folder failure.");
}
}
- 此測試項目是使用
SpringBoot
創建的,此時目錄的真實路徑是target/{項目名稱}/files
,這樣說可能不太清晰,詳細情況請看結構圖,如下:

files
目錄真實存在於已編譯項目下。真實部署環境下,並未測試過是否可行,因此關於真實創建的目錄所在位置,需要以實際項目為准。過多的關於下載文件的代碼不再贅述,下面講一下我認為的可能遇到的設計難點和解決思路:
- 如何確定用戶想要導出的是什么類型?
- 由於初步接觸
EasyPOI
,我並不知道是否可以導出一些自定義的表格數據,因為ExcelExportUtil
工具類總是要求編寫者需要輸入一個表示當前表格的pojo
類的Class
對象;
- 假設用戶導出的表格都是提前設計並規范好的表格,此時我們需要知道用戶導出
Collection<T>
對象中的T
是什么,在本例中我創建了一個ReturnClassUtils
做基礎的校驗,實際上ReturnClassUtils
應該具備校驗所有pojo
類的功能。
ReturnClassUtils
代碼如下:
package cn.dylanphang.util;
import cn.dylanphang.pojo.Student;
import java.util.Collection;
import java.util.Iterator;
/**
* @author dylan
*/
public class ReturnClassUtils {
public static <T> Class<?> getReturnClass(Collection<T> collection) {
final Iterator<T> iterator = collection.iterator();
if (iterator.hasNext() && iterator.next() instanceof Student) {
return Student.class;
} else {
return null;
}
}
}
- 在導出類型都是設計者規定好的前提下,用戶如果需要導出,如何獲取用戶正在瀏覽的表格是什么?
- 同樣地,如果導出類型是固定的,那么我們可以簡單地根據
url
進行判斷。
總結
- 關於
EasyPOI
的基本使用已介紹完畢,更多功能可以前往EasyPOI
查看。