Spring Boot + EasyExcel導入導出,簡直太好用了!

背景

老項目主要採用的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已經附帶了具體的格式。其中表頭“用戶名”上也攜帶了對應的超鏈接。其他更精細化的控制,大家可以在策略類中做進一步的控制。

同步獲取結果導入實現

所謂的同步獲取結果導入,就是執行導入操作時,將導入內容解析封裝成一個結果列表返回給業務,業務代碼再對列表中的數據進行集中的處理。

先來看同步導入的實現方式。

	/**
	 * 從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進行相應的文件上傳請求:

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源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章