手把手教你如何玩轉EasyExcel的導入和導出

情景引入

小白:起牀起牀,快起牀!!!
我:小白,你又怎麼了,每次來都是這樣火急火燎,成年人了能不能沉穩一點呢?
小白:我要被需求給打敗了,因爲老是碰到這樣的功能;
我:咦,說說看,怎麼樣的需求把你折磨成這個樣子,還打擾了我的美夢;
小白:就是老是碰到前端頁面數據的導入和導出功能,而且還是用Excel格式的文件,可煩了。
我:這很正常呀,Excel的導入導出是常見的功能,特別是月末月初,各種統計表格來來回回。
小白:對呀,都要被各種Excel的模板給折磨瘋了,能不能告訴我一些常用的導入導出的方法呀,拯救一下我;
我:好吧好吧,看你這麼可憐的情面上,就給你科普科普,快去搬小板凳來吧!
小白:端端正正的坐好,等待上課!

EasyExcel的引入

對於EasyExcel,我想大家都不會太陌生,從它的名字就可以看出來,它就是爲了Excel文件而生。那麼,它到底是怎樣的一個東西呢?
Java領域解析、生成Excel比較有名的框架有Apache poi、jxl等。但他們都存在一個嚴重的問題就是非常的耗內存。如果你的系統併發量不大的話可能還行,但是一旦併發上來後一定會OOM或者JVM頻繁的full gc。
在這樣的一種場景下,EasyExcel就誕生了,其是阿里巴巴開源的一個excel處理框架,以使用簡單、節省內存著稱,因此,也受到了很多公司和個人的喜愛。

引入EasyExcel的依賴

建議項目採取Maven的方式,使用EasyExcel則添加如下的依賴即可:
PS:建議採取2.X的版本

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>2.0.5</version>
</dependency>

導出

**爲什麼要先說導出而不說導入呢?**其實,很簡單,我們在導入數據的時候,其實都有一定的Excel格式,而我們在解析的時候,都不是隨便解析一個Excel,而它肯定是包含有一定的格式。所以,既然要支持一定格式的Excel導入,那麼當然是要先給用戶提供一個 “數據模板”,否則,用戶怎麼知道要用什麼Excel的格式呢?因此,導出Excel的功能就自然要先說了。

普通樣式的模板導出

步驟

定義導出模板的實體

package com.hnu.scw.model;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:22 2020/3/16 0016
 * @ Description:普通樣式excel導出模板實體
 * @ Modified By:
 * @Version: $version$
 */
@Data
public class ExportMouldDto implements Serializable{

    @ExcelProperty(index = 0, value = "年齡")
    private Integer age;

    @ExcelProperty(index = 1, value = "名字")
    private String name;

    @ExcelProperty(index = 2, value = "性別")
    private String sex;

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

PS:其中的還有很多註解這個就不多說了,如果不明白的可以百度看看哦。

編寫導出excel的工具類

package com.hnu.scw.utils;

import com.alibaba.excel.EasyExcel;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 9:53 2020/3/16 0016
 * @ Description:Excel操作常用的工具類
 * @ Modified By:
 * @Version: $version$
 */
public class ExcelUtil {

    /**
     * 默認excel文件名和單元sheet名一樣的 Excel文件導出
     * @param httpServletResponse
     * @param data
     * @param fileName
     * @param clazz
     * @throws IOException
     */
    public static void writeExcel(HttpServletResponse httpServletResponse, List data, String fileName, Class clazz) throws IOException {
        writeExcel(httpServletResponse, data, fileName, fileName, clazz);
    }

    /**
     * 導出數據爲Excel文件
     * @param response  響應實體
     * @param data  導出數據
     * @param fileName 文件名
     * @param sheetName 單元格名
     * @param clazz  定義excel導出的實體
     * @throws IOException
     */
    public static void writeExcel(HttpServletResponse response, List data, String fileName, String sheetName, Class clazz) throws IOException {
        //防止中文亂碼
        fileName = URLEncoder.encode(fileName, "UTF-8");
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        //防止導入excel文件名中文不亂碼
        response.setHeader("Content-disposition", "attachment;fileName=" + fileName + ".xlsx" + ";fileName*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(data);
    }

}

定義導出模板的實體

package com.hnu.scw.model;

import com.alibaba.excel.annotation.ExcelProperty;
import com.hnu.scw.annotation.DownExcelValue;
import com.hnu.scw.service.CustomDownExcelService;
import lombok.Data;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:50 2020/3/16 0016
 * @ Description:具有下拉列表的excel實體
 * @ Modified By:
 * @Version: $version$
 */
@Data
public class ExportMouldDownDto {

    private Integer age;

    @DownExcelValue(source = {"小白", "小黑"})
    private String name;

    @DownExcelValue(sourceClass = CustomDownExcelService.class)
    private String sex;

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

編寫調用導出方法的服務類

package com.hnu.scw.service;

import com.hnu.scw.model.ExportMouldDto;
import com.hnu.scw.utils.ExcelUtil;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:20 2020/3/16 0016
 * @ Description:Excel的導入和導出實現類
 * @ Modified By:
 * @Version: $version$
 */
public class ExcelServiceImpl {

    /**
     * 導出 普通樣式的excel 模板
     * @param response
     */
    public void exportExcelMould(HttpServletResponse response) throws IOException {
        //定義模板的樣例數據(PS:如果不需要模板有樣例數據那麼則不需要處理)
        ExportMouldDto example = new ExportMouldDto();
        example.setAge(18);
        example.setName("小白");
        example.setSex("男");
        //調用工具類
        ExcelUtil.writeExcel(response, Arrays.asList(example), "導入數據模板", ExportMouldDto.class);
    }
}

PS:這裏就是模擬一下導出方法的使用的關鍵代碼,這個也是根據實際的情況看進行選擇性的對應處理;

具有單元格下拉列表的模板導出

我們在平常中,會經常看到有Excel中的單元格是不讓進行編輯,而只能通過下拉列表的值進行選擇,那麼這樣的功能的Excel表格是怎樣實現的呢?

步驟

定義設置下拉列表內容的註解

package com.hnu.scw.annotation;

import java.lang.annotation.*;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:44 2020/3/16 0016
 * @ Description:${description}
 * @ Modified By:
 * @Version: $version$
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface DownExcelValue {
    //定義固定下拉的內容
    String[] source() default {};

    //定義動態下拉的內容,
    Class[] sourceClass() default {};
}

定義下拉列表接口

package com.hnu.scw.service;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:46 2020/3/16 0016
 * @ Description:${description}
 * @ Modified By:
 * @Version: $version$
 */
public interface CustomDownExcelService {
    /**
     * 下拉列表的內容數組
     * PS:主要用於定義下拉列表的內容
     * @return
     */
    String[] source();
}

定義下拉列表接口實現類

package com.hnu.scw.service;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:48 2020/3/16 0016
 * @ Description:定義性別的下拉列表的內容
 * @ Modified By:
 * @Version: $version$
 */
public class CustomDownExcelServiceImpl implements  CustomDownExcelService {

    public String[] source() {
        return new String[]{"男","女","不詳"};
    }
}

定義下拉列表的excel模板導出方法

/**
     * 導出 單元格具有下拉列表樣式的excel 模板
     * @param response
     */
    public void exportDownExcelMould(HttpServletResponse response) throws IOException {
        //存儲下拉列表集合
        Map<Integer, String[]> explicitListConstraintMap = new HashMap<Integer, String[]>();

        Field[] declaredFields = ExportMouldDownDto.class.getDeclaredFields();
        for (int i = 0; i < declaredFields.length; i++) {
            Field field = declaredFields[i];
            DownExcelValue explicitConstraint = field.getAnnotation(DownExcelValue.class);
            //解析註解信息
            String[] explicitArray = dealDownExcelAnnotation(explicitConstraint);
            if (explicitArray != null && explicitArray.length > 0) {
                explicitListConstraintMap.put(i, explicitArray);
            }
        }

        ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExportMouldDownDto.class).registerWriteHandler(new SheetWriteHandler() {
            public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
            }
            public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
                //通過sheet處理下拉信息
                Sheet sheet = writeSheetHolder.getSheet();
                DataValidationHelper helper = sheet.getDataValidationHelper();
                explicitListConstraintMap.forEach((k, v) -> {
                    CellRangeAddressList rangeList = new CellRangeAddressList();
                    CellRangeAddress addr = new CellRangeAddress(1, 1000, k, k);
                    rangeList.addCellRangeAddress(addr);
                    DataValidationConstraint constraint = helper.createExplicitListConstraint(v);
                    DataValidation validation = helper.createValidation(constraint, rangeList);
                    sheet.addValidationData(validation);
                });
            }
        }).build();
        WriteSheet sheet = EasyExcel.writerSheet().build();
        //設置樣例數據
        ExportMouldDownDto example = new ExportMouldDownDto();
        example.setAge(18);
        example.setName("哈哈哈哈");
        example.setSex("男");
        excelWriter.write(null,sheet).finish();
    }

分Sheet單元導出大量數據

場景:有時候我們會遇到導出的數據量較大,而每個單元頁顯示的內容的條數有一定的限制,那麼如何實現“分頁”單元頁的導出呢?

步驟

分多Sheet導出excel數據

設置響應流的頭信息

package com.hnu.scw.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.List;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 9:53 2020/3/16 0016
 * @ Description:Excel操作常用的工具類
 * @ Modified By:
 * @Version: $version$
 */
public class ExcelUtil {

    /**
     * 設置響應頭信息
     * @param response
     * @param fileName
     */
    public static void setHead(HttpServletResponse response, String fileName){
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        //防止導入excel文件名中文不亂碼
        response.setHeader("Content-disposition", "attachment;fileName=" + fileName + ".xlsx" + ";fileName*=utf-8''" + fileName + ".xlsx");
    }

}

分sheet頁導出數據方法

/**
     * 分sheet單元導出excel數據
     * @param response
     */
    public void pageExportExcelData(HttpServletResponse response) throws IOException {
        OutputStream outputStream;
        ExcelWriter excelWriter;
        // 設置導出文件名(PS:防止中文亂碼)
        String fileName = URLEncoder.encode("分sheet導出數據", "UTF-8");
        // 設置響應流
        ExcelUtil.setHead(response, fileName);
        // 獲取響應流
        outputStream = response.getOutputStream();
        // 設置導出格式
        excelWriter = new ExcelWriter(outputStream, ExcelTypeEnum.XLSX);
        // 當前導出的總數據
        int currentExportTotalNumber = 0;
        // 每個sheet的數據條數
        int pageEverySheetNumber = 2000;
        // TODO 查詢要導出的數據總條數(PS:這個就根據對應需求處理即可)
        int totalExportNumber = 10000;
        // 當前數據所需要導出的sheet序號
        int currentSheetOrder = 0;
        // 需要導出的sheet數(PS:這個規則根據需求即可,這裏模擬每個sheet就200條數據)
        int totalPage = totalExportNumber % pageEverySheetNumber == 0 ? totalExportNumber / pageEverySheetNumber : (totalExportNumber / pageEverySheetNumber) + 1;
        // 創建第一個sheet單元
        Sheet sheet = new Sheet(1, 0, TemplateExportBean.class, fileName, null);
        for (int i = 0; i < totalPage; i++) {
            // TODO 查詢當前sheet需要導出的數據內容(PS:這裏的話就不處理了)
            List<TemplateExportBean> currentExportList = new ArrayList<>();
            // 當前sheet的序號
            int belongSheetOrder = currentExportTotalNumber / pageEverySheetNumber;
            // 判斷是否需要創建新的sheet
            if(belongSheetOrder == currentSheetOrder){
                // 設置sheet序號
                currentSheetOrder = belongSheetOrder;
            }else{
                // 將數據寫入不同的sheet單元
                sheet = new Sheet(belongSheetOrder + 1, 0, TemplateExportBean.class, fileName, null);
            }
            if(sheet != null){
                // 設置開始寫excel表格的sheet位置
                sheet.setStartRow(currentExportTotalNumber - belongSheetOrder * pageEverySheetNumber);
                // 寫入數據
                excelWriter.write(currentExportList, sheet);
                // 增加已經導出數據的條數
                currentExportTotalNumber += currentExportList.size();
                // 釋放資源
                currentExportList.clear();
            }
        }
        if(excelWriter != null){
            excelWriter.finish();
        }
    }

導入

上面已經針對導出功能進行了講解,那麼接下來就說說導入又是如何實現的呢?

步驟

導入pom依賴

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>4.3.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.0.2</version>
        </dependency>

上傳excel導入模板對應實體

PS:這裏就是簡單的模擬,後續根據需求進行自己對應補充哦!

package com.hnu.scw.model;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 10:01 2020/3/31 0031
 * @ Description:Excel導入模板類
 * @ Modified By:
 * @Version: $version$
 */
@Data
public class ExcelImportTemplateDto {
    @ExcelProperty(value = "名稱", index = 0)
    private String name;
}

上傳處理成功的消息提示實體

PS:如果不需要進行任何的提示,而只需要處理上傳任務,這可以不需要;

package com.hnu.scw.model;

import lombok.Data;

import java.io.Serializable;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 8:56 2020/3/31 0031
 * @ Description:導入成功的提示實體
 * @ Modified By:
 * @Version: $version$
 */
@Data
public class ImportSuccessDto implements Serializable{

    private Object object;

    public ImportSuccessDto(Object object) {
        this.object = object;
    }
}

上傳處理失敗的消息提示實體

PS:如果不需要進行任何的提示,而只需要處理上傳任務,這可以不需要;

package com.hnu.scw.model;

import lombok.Data;

import java.io.Serializable;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 8:56 2020/3/31 0031
 * @ Description:導入失敗的提示實體
 * @ Modified By:
 * @Version: $version$
 */
@Data
public class ImportFailDto implements Serializable{
    // 導入實體信息
    private Object object;
    // 導入錯誤的提示
    private String errMsg;

    public ImportFailDto(Object object, String errMsg) {
        this.object = object;
        this.errMsg = errMsg;
    }

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }
}

編寫處理上傳數據邏輯返回的實體類

PS:如果不需要對數據邏輯校驗後的響應信息的提示,那麼也可以不需要該實體;

package com.hnu.scw.model;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 9:19 2020/3/31 0031
 * @ Description:Excel處理結果
 * @ Modified By:
 * @Version: $version$
 */
@Data
public class ExcelImportResultDto {
    // 導入成功的消息列表
    private List<ImportSuccessDto> successDtoList;
    // 導入失敗的消息列表
    private List<ImportFailDto> failDtoList;

    public ExcelImportResultDto(List<ImportSuccessDto> successDtoList, List<ImportFailDto> failDtoList) {
        this.successDtoList = successDtoList;
        this.failDtoList = failDtoList;
    }

    public ExcelImportResultDto(List<ImportFailDto> failDtoList) {
        this.failDtoList = failDtoList;
        this.successDtoList = new ArrayList<>();
    }

    public List<ImportSuccessDto> getSuccessDtoList() {
        return successDtoList;
    }

    public void setSuccessDtoList(List<ImportSuccessDto> successDtoList) {
        this.successDtoList = successDtoList;
    }

    public List<ImportFailDto> getFailDtoList() {
        return failDtoList;
    }

    public void setFailDtoList(List<ImportFailDto> failDtoList) {
        this.failDtoList = failDtoList;
    }
}

編寫處理上傳數據的邏輯處理類

PS:這裏只是簡單的梳理了處理流程,而具體的處理邏輯,則根據需求來進行即可。

package com.hnu.scw.service;

import com.hnu.scw.model.ExcelImportResultDto;

import java.util.List;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 9:02 2020/3/31 0031
 * @ Description:處理導入數據的邏輯類
 * @ Modified By:
 * @Version: $version$
 */
public class HandleImportExcelService {
    public <T> ExcelImportResultDto checkImportData(List<T> list){
        // TODO 編寫校驗的邏輯
        return new ExcelImportResultDto(null);
    }
}

編寫上傳處理excel監聽

package com.hnu.scw.listener;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.context.AnalysisContextImpl;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.hnu.scw.model.ExcelImportResultDto;
import com.hnu.scw.model.ImportFailDto;
import com.hnu.scw.model.ImportSuccessDto;
import com.hnu.scw.service.HandleImportExcelService;
import com.hnu.scw.utils.ImportExcelValidHelper;

import java.lang.reflect.Field;
import java.util.*;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 8:54 2020/3/31 0031
 * @ Description:導入excel的監聽處理類
 * @ Modified By:
 * @Version: $version$
 */
public class ExcelImportListener<T>  extends AnalysisEventListener<T>{
    // 導入成功的數據
    private List<ImportSuccessDto> importSuccessDtoList = new ArrayList<>();
    // 導入失敗的數據
    private List<ImportFailDto> importFailDtoList = new ArrayList<>();

    private List<T> list = new ArrayList<>();
    // 處理導入數據的邏輯
    private HandleImportExcelService handleImportExcelService;

    private Class<T> tClass;

    public List<ImportSuccessDto> getImportSuccessDtoList() {
        return importSuccessDtoList;
    }

    public void setImportSuccessDtoList(List<ImportSuccessDto> importSuccessDtoList) {
        this.importSuccessDtoList = importSuccessDtoList;
    }

    public List<ImportFailDto> getImportFailDtoList() {
        return importFailDtoList;
    }

    public void setImportFailDtoList(List<ImportFailDto> importFailDtoList) {
        this.importFailDtoList = importFailDtoList;
    }

    public List<T> getList() {
        return list;
    }

    public void setList(List<T> list) {
        this.list = list;
    }

    public HandleImportExcelService getHandleImportExcelService() {
        return handleImportExcelService;
    }

    public void setHandleImportExcelService(HandleImportExcelService handleImportExcelService) {
        this.handleImportExcelService = handleImportExcelService;
    }

    public Class<T> gettClass() {
        return tClass;
    }

    public void settClass(Class<T> tClass) {
        this.tClass = tClass;
    }

    public ExcelImportListener(HandleImportExcelService handleImportExcelService) {
        this.handleImportExcelService = handleImportExcelService;
    }

    public ExcelImportListener(HandleImportExcelService handleImportExcelService, Class<T> tClass) {
        this.handleImportExcelService = handleImportExcelService;
        this.tClass = tClass;
    }

    /**
     * 導入數據的處理邏輯
     * @param t
     * @param analysisContext
     */
    @Override
    public void invoke(T t, AnalysisContext analysisContext) {
        //錯誤提示
        String msg = "";
        try {
            msg = ImportExcelValidHelper.checkDataValid(t);
        }catch (Exception e){
            msg = "解析處理校驗異常";
        }
        // 如果校驗失敗
        if(msg.length() != 0){
            ImportFailDto importFailDto = new ImportFailDto(t, msg);
            importFailDtoList.add(importFailDto);
        }else{
            list.add(t);
        }
        // 每1000條處理一次(PS:根據需求來即可)
        if(list.size() > 1000){
            ExcelImportResultDto excelImportResultDto = handleImportExcelService.checkImportData(list);
            if(excelImportResultDto.getSuccessDtoList().size() >= 0){
                importSuccessDtoList.addAll(excelImportResultDto.getSuccessDtoList());
            }
            if(excelImportResultDto.getFailDtoList().size() >= 0){
                importFailDtoList.addAll(excelImportResultDto.getFailDtoList());
            }
            list.clear();
        }
    }

    /**
     * 所有數據處理完之後的處理方法
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
         // PS:之所以最後這裏還進行處理是防止最後一次的數據量沒有達到批量值
        ExcelImportResultDto excelImportResultDto = handleImportExcelService.checkImportData(list);
        if(excelImportResultDto.getSuccessDtoList().size() >= 0){
            importSuccessDtoList.addAll(excelImportResultDto.getSuccessDtoList());
        }
        if(excelImportResultDto.getFailDtoList().size() >= 0){
            importFailDtoList.addAll(excelImportResultDto.getFailDtoList());
        }
        list.clear();
    }

    /**
     * 校驗導入的表格的頭是否匹配
     * @param headMap
     * @param analysisContext
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext analysisContext){
        super.invokeHeadMap(headMap, analysisContext);
        if(tClass != null){
            try {
            // 獲取Excel導入實體的單元格內容
                Map<Integer, String> indexNameMap = getIndexName(tClass);
                Set<Integer> keySet = indexNameMap.keySet();
                for (Integer key: keySet) {
                    // 頭表是否存在空值
                    if(headMap.get(key).length() == 0){
                        throw new ExcelAnalysisException("Excel格式非法");
                    }
                    // 對比導入Excel實體模板和當前上傳Excel是否匹配
                    if(!headMap.get(key).equals(indexNameMap.get(key))){
                        throw new ExcelAnalysisException("Excel格式非法");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    private Map<Integer, String> getIndexName(Class<T> tClass) throws NoSuchFieldException {
        Map<Integer, String> result = new HashMap<>();
        Field[] declaredFields = tClass.getDeclaredFields();
        for (Field currentField: declaredFields) {
            currentField.setAccessible(true);
            ExcelProperty annotation = currentField.getAnnotation(ExcelProperty.class);
            if(annotation != null){
                int index =annotation.index();
                String[] value = annotation.value();
                StringBuilder sb = new StringBuilder();
                for (String cur : value) {
                    sb.append(cur);
                }
                result.put(index, sb.toString());
            }
        }
        return result;
    }


}

導入excel的數據一般性校驗工具類

PS:主要是對上傳數據的數據類型或者簡單合法性的校驗處理

package com.hnu.scw.utils;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 9:06 2020/3/31 0031
 * @ Description:導入excel數據的校驗類
 * @ Modified By:
 * @Version: $version$
 */
public class ImportExcelValidHelper {

    /**
     * 執行數據的校驗處理
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String checkDataValid(T obj){
        // TODO 執行數據的校驗處理
        return null;
    }
}

接受 excel導入和導出數據的工具類

PS:主要用於接受前端上傳的文件和excel數據監聽處理

package com.hnu.scw.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.List;

/**
 * @ Author     :scw
 * @ Date       :Created in 下午 9:53 2020/3/16 0016
 * @ Description:Excel操作常用的工具類
 * @ Modified By:
 * @Version: $version$
 */
public class ExcelUtil {

    /**
     * 默認excel文件名和單元sheet名一樣的 Excel文件導出
     * @param httpServletResponse
     * @param data
     * @param fileName
     * @param clazz
     * @throws IOException
     */
    public static void writeExcel(HttpServletResponse httpServletResponse, List data, String fileName, Class clazz) throws IOException {
        writeExcel(httpServletResponse, data, fileName, fileName, clazz);
    }

    /**
     * 導出數據爲Excel文件
     * @param response  響應實體
     * @param data  導出數據
     * @param fileName 文件名
     * @param sheetName 單元格名
     * @param clazz  定義excel導出的實體
     * @throws IOException
     */
    public static void writeExcel(HttpServletResponse response, List data, String fileName, String sheetName, Class clazz) throws IOException {
        //防止中文亂碼
        fileName = URLEncoder.encode(fileName, "UTF-8");
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        //防止導入excel文件名中文不亂碼
        response.setHeader("Content-disposition", "attachment;fileName=" + fileName + ".xlsx" + ";fileName*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(data);
    }

    /**
     *  easyExcel處理上傳的文件
     * @param request 請求實體
     * @param fileName 請求文件名(PS:要注意與前端上傳的匹配)
     * @param sheetNo 單元格
     * @param rowNumber 行數
     * @param analysisEventListener  上傳處理監聽
     * @param clazz  上傳處理excel類
     * @return
     * @throws Exception
     */
    public static List<Object> readExcel(HttpServletRequest request, String fileName, Integer sheetNo, Integer rowNumber, AnalysisEventListener analysisEventListener, Class clazz) throws Exception {
        // 獲取解析器
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver(request.getSession().getServletContext());
        MultipartHttpServletRequest multipartHttpServletRequest = commonsMultipartResolver.resolveMultipart(request);
        MultipartFile file = multipartHttpServletRequest.getFile(fileName);
        String originalFilename = file.getOriginalFilename();
        if(StringUtils.isEmpty(originalFilename)){
            throw new Exception("上傳的文件不能爲空");
        }
        if(!originalFilename.toLowerCase().endsWith(ExcelTypeEnum.XLS.getValue()) ||
                !originalFilename.toLowerCase().endsWith(ExcelTypeEnum.XLSX.getValue())   ){
            throw new Exception("上傳的文件格式不匹配");
        }
        InputStream   inputStream = file.getInputStream();
        return EasyExcel.read(inputStream, clazz, analysisEventListener).sheet(sheetNo).headRowNumber(rowNumber).doReadSync();
    }

}


Controller層接受文件上傳方法以及處理導入數據

PS:(1)主要是用於接受前端請求的excel文件和調用數據的處理邏輯,以及返回處理之後的響應結果的導出;
(2)當然不一定是Controller層,如果是採取的微服務的架構,那麼也就對應數據暴露接口的層;

/**
     * 執行 Excel的數據的導入
     * @param response
     * @param request
     */
    public void importExcel(HttpServletResponse response, HttpServletRequest request) throws Exception {
        ExcelImportListener excelImportListener = new ExcelImportListener(handleImportExcelService, ExcelImportTemplateDto.class);
        // 讀取數據
        ExcelUtil.readExcel(request, "file", 0, 1, excelImportListener, ExcelImportTemplateDto.class);
        // 錯誤集
        List<ImportFailDto> importFailDtoList = excelImportListener.getImportFailDtoList();
        if(!importFailDtoList.isEmpty()){
            List<ExcelTemplateCompleteDto> excelTemplateCompleteDtoList = importFailDtoList.stream().map(current->{
                ExcelTemplateCompleteDto excelTemplateCompleteDto = new ExcelTemplateCompleteDto();
                BeanUtils.copyProperties(current.getObject(), excelTemplateCompleteDto);
                // 設置錯誤信息
                excelTemplateCompleteDto.setErrMsg(current.getErrMsg());
                return excelTemplateCompleteDto;
            }).collect(Collectors.toList());
            //導出excel(PS:主要用於將錯誤的數據導出)
            String fileName = URLEncoder.encode("導入結果", "UTF-8");
            ExcelUtil.writeExcel(response, excelTemplateCompleteDtoList, fileName, ExcelTemplateCompleteDto.class);
        }
    }

彩蛋(讀取項目模板直接下載)

場景

在有的時候,我們已經存在着模板,並且已經存放在項目中的某個目錄中,那麼如何快速讀取項目中的模板文件並且實現導出呢?

步驟

注意:別忘記首先在項目中添加好模板文件哦!如下:
在這裏插入圖片描述

方法一:HttpServletResponse實現

/**
     * 通過 http response實現模板Excel文件的下載
     * @param response
     */
    public void downloadExcelTemplate(HttpServletResponse response) throws IOException {
        InputStream in = null;
        OutputStream outputStream = null;
        try {
            // 設置導出文件名(PS:防止中文亂碼)
            String fileName = URLEncoder.encode("測試文件.xlsx", "UTF-8");
            // 讀取項目excel模板流
            in = this.getClass().getResourceAsStream("/template/" + fileName);
            // 設置response響應信息
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            //防止導入excel文件名中文不亂碼
            response.setHeader("Content-disposition", "attachment;fileName=" + fileName + ";fileName*=utf-8''" + fileName);
            outputStream = response.getOutputStream();
            int readSize = 0;
            byte[] buff = new byte[1024];
            while((readSize = in.read(buff)) > -1){
                outputStream.write(buff, 0, readSize);
            }
            outputStream.flush();
        }finally {
            if(in != null){
                in.close();
            }
            if(outputStream != null){
                outputStream.close();
            }
        }
    }

方法二:Response返回實現

/**
     * 通過 將流返回給前端實現模板Excel文件的下載
     */
    public Response downloadExcelTemplate() throws IOException {
        InputStream in = null;
        OutputStream outputStream = null;
        try {
            // 設置導出文件名(PS:防止中文亂碼)
            String fileName = URLEncoder.encode("測試文件.xlsx", "UTF-8");
            // 讀取項目excel模板流
            in = this.getClass().getResourceAsStream("/template/" + fileName);
            /* 這一段可以簡單的變爲後續的那種寫法
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            int readSize = 0;
            byte[] buff = new byte[1024];
            while((readSize = in.read(buff)) > -1){
                byteArrayOutputStream.write(buff, 0, readSize);
            }
            if(in != null){
                in.close();
            }
            StreamingOutput streamingOutput = out -> out.write(byteArrayOutputStream.toByteArray());*/
            // 簡化版的寫法
            byte[] bytes = IOUtils.toByteArray(in);
            StreamingOutput streamingOutput = out -> out.write(bytes);
            Response.ResponseBuilder responseBuilder = Response.ok(streamingOutput);
            responseBuilder.header("Content-disposition", "attachment;fileName=" + fileName + ";fileName*=utf-8''" + fileName);
            responseBuilder.type(MediaType.APPLICATION_OCTET_STREAM);
            return responseBuilder.build();
        }finally {
            if(in != null){
                in.close();
            }
            if(outputStream != null){
                outputStream.close();
            }
        }
    }

驚喜發生

別以爲上面這樣一寫就大功告成了,當我們運行之後,我們會發現確實文件是下載下來了,但是打開文件就會出現如下的錯誤;
在這裏插入圖片描述
這個是爲什麼爲什麼呢?

原因分析

(1)下載的方式不對嗎?
錯,文件能下載說明下載的方式是OK的!
(2)Excel打開的限制?安全檢查?
看到很多文章說到這個的原因,實際不是的,假設即使如此操作了還是無法解決呢?
在這裏插入圖片描述

(3)那麼真實的原因呢?
這其實原因在於,當我們把excel作爲項目目錄中時,假設我們採取的框架是Spring或者SpringBoot時,其實是會將我們的excel文件進行壓縮。而問題來了,正是因爲壓縮,導致我們下載下來的文件肯定就是缺失了某些字節文件的,因爲我們下載的時候並沒有還原文件。因此,這樣下載下來肯定就是有問題的啦!!!!

解決辦法

在項目的pom文件中,設置項目不需要將excel格式的文件進行壓縮處理即可。

<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<version>2.6</version>
				<artifactId>maven-resources-plugin</artifactId>
				<configuration>
					<encoding>UTF-8</encoding>
					<nonFilteredFileExtensions>
						<nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
					</nonFilteredFileExtensions>
				</configuration>
			</plugin>

總結

  1. 針對EasyExcel的常用的導入導出,在上面都已經進行了詳細的描述。當然,這對於我們實際項目中可能遇到的場景還遠遠不夠豐富,但是,主要是通過閱讀這樣的處理方式,而讓我們能有“舉一反三”的思想,這樣才能更好的實現需求。
  2. 在實際中,easyExcel還是存在很多的弊端,因爲它畢竟是同步調用的,而我們很多情況都是採取異步的處理方式。那麼,這樣我們又能如何去做呢?(1)將處理方式通過異步方式進行後臺異步的處理,當處理完成再回調;(2)可以通過公司中的文件服務器進行作爲中間層,然後再利用消息隊列的方式或者redis的形式將需要處理的內容進行告知程序再進行處理。(3)還有很多的方法的,打開你的小腦瓜,相信你一定可以的。
  3. 此外,建議大家可以多看看easyExcel的源碼,然後自己封裝一層,能作爲更好更通用的“導入導出”工具。這也是很體現一個人的能力的呢。
  4. 建議大家如果有需要的話,直接拷貝對應內容即可,而不需要我一一發送源碼了,因爲關鍵的內容都在文章中進行了詳細的說明;
  5. 最後,感謝各位的閱讀哦!!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章