前言:
? ? ? ? 近期由于工作的需要,根據需求需要導出導入Excel模板。于是自學了一下下,在此記錄并分享!!
EasyExcel:
? ? ? ? 首先我要在這里非常感謝阿里的大佬們!封裝這么好用的Excel相關的API,真的是拯救了許多猿猿們!!
? ? ? ? EasyExcel官網:關于Easyexcel | Easy Excel 官網
導出Excel(write):
? ? ? ? 接到項目之后,我負責的其中一個小需求是需要導出Excel,網上查了大量資料,發現EasyExcel包中已經包含了大量的對Excel的操作,當然對于一些特殊的需求可能需要自己寫一個對Excel操作的方法~
@ExcelProperty注解:
? ? ? ? 我將這個注解作為首要講解對象,足以看出其重要性。
? ? ? ? 示例代碼:
@ExcelProperty(value = "序號",order = 1,converter = IdConverter.class)private Long id = 1L;@ExcelProperty(value = "出生日期", format = "yyyy-MM-dd",headStyle = CustomHeadStyleStrategy.class)private Date birthDate;
只要在對應字段上加上該注解以后,意味著該字段將作為表頭出現在Excel中,其中有一些參數如下:
1.?
value
- 類型:
String[]
- 作用:指定 Excel 表頭的名稱。可以傳入一個字符串數組,當存在多級表頭時,數組中的元素按順序對應各級表頭。
2.?
index
- 類型:
int
- 作用:指定該屬性對應 Excel 表格中的列索引,索引從 0 開始。當 Excel 列順序固定時,可以使用此參數進行精確映射。
3.?
converter
- 類型:
Class<? extends Converter<?>>
- 作用:指定自定義的轉換器,用于將 Java 對象屬性與 Excel 單元格數據進行轉換。當默認的轉換器無法滿足需求時,可以自定義轉換器并通過此參數指定。
4.?
format
- 類型:
String
- 作用:用于指定日期、數字等類型數據的格式化模式。當讀取或寫入 Excel 時,會按照指定的格式進行轉換。
5.?
order
- 類型:
int
- 作用:指定該屬性在生成 Excel 表格時的列順序,數值越小越靠前。
6.?
headStyle
- 類型:
Class<? extends HeadStyleStrategy>
- 作用:指定表頭樣式的策略類,用于自定義表頭的樣式,如字體、顏色、背景色等。
7.?
contentStyle
- 類型:
Class<? extends ContentStyleStrategy>
- 作用:指定內容樣式的策略類,用于自定義表格內容的樣式,如字體、顏色、對齊方式等。
當然隨著EasyExcel引入的版本不一樣,有些參數可能被淘汰或者是換成了新的~
著重需要說明兩個參數:
1.converter參數,這個參數需要實現Converter接口
public class IdConverter implements Converter<Integer> {
//todo
}
這個接口需要重寫三個方法:
Class<?> supportJavaTypeKey();
- 作用:指明此轉換器所支持的 Java 類型。返回值是一個?
Class
?對象,代表支持轉換的 Java 類型。
CellDataTypeEnum supportExcelTypeKey();
- 作用:指定該轉換器支持的 Excel 單元格數據類型。
CellDataTypeEnum
?是一個枚舉類型,包含了各種 Excel 單元格數據類型,像字符串、數字、日期等。
T convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception;
- 作用:把 Excel 單元格數據轉換為 Java 對象屬性。參數如下:
cellData
:表示從 Excel 讀取到的單元格數據。contentProperty
:包含單元格的一些屬性信息。globalConfiguration
:全局配置信息。
綜上示例如下:
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;public class StringConverter implements Converter<String> {@Overridepublic Class<?> supportJavaTypeKey() {return String.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}@Overridepublic String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {return cellData.getStringValue();}@Overridepublic WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {return new WriteCellData<>(value);}
}
當然也可以根據自身的需求進行補充,例如:希望在數字后面+單位,在人名后面+稱呼,希望id是一個自增的等等行為~~
Excel中的id列自增:
? ? ? ? 這里我舉例id列希望是自增的:
實體類代碼:
@ExcelProperty(value = "序號",order = 1,converter = IdConverter.class)private Long id = 1L;
converter代碼:?
public class IdConverter implements Converter<Integer> {private int currentId = 1;@Overridepublic Class<?> supportJavaTypeKey() {return Integer.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.NUMBER;}@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {return new WriteCellData<>(String.valueOf(currentId++));}
}
效果如下:
????????
復雜表頭:
? ? ? ? 如果想要一個多級表頭,使用上述的注解一樣可以做到:
只不過value的值要變成層級關系:
@ExcelProperty(value = {"項目交付信息","牽頭部門名稱"},order = 14)private String leadingDepartment;@ExcelProperty(value = {"項目交付信息","交付部門名稱"},order = 15)private String deliveryDepartment;@ExcelProperty(value = {"項目交付信息","項目PO"},order = 16)private String projectPo;@ExcelProperty(value = {"項目交付信息","項目經理"},order = 17)private String projectManager;
@DateTimeFormat注解:
? ? ? ? 該注解也是一個確定時間格式的注解,由于版本的迭代,上述的@ExcelProperty的format
屬性已經被廢除,建議使用該注解進行時間格式的限制。
????????@DateTimeFormat
?注解是 Spring 框架提供的一個用于日期和時間格式化的注解,它主要用于將字符串類型的日期時間數據綁定到 Java 對象的日期時間類型字段上,或者將 Java 對象中的日期時間類型字段按照指定格式輸出為字符串。
pattern
- 類型:
String
- 作用:指定日期時間的格式化模式。模式遵循 Java 的?
SimpleDateFormat
?或?DateTimeFormatter
?的規則。例如,"yyyy-MM-dd"
?表示年 - 月 - 日的格式,"yyyy-MM-dd HH:mm:ss"
?表示年 - 月 - 日 時:分: 秒的格式。
iso
- 類型:
ISO
?枚舉類型- 作用:使用預定義的 ISO 日期時間格式。
ISO
?枚舉包含了幾個常用的 ISO 日期時間格式,如?ISO.DATE
?表示?yyyy-MM-dd
?格式,ISO.TIME
?表示?HH:mm:ss.SSSXXX
?格式,ISO.DATE_TIME
?表示?yyyy-MM-dd'T'HH:mm:ss.SSSXXX
?格式。
示例如下:
@DateTimeFormat(pattern = "yyyy-MM-dd")private Date birthDate;
@ContentRowHeight,@HeadRowHeight,@ColumnWidth
? ? ? ? 設置行高與列寬,上述的三個注解可以實現設置操作,注意,上述注解@ColumnWidth可以使用在類上面,也可以單獨注解在屬性上面。
? ? ? ? 示例如下:
@ContentRowHeight(20)//內容行高
@HeadRowHeight(20)//表頭行高
@ColumnWidth(25)//列寬
public class DeriveExcelDTO {@ColumnWidth(40)@ExcelProperty(value = "項目名稱",order = 2)private String projectName;
}
這里就不展示效果圖了,大家有興趣可以自己進行嘗試!!!
示例完整導出代碼:
// 實體類,用于映射Excel列
public class DemoData {@ExcelProperty("字符串標題")private String string;@ExcelProperty("日期標題")private Date date;@ExcelProperty("數字標題")private Double doubleData;// 構造函數、getter和setterpublic DemoData() {}public DemoData(String string, Date date, Double doubleData) {this.string = string;this.date = date;this.doubleData = doubleData;}// getters and setterspublic String getString() { return string; }public void setString(String string) { this.string = string; }public Date getDate() { return date; }public void setDate(Date date) { this.date = date; }public Double getDoubleData() { return doubleData; }public void setDoubleData(Double doubleData) { this.doubleData = doubleData; }
}// 控制器示例(Spring MVC)
@RestController
public class ExcelExportController {@GetMapping("/export")public void export(HttpServletResponse response) throws IOException {// 準備數據List<DemoData> data = new ArrayList<>();for (int i = 0; i < 10; i++) {DemoData demoData = new DemoData();demoData.setString("字符串" + i);demoData.setDate(new Date());demoData.setDoubleData(0.56 + i);data.add(demoData);}// 設置響應頭response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileName = URLEncoder.encode("測試", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");// 導出ExcelEasyExcel.write(response.getOutputStream(), DemoData.class).sheet("模板").doWrite(data);}
}
導入Excel(read):
? ? ? ? 導出Excel其實和導入是差不多的,但是在使用EasyExcel時,需要注意表頭的書寫是否和讀取時一致(例如讀取時讀二級表頭,Excel中對應的數據的表頭必須也是二級的)!后面會詳細介紹到。
設置監聽器:
? ? ? ? 讀取Excel表格時,必須設置一個監聽器,但是需要注意的是該監聽器不能被Spring管理,每次使用時必須手動new。
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {/*** 每隔5條存儲數據庫,實際使用中可以100條,然后清理list ,方便內存回收*/private static final int BATCH_COUNT = 100;/*** 緩存的數據*/private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);/*** 假設這個是一個DAO,當然有業務邏輯這個也可以是一個service。當然如果不用存儲這個對象沒用。*/private DemoDAO demoDAO;public DemoDataListener() {// 這里是demo,所以隨便new一個。實際使用如果到了spring,請使用下面的有參構造函數demoDAO = new DemoDAO();}/*** 如果使用了spring,請使用這個構造方法。每次創建Listener的時候需要把spring管理的類傳進來** @param demoDAO*/public DemoDataListener(DemoDAO demoDAO) {this.demoDAO = demoDAO;}/*** 這個每一條數據解析都會來調用** @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}* @param context*/@Overridepublic void invoke(DemoData data, AnalysisContext context) {log.info("解析到一條數據:{}", JSON.toJSONString(data));cachedDataList.add(data);// 達到BATCH_COUNT了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOMif (cachedDataList.size() >= BATCH_COUNT) {saveData();// 存儲完成清理 listcachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}/*** 所有數據解析完成了 都會來調用** @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 這里也要保存數據,確保最后遺留的數據也存儲到數據庫saveData();log.info("所有數據解析完成!");}/*** 加上存儲數據庫*/private void saveData() {log.info("{}條數據,開始存儲數據庫!", cachedDataList.size());demoDAO.save(cachedDataList);log.info("存儲數據庫成功!");}
}
當然阿里為了更加方便猿猿們,也就行了二次封裝,封裝后的ReadListener可以交給Spring進行管理,也是非常方便了~
? ? ? ? 沒錯就是這個AnalysisEventListener類,里面繼承了ReadListener,也是更加簡便清晰了~
可以重寫一下的方法:
每個方法的作用以及參數如下:
????????
onException(Exception exception, AnalysisContext context)
- 作用:讀取過程中發生異常時觸發
- 參數:
exception
:異常對象context
:讀取上下文
doAfterAllAnalysed(AnalysisContext context)
- 作用:整個 Excel 文件讀取完成后觸發
- 參數:讀取上下文
invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context)
- 作用:解析表頭時觸發(默認表頭占 1 行)
- 參數:
headMap
:表頭數據(key 為列索引,value 為單元格數據)context
:讀取上下文
hasNext(AnalysisContext context)
- 作用:控制是否繼續讀取下一行(返回 false 時終止讀取)
- 參數:讀取上下文
?完整示例如下:
public class DemoDataListener extends AnalysisEventListener<DemoData> {private List<DemoData> dataList = new ArrayList<>();@Overridepublic void invoke(DemoData data, AnalysisContext context) {// 每行數據解析后調用dataList.add(data);System.out.println("解析第" + context.readRowHolder().getRowIndex() + "行:" + data.getName());}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 所有數據解析完成后調用System.out.println("共解析" + dataList.size() + "行數據");}@Overridepublic void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {// 表頭解析后調用System.out.println("解析表頭:" + headMap.get(0).getStringValue());}@Overridepublic void onException(Exception exception, AnalysisContext context) {// 異常處理System.err.println("解析失敗,行號:" + context.readRowHolder().getRowIndex());exception.printStackTrace();}public List<DemoData> getDataList() {return dataList;}
}
?完整代碼:
????????
public String importUser(@RequestParam("file") MultipartFile file) {try {EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(userService)).sheet().doRead();return "導入并更新成功";} catch (Exception e) {return "導入并更新失敗:" + e.getMessage();}}