EasyExcel 輕鬆靈活讀取Excel內容

寫在前面

Java 後端程序員應該會遇到讀取 Excel 信息到 DB 等相關需求,腦海中可能忽然間想起 Apache POI 這個技術解決方案,可是當 Excel 的數據量很是大的時候,你也許發現,POI 是將整個 Excel 的內容所有讀出來放入到內存中,因此內存消耗很是嚴重,若是同時進行包含大數據量的 Excel 讀操做,很容易形成內存溢出問題html

但 EasyExcel 的出現很好的解決了 POI 相關問題,本來一個 3M 的 Excel 用 POI 須要100M左右內存, 而 EasyExcel 能夠將其下降到幾 M,同時再大的 Excel 都不會出現內存溢出的狀況,由於是逐行讀取 Excel 的內容 (老規矩,這裏不用過度關心下圖,腦海中有個印象便可,看完下面的用例再回看這個圖,就很簡單了)java

另外 EasyExcel 在上層作了模型轉換的封裝,不須要 cell 等相關操做,讓使用者更加簡單和方便,且看git

簡單讀

假設咱們 excel 中有如下內容:程序員

咱們須要新建 User 實體,同時爲其添加成員變量github

@Data
public class User {

	/** * 姓名 */
	@ExcelProperty(index = 0)
	private String name;

	/** * 年齡 */
	@ExcelProperty(index = 1)
	private Integer age;
}
複製代碼

你也許關注到了 @ExcelProperty 註解,同時使用了 index 屬性 (0 表明第一列,以此類推),該註解同時支持以「列名」name 的方式匹配,好比:web

@ExcelProperty("姓名")
private String name;
複製代碼

按照 github 文檔的說明:面試

不建議 index 和 name 同時用,要麼一個對象只用index,要麼一個對象只用name去匹配數據庫

  1. 若是讀取的 Excel 模板信息列固定,這裏建議以 index 的形式使用,由於若是用名字去匹配,名字重複,會致使只有一個字段讀取到數據,因此 index 是更穩妥的方式
  2. 若是 Excel 模板的列 index 常常有變化,那仍是選擇 name 方式比較好,不用常常性修改實體的註解 index 數值

因此你們能夠根據本身的狀況自行選擇編程

編寫測試用例 後端

EasyExcel 類中重載了不少個 read 方法,這裏不一一列舉說明,請你們自行查看;同時 sheet 方法也能夠指定 sheetNo,默認是第一個 sheet 的信息

上面代碼的 new UserExcelListener() 異常醒目,這也是 EasyExcel 逐行讀取 Excel 內容的關鍵所在,自定義 UserExcelListener 繼承 AnalysisEventListener

@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {

	/** * 批處理閾值 */
	private static final int BATCH_COUNT = 2;
	List<User> list = new ArrayList<User>(BATCH_COUNT);

	@Override
	public void invoke(User user, AnalysisContext analysisContext) {
		log.info("解析到一條數據:{}", JSON.toJSONString(user));
		list.add(user);
		if (list.size() >= BATCH_COUNT) {
			saveData();
			list.clear();
		}
	}

	@Override
	public void doAfterAllAnalysed(AnalysisContext analysisContext) {
		saveData();
		log.info("全部數據解析完成!");
	}

	private void saveData(){
		log.info("{}條數據,開始存儲數據庫!", list.size());
		log.info("存儲數據庫成功!");
	}
}
複製代碼

到這裏請回看文章開頭的 EasyExcel 原理圖,invoke 方法逐行讀取數據,對應的就是訂閱者 1;doAfterAllAnalysed 方法對應的就是訂閱者 2,這樣你理解了嗎?

打印結果:

從這裏能夠看出,雖然是逐行解析數據,但咱們能夠自定義閾值,完成數據的批處理操做,可見 EasyExcel 操做的靈活性

自定義轉換器

這是最基本的數據讀寫,咱們的業務數據一般不可能這麼簡單,有時甚至須要將其轉換爲程序可讀的數據

性別信息轉換

好比 Excel 中新增「性別」列,其性別爲男/女,咱們須要將 Excel 中的性別信息轉換成程序信息: 「1: 男;2:女」

首先在 User 實體中添加成員變量 gender:

@ExcelProperty(index = 2)
private Integer gender;
複製代碼

EasyExcel 支持咱們自定義 converter,將 excel 的內容轉換爲咱們程序須要的信息,這裏新建 GenderConverter,用來轉換性別信息

public class GenderConverter implements Converter<Integer> {

	public static final String MALE = "男";
	public static final String FEMALE = "女";

	@Override
	public Class supportJavaTypeKey() {
		return Integer.class;
	}

	@Override
	public CellDataTypeEnum supportExcelTypeKey() {
		return CellDataTypeEnum.STRING;
	}

	@Override
	public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
		String stringValue = cellData.getStringValue();
		if (MALE.equals(stringValue)){
			return 1;
		}else {
			return 2;
		}
	}

	@Override
	public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
		return null;
	}
}
複製代碼

上面程序的 Converter 接口的泛型是指要轉換的 Java 數據類型,與 supportJavaTypeKey 方法中的返回值類型一致

打開註解 @ExcelProperty 查看,該註解是支持自定義 Converter 的,因此咱們爲 User 實體添加 gender 成員變量,並指定 converter

/** * 性別 1:男;2:女 */
@ExcelProperty(index = 2, converter = GenderConverter.class)
private Integer gender;
複製代碼

來看運行結果:

數據按照咱們預期作出了轉換,從這裏也能夠看出,Converter 能夠一次定義處處是用的便利性

日期信息轉換

日期信息也是咱們常見的轉換數據,好比 Excel 中新增「出生年月」列,咱們要解析成 yyyy-MM-dd 格式,咱們須要將其進行格式化,EasyExcel 經過 @DateTimeFormat 註解進行格式化

在 User 實體中添加成員變量 birth,同時應用 @DateTimeFormat 註解,按照要求作格式化

/** * 出生日期 */
@ExcelProperty(index = 3)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private String birth;
複製代碼

來看運行結果:

若是這裏你指定 birth 的類型爲 Date,試試看,你獲得的結果是什麼?

到這裏都是以測試的方式來編寫程序代碼,做爲 Java Web 開發人員,尤爲在目前主流 Spring Boot 的架構下,因此如何實現 Web 方式讀取 Excel 的信息呢?

web 讀

簡單 Web

很簡單,只是將測試用例的關鍵代碼移動到 Controller 中便可,咱們新建一個 UserController,在其添加 upload 方法

@RestController
@RequestMapping("/users")
@Slf4j
public class UserController {
	@PostMapping("/upload")
	public String upload(MultipartFile file) throws IOException {
		EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener()).sheet().doRead();
		return "success";
	}
}
複製代碼

其實在寫測試用例的時候你也許已經發現,listener 是以 new 的形式做爲參數傳入到 EasyExcel.read 方法中的,這是不符合 Spring IoC 的規則的,咱們一般讀取 Excel 數據以後都要針對讀取的數據編寫一些業務邏輯的,而業務邏輯一般又會寫在 Service 層中,咱們如何在 listener 中調用到咱們的 service 代碼呢?

**先不要向下看,你腦海中有哪些方案呢? **

匿名內部類方式

匿名內部類是最簡單的方式,咱們須要先新建 Service 層的信息: 新建 IUser 接口:

public interface IUser {
	public boolean saveData(List<User> users);
}
複製代碼

新建 IUser 接口實現類 UserServiceImpl:

@Service
@Slf4j
public class UserServiceImpl implements IUser {
	@Override
	public boolean saveData(List<User> users) {
		log.info("UserService {}條數據,開始存儲數據庫!", users.size());
		log.info(JSON.toJSONString(users));
		log.info("UserService 存儲數據庫成功!");
		return true;
	}
}
複製代碼

接下來,在 Controller 中注入 IUser:

@Autowired
private IUser iUser;
複製代碼

修改 upload 方法,以匿名內部類重寫 listener 方法的形式來實現:

@PostMapping("/uploadWithAnonyInnerClass")
	public String uploadWithAnonyInnerClass(MultipartFile file) throws IOException {
		EasyExcel.read(file.getInputStream(), User.class, new AnalysisEventListener<User>(){
			/** * 批處理閾值 */
			private static final int BATCH_COUNT = 2;
			List<User> list = new ArrayList<User>();

			@Override
			public void invoke(User user, AnalysisContext analysisContext) {
				log.info("解析到一條數據:{}", JSON.toJSONString(user));
				list.add(user);
				if (list.size() >= BATCH_COUNT) {
					saveData();
					list.clear();
				}
			}

			@Override
			public void doAfterAllAnalysed(AnalysisContext analysisContext) {
				saveData();
				log.info("全部數據解析完成!");
			}

			private void saveData(){
				iUser.saveData(list);
			}
		}).sheet().doRead();
		return "success";
	}
複製代碼

查看結果:

這種實現方式,其實這只是將 listener 中的內容所有重寫,並在 controller 中展示出來,當你看着這麼臃腫的 controller 是否是很是難受?很顯然這種方式不是咱們的最佳編碼實現

構造器傳參

在以前分析 SpringBoot 統一返回源碼時,不知道你是否發現,Spring 底層源碼多數以構造器的形式傳參,因此咱們能夠將爲 listener 添加有參構造器,將 Controller 中依賴注入的 IUser 以構造器的形式傳入到 listener :

@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {

	private IUser iUser;

	public UserExcelListener(IUser iUser){
		this.iUser = iUser;
	}

    // 省略相應代碼...

    private void saveData(){
		iUser.saveData(list); //調用 userService 中的 saveData 方法
	}
	
複製代碼

更改 Controller 方法:

@PostMapping("/uploadWithConstructor")
public String uploadWithConstructor(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(iUser)).sheet().doRead();
    return "success";
}
複製代碼

運行結果: 同上

這樣更改後,controller 代碼看着很清晰,但若是後續業務還有別的 Service 須要注入,咱們難道要一直添加有參構造器嗎?很明顯,這種方式一樣不是很靈活。

其實在使用匿名內部類的時候,你也許會想到,咱們能夠經過 Java8 lambda 的方式來解決這個問題

Lambda 傳參

爲了解決構造器傳參的痛點,同時咱們又但願 listener 更具備通用性,不必爲每一個 Excel 業務都新建一個 listener,由於 listener 都是逐行讀取 Excel 數據,只須要將咱們的業務邏輯代碼傳入給 listener 便可,因此咱們需用到 Consumer<T> ,將其做爲構造 listener 的參數。

新建一個工具類 ExcelDemoUtils,用來構造 listener:

咱們看到,getListener 方法接收一個 Consumer<List<T>> 的參數,這樣下面代碼被調用時,咱們的業務邏輯也就會被相應的執行了:

consumer.accept(linkedList);
複製代碼

繼續改造 Controller 方法:

運行結果: 同上

到這裏,咱們只須要將業務邏輯定製在 batchInsert 方法中:

  1. 知足 Controller RESTful API 的簡潔性
  2. listener 更加通用和靈活,它更可能是扮演了抽象類的角色,具體的邏輯交給抽象方法的實現來完成
  3. 業務邏輯可擴展性也更好,邏輯更加清晰

總結

到這裏,關於如何使用 EasyExcel 讀取 Excel 信息的基本使用方式已經介紹完了,還有不少細節內容沒有講,你們能夠自行查閱 EasyExcel Github 文檔去發現更多內容。靈活使用 Java 8 的函數式接口,更容易讓你提升代碼的複用性,同時看起來更簡潔規範

除了讀取 Excel 的讀取,還有 Excel 的寫入,若是須要將其寫入到指定位置,配合 HuTool 的工具類 FileWriter 的使用是很是方便的,針對 EasyExcel 的使用,若是你們有什麼問題,也歡迎到博客下方探討

完整代碼請在公衆號回覆「demo」,點開連接,查看「easy-excel-demo」文件夾的內容便可,另外我的博客因爲特殊緣由暫時關閉首頁,其餘目錄訪問一切正常,更多文章能夠從 dayarch.top/archives 入口查看

感謝

很是感謝 EasyExcel 的做者 🌹🌹,讓 Excel 的讀寫更加方便

靈魂追問

  1. 除了 Consumer,若是須要返回值的業務邏輯,須要用到哪一個函數式接口呢?
  2. 當出現複雜表頭的時候要如何處理呢?
  3. 將 DB 數據寫入到 Excel 並下載,如何實現呢?
  4. 從 EasyExcel 的設計上,你學到了什麼,歡迎博客下方留言討論

提升效率工具


推薦閱讀

  1. 此次走進併發的世界,請不要錯過
  2. 學併發編程,透徹理解這三個核心是關鍵
  3. 併發Bug之源有三,請睜大眼睛看清它們
  4. 可見性有序性,Happens-before來搞定
  5. 解決原子性問題?你首先須要的是宏觀理解
  6. 面試併發volatile關鍵字時,咱們應該具有哪些談資?

歡迎持續關注公衆號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


2019.10.24 補充說明

看到評論區有童鞋針對Lambda方式有一些困惑,因而單獨補充說明於此

很是坦誠的說,這種書寫方式一開始我也不會,多虧了朋友的點播,在此先感謝。

Lambda的誕生大大方便了咱們集合的處理,咱們天天處理集合,也就容易讓咱們忽略Lambda 函數式接口出發點是解決了匿名內部類的代碼臃腫問題,好比教程中常被說起的() -> System.out.println("hello world");

public void testRunnable(Runnable runnable){
    runnable.run();
}

@Test
public void callTestRunnable1(){
    testRunnable(new Runnable() {
        @Override
        public void run() {
            System.out.println("hello Runnable");
        }
    });
}

@Test
public void callTestRunnable(){
    testRunnable(() -> System.out.println("hello Runnable"));
}
複製代碼

到這裏有朋友可能會說怎麼將 AnalysisEventListener 匿名內部類轉換爲 Lambda 方式呢?答案是不能, 這違背了函數式接口的定義(只有一個抽象方法,其餘都爲 default),回看上面的代碼,我只是把匿名內部類的方式放到了工具類 ExcelDemoUtils 裏面,這種方式較傳統方式有了進步,咱們使用了 Java 泛型,除了 User 實體,咱們能夠處理任何實體,請看代碼:

public static <T> AnalysisEventListener<T> getListener() {
    return new AnalysisEventListener<T>() {
        private static final int BATCH_COUNT = 2;
        List<T> list = new ArrayList<T>();

        @Override
        public void invoke(T user, AnalysisContext analysisContext) {
            log.info("解析到一條數據:{}", JSON.toJSONString(user));
            list.add(user);
            if (list.size() >= BATCH_COUNT) {
                saveData();
                list.clear();
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
            saveData();
            log.info("全部數據解析完成!");
        }

        private void saveData(){
            log.info("{}條數據,開始存儲數據庫!", list.size());
            log.info("存儲數據庫成功!");
        }
    };
}
複製代碼

這裏寫的工具類不具有業務落地的可能性,一般咱們要將讀取到的 Excel 內容 (list) 執行特定業務邏輯最終持久化到 DB,這句話按照 Lambda 翻譯過來就是(list) -> 特定業務邏輯的執行

這正好匹配 Consumer 函數式接口的 accept(T t) 方法,因此咱們爲上面的 getListener 方法添加 Consumer 類型的參數就行了,這樣調用 getListener 方法咱們須要傳遞 Consumer 類型的參數,當執行 consumer.accept 方法時,就會執行咱們寫的 batchInsert 方法裏面的業務邏輯

因此說,當基本執行邏輯固定,只有局部須要特定業務處理的,咱們均可以使用函數式接口來傳參數,記住這個思惟定式就好

歡迎關注上面二維碼公衆號,將技術落地,趣談Coding那些事,最後1024節日快樂

相關文章
相關標籤/搜索