背景
老項目主要採用的POI框架來進行Excel數據的導入和導出,但經常會出現OOM的情況,導致整個服務不可用。後續逐步轉移到EasyExcel,簡直不能太好用了。
EasyExcel是阿里巴巴開源插件之一,主要解決了poi框架使用複雜,sax解析模式不容易操作,數據量大起來容易OOM,解決了POI併發造成的報錯。主要解決方式:通過解壓文件的方式加載,一行一行地加載,並且拋棄樣式字體等不重要的數據,降低內存的佔用。
在之前專門寫過一篇文章《EasyExcel太方便易用了,強烈推薦!》,介紹EasyExcel功能的基本使用。今天這篇文章,我們基於SpringBoot來實現一下EasyExcel的集成,更加方便大家在實踐中的直接使用。
SpringBoot項目集成
依賴集成
創建一個基礎的SpringBoot項目,比如這裏採用SpringBoot 2.7.2版本。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
EasyExcel在SpringBoot的集成非常方便,只需引入對應的pom依賴即可。在上述dependencies中添加EasyExcel的依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.11</version>
</dependency>
EasyExcel目前穩定最新版本2.2.11。如果想查看開源項目或最新版本,可在GitHub上獲得:https://github.com/alibaba/easyexcel。
爲了方便和簡化代碼編寫,這裏同時引入了Lombok的依賴,後續代碼中也會使用對應的註解。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
下面正式開始業務相關代碼的編寫。如果你想直接獲得完整源碼,對照源碼閱讀本篇文章,可在公號「程序新視界」內回“1007”獲得完整源碼。
實體類實現
這裏創建一個Member,會員的實體類,並在實體類中填寫基礎的個人信息。
@Data
public class Member {
/**
* EasyExcel使用:導出時忽略該字段
*/
@ExcelIgnore
private Integer id;
@ExcelProperty("用戶名")
@ColumnWidth(20)
private String username;
/**
* EasyExcel使用:日期的格式化
*/
@ColumnWidth(20)
@ExcelProperty("出生日期")
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
/**
* EasyExcel使用:自定義轉換器
*/
@ColumnWidth(10)
@ExcelProperty(value = "性別", converter = GenderConverter.class)
private Integer gender;
}
爲了儘量多的演示EasyExcel的相關功能,在上述實體類中使用了其常見的一些註解:
- @ExcelIgnore:忽略掉該字段;
- @ExcelProperty(“用戶名”):設置該列的名稱爲”用戶名“;
- @ColumnWidth(20):設置表格列的寬度爲20;
- @DateTimeFormat(“yyyy-MM-dd”):按照指定的格式對日期進行格式化;
- @ExcelProperty(value = “性別”, converter = GenderConverter.class):自定義內容轉換器,類似枚舉的實現,將“男”、“女”轉換成“0”、“1”的數值。
GenderConverter轉換器的代碼實現如下:
public class GenderConverter implements Converter<Integer> {
private static final String MAN = "男";
private static final String WOMAN = "女";
@Override
public Class<?> supportJavaTypeKey() {
// 實體類中對象屬性類型
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
// Excel中對應的CellData屬性類型
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty,
GlobalConfiguration globalConfiguration) {
// 從Cell中讀取數據
String gender = cellData.getStringValue();
// 判斷Excel中的值,將其轉換爲預期的數值
if (MAN.equals(gender)) {
return 0;
} else if (WOMAN.equals(gender)) {
return 1;
}
return null;
}
@Override
public CellData<?> convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty,
GlobalConfiguration globalConfiguration) {
// 判斷實體類中獲取的值,轉換爲Excel預期的值,並封裝爲CellData對象
if (integer == null) {
return new CellData<>("");
} else if (integer == 0) {
return new CellData<>(MAN);
} else if (integer == 1) {
return new CellData<>(WOMAN);
}
return new CellData<>("");
}
}
不同版本中,convertToJavaData和convertToExcelData的方法參數有所不同,對應的值的獲取方式也不同,大家在使用時注意對照自己的版本即可。
業務邏輯實現
爲方便驗證功能,DAO層的邏輯便不再實現,直接通過Service層來封裝數據,先來看導出功能的業務類實現。
MemberService實現
定義MemberService接口:
public interface MemberService {
/**
* 獲取所有的成員信息
* @return 成員信息列表
*/
List<Member> getAllMember();
}
定義MemberServiceImpl實現類:
@Service("memberService")
public class MemberServiceImpl implements MemberService {
@Override
public List<Member> getAllMember() {
// 這裏構造一些測試數據,具體業務場景可從數據庫等其他地方獲取
List<Member> list = new ArrayList<>();
Member member = new Member();
member.setUsername("張三");
member.setBirthday(getDate(1990, 10, 11));
member.setGender(0);
list.add(member);
Member member1 = new Member();
member1.setUsername("王紅");
member1.setBirthday(getDate(1999, 3, 29));
member1.setGender(1);
list.add(member1);
Member member2 = new Member();
member2.setUsername("李四");
member2.setBirthday(getDate(2000, 2, 9));
member2.setGender(0);
list.add(member2);
return list;
}
private Date getDate(int year, int month, int day) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, month, day);
return calendar.getTime();
}
}
其中數據採用模擬的靜態數據,返回Member列表。
簡單導出實現
在Controller層的實現一個簡單的導出實現:
/**
* 普通導出方式
*/
@RequestMapping("/export1")
public void exportMembers1(HttpServletResponse response) throws IOException {
List<Member> members = memberService.getAllMember();
// 設置文本內省
response.setContentType("application/vnd.ms-excel");
// 設置字符編碼
response.setCharacterEncoding("utf-8");
// 設置響應頭
response.setHeader("Content-disposition", "attachment;filename=demo.xlsx");
EasyExcel.write(response.getOutputStream(), Member.class).sheet("成員列表").doWrite(members);
}
這個實現方式非常簡單直接,使用EasyExcel的write方法將查詢到的數據進行處理,以流的形式寫出即可。
在瀏覽器訪問對應的鏈接,可下載到如下Excel內容:
如果我們需要將導出的Excel進行一些格式化的處理,這就需要用到導出策略的實現了。
自定義導入實現
在EasyExcel執行write方法之後,獲得ExcelWriterBuilder類,通過該類的registerWriteHandler方法可以設置一些處理策略。
這裏先實現一個通用的格式策略工具類CommonCellStyleStrategy:
public class CommonCellStyleStrategy {
/**
* 設置單元格樣式(僅用於示例)
*
* @return 樣式策略
*/
public static HorizontalCellStyleStrategy getHorizontalCellStyleStrategy() {
// 表頭策略
WriteCellStyle headerCellStyle = new WriteCellStyle();
// 表頭水平對齊居中
headerCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 背景色
headerCellStyle.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex());
WriteFont headerFont = new WriteFont();
headerFont.setFontHeightInPoints((short) 15);
headerCellStyle.setWriteFont(headerFont);
// 自動換行
headerCellStyle.setWrapped(Boolean.FALSE);
// 內容策略
WriteCellStyle contentCellStyle = new WriteCellStyle();
// 設置數據允許的數據格式,這裏49代表所有可以都允許設置
contentCellStyle.setDataFormat((short) 49);
// 設置背景色: 需要指定 FillPatternType 爲FillPatternType.SOLID_FOREGROUND 不然無法顯示背景顏色.頭默認了 FillPatternType所以可以不指定
contentCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
contentCellStyle.setFillForegroundColor(IndexedColors.GREY_40_PERCENT.getIndex());
// 設置內容靠左對齊
contentCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
// 設置字體
WriteFont contentFont = new WriteFont();
contentFont.setFontHeightInPoints((short) 12);
contentCellStyle.setWriteFont(contentFont);
// 設置自動換行
contentCellStyle.setWrapped(Boolean.FALSE);
// 設置邊框樣式和顏色
contentCellStyle.setBorderLeft(BorderStyle.MEDIUM);
contentCellStyle.setBorderTop(BorderStyle.MEDIUM);
contentCellStyle.setBorderRight(BorderStyle.MEDIUM);
contentCellStyle.setBorderBottom(BorderStyle.MEDIUM);
contentCellStyle.setTopBorderColor(IndexedColors.RED.getIndex());
contentCellStyle.setBottomBorderColor(IndexedColors.GREEN.getIndex());
contentCellStyle.setLeftBorderColor(IndexedColors.YELLOW.getIndex());
contentCellStyle.setRightBorderColor(IndexedColors.ORANGE.getIndex());
// 將格式加入單元格樣式策略
return new HorizontalCellStyleStrategy(headerCellStyle, contentCellStyle);
}
}
該類中示例設置了Excel的基礎格式。
再來實現一個精細化控制單元格內容CellWriteHandler的實現類:
/**
* 實現CellWriteHandler接口, 實現對單元格樣式的精確控制
*
* @author sec
* @version 1.0
* @date 2022/7/31
**/
public class CustomCellWriteHandler implements CellWriteHandler {
/**
* 創建單元格之前的操作
*/
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Head head, Integer integer, Integer integer1, Boolean aBoolean) {
}
/**
* 創建單元格之後的操作
*/
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
Head head, Integer integer, Boolean aBoolean) {
}
/**
* 單元格內容轉換之後的操作
*/
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
CellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
/**
* 單元格處理後(已寫入值)的操作
*/
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<CellData> list, Cell cell, Head head, Integer integer, Boolean isHead) {
// 設置超鏈接
if (isHead && cell.getRowIndex() == 0 && cell.getColumnIndex() == 0) {
CreationHelper helper = writeSheetHolder.getSheet().getWorkbook().getCreationHelper();
Hyperlink hyperlink = helper.createHyperlink(HyperlinkType.URL);
hyperlink.setAddress("https://github.com/alibaba/easyexcel");
cell.setHyperlink(hyperlink);
}
// 精確設置單元格格式
boolean bool = isHead && cell.getRowIndex() == 1;
if (bool) {
// 獲取工作簿
Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
CellStyle cellStyle = workbook.createCellStyle();
Font cellFont = workbook.createFont();
cellFont.setBold(Boolean.TRUE);
cellFont.setFontHeightInPoints((short) 14);
cellFont.setColor(IndexedColors.SEA_GREEN.getIndex());
cellStyle.setFont(cellFont);
cell.setCellStyle(cellStyle);
}
}
}
在這裏,對單元格表頭的第0個Cell設置了一個超鏈接。
通過上面的定義兩個策略實現,在導出Excel可以使用上述兩個策略實現:
/**
* 基於策略及攔截器導出
*/
@RequestMapping("/export2")
public void exportMembers2(HttpServletResponse response) throws IOException {
List<Member> members = memberService.getAllMember();
// 設置文本內省
response.setContentType("application/vnd.ms-excel");
// 設置字符編碼
response.setCharacterEncoding("utf-8");
// 設置響應頭
response.setHeader("Content-disposition", "attachment;filename=demo.xlsx");
EasyExcel.write(response.getOutputStream(), Member.class).sheet("成員列表")
// 註冊通用格式策略
.registerWriteHandler(CommonCellStyleStrategy.getHorizontalCellStyleStrategy())
// 設置自定義格式策略
.registerWriteHandler(new CustomCellWriteHandler())
.doWrite(members);
}
通過瀏覽器,訪問上述接口,導出的Excel格式如下:
可以看出,導出的Excel已經附帶了具體的格式。其中表頭“用戶名”上也攜帶了對應的超鏈接。其他更精細化的控制,大家可以在策略類中做進一步的控制。
同步獲取結果導入實現
所謂的同步獲取結果導入,就是執行導入操作時,將導入內容解析封裝成一個結果列表返回給業務,業務代碼再對列表中的數據進行集中的處理。
先來看同步導入的實現方式。
/**
* 從Excel導入會員列表
*/
@RequestMapping(value = "/import1", method = RequestMethod.POST)
@ResponseBody
public void importMemberList(@RequestPart("file") MultipartFile file) throws IOException {
List<Member> list = EasyExcel.read(file.getInputStream())
.head(Member.class)
.sheet()
.doReadSync();
for (Member member : list) {
System.out.println(member);
}
}
注意,在上述代碼中,最終調用的是doReadSync()方法。
這裏直接用PostMan進行相應的文件上傳請求:
執行導入請求,會發現控制檯打印出對應的解析對象:
Member(id=null, username=張三, birthday=Sun Nov 11 00:00:00 CST 1990, gender=0)
Member(id=null, username=王紅, birthday=Thu Apr 29 00:00:00 CST 1999, gender=1)
Member(id=null, username=李四, birthday=Thu Mar 09 00:00:00 CST 2000, gender=0)
說明上傳成功,並且解析成功。
基於監聽導入實現
上面示例中是基於同步獲取結果列表的形式進行導入,還有一種實現方式是基於監聽器的形式來實現。這種形式可以達到邊解析邊處理業務邏輯的效果。
定義Listener:
public class MemberExcelListener extends AnalysisEventListener<Member> {
@Override
public void invoke(Member member, AnalysisContext analysisContext) {
// do something
System.out.println("讀取Member=" + member);
// do something
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// do something
System.out.println("讀取Excel完畢");
// do something
}
}
在MemberExcelListener中可以針對每條數據進行對應的業務邏輯處理。
對外接口實現如下:
/**
* 基於Listener方式從Excel導入會員列表
*/
@RequestMapping(value = "/import2", method = RequestMethod.POST)
@ResponseBody
public void importMemberList2(@RequestPart("file") MultipartFile file) throws IOException {
// 方式一:同步讀取,將解析結果返回,比如返回List<Member>,業務再進行相應的數據集中處理
// 方式二:對照doReadSync()方法的是最後調用doRead()方法,不進行結果返回,而是在MemberExcelListener中進行一條條數據的處理;
// 此處示例爲方式二
EasyExcel.read(file.getInputStream(), Member.class, new MemberExcelListener()).sheet().doRead();
}
這裏採用了doRead()方法進行讀取操作。在PostMan中再次上傳Excel,打印日誌如下:
讀取Member=Member(id=null, username=張三, birthday=Sun Nov 11 00:00:00 CST 1990, gender=0)
讀取Member=Member(id=null, username=王紅, birthday=Thu Apr 29 00:00:00 CST 1999, gender=1)
讀取Member=Member(id=null, username=李四, birthday=Thu Mar 09 00:00:00 CST 2000, gender=0)
讀取Excel完畢
說明解析成功,並且在解析的過程中,進行了業務邏輯的處理。
小結
本篇文章基於SpringBoot集成EasyExcel的實現展開,爲大家講解了EasyExcel在實踐中的具體運用。大家可根據需要,進行變通處理。同時,基於自定義轉換器、自定義策略、自定義監聽器等形式達到靈活適用於各種場景。希望本篇文章能給大家帶來幫助。
博主簡介:《SpringBoot技術內幕》技術圖書作者,酷愛鑽研技術,寫技術乾貨文章。
公衆號:「程序新視界」,博主的公衆號,歡迎關注~
技術交流:請聯繫博主微信號:zhuan2quan
“ 程序新視界”,一個100%技術乾貨的公衆號
本文同步分享在 博客“程序新視界”(CSDN)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。