相信如今不少搞後端的同窗大部分作的都是後臺管理系統,那麼管理系統就確定免不了 Excel
的導出導入功能,今天咱們就來介紹一下 Java
如何實現 Excel
的導入導出功能。前端
Java領域解析,生成Excel比較有名的框架有Apache poi,Jxl等,但他們都存在一個嚴重的問題就是很是的耗內存,若是你的系統併發量不大的話可能還行,可是一旦併發上來後必定會OOM或者JVM頻繁的full gc.java
EasyExcel
是阿里巴巴開源的一個excel處理框架,以使用簡單,節省內存著稱,今天咱們來使用阿里巴巴開源的EasyExcel
框架來實現Excel
的導入導出功能。git
官方文檔:EasyExcelgithub
本文主要有如下幾個知識點:spring
從Excel讀取數據
導出數據到Excel
Excel模板填充
首先第一步得先導入EasyExcel
的Jar包apache
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.4</version> </dependency> <!--xls--> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency>
接下來看看如何導出數據到到Excel
中,有兩種寫法,一種是不建立對象的寫入,另外一種是根據對象寫入。segmentfault
@SpringBootTest class Tests { /* * 不建立對象的寫 */ @Test public void test() { // 生成Excel路徑 String fileName = "C:\\Users\\likun\\Desktop\\測試.xlsx"; EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList()); } private List<List<String>> head() { List<List<String>> list = new ArrayList<>(); List<String> head0 = new ArrayList<>(); head0.add("姓名"); List<String> head1 = new ArrayList<>(); head1.add("年齡"); List<String> head2 = new ArrayList<>(); head2.add("生日"); list.add(head0); list.add(head1); list.add(head2); return list; } private List<List<Object>> dataList() { List<List<Object>> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { List<Object> data = new ArrayList<>(); data.add("張三"); data.add(25); data.add(new Date()); list.add(data); } return list; } }
代碼很簡單,核心就一句代碼:後端
EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList());
head()
用來放表頭數據,dataList()
用來放每一行的數據。併發
看下效果圖:app
若是想設置自動列寬能夠這樣子:
EasyExcel.write(fileName).head(head()).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet("模板").doWrite(dataList());
效果圖:
接下來是根據對象導入Excel
,首先咱們要定義一個對象:
@Data public class User { @ExcelProperty("姓名") private String name; @ExcelProperty("性別") private String sex; @ExcelProperty("年齡") private Integer age; @ExcelProperty("身份證") private String cardid; }
使用@ExcelProperty
註解來指定標題名稱
@SpringBootTest class Tests { @Test public void test() { // 生成Excel路徑 String fileName = "C:\\Users\\likun\\Desktop\\測試.xlsx"; EasyExcel.write(fileName, User.class).sheet("模板").doWrite(data()); } private List<User> data() { List<User> userList = new ArrayList<>(); User user; for (int i = 1; i <= 10; i++) { user = new User(); user.setName("張三" + i); user.setSex("男"); user.setAge(i); user.setCardid("440582xxxx"); userList.add(user); } return userList; } }
使用對象導出數據也是很簡單,只要doWrite
方法傳入咱們的對象集合就能夠了。
效果圖:
若是對象裏面有些字段咱們並不想導出到Excel
中,只要使用@ExcelIgnore
註解就能夠了:
/* 忽略這個字段 */ @ExcelIgnore private String filed;
若是咱們想導出數據到指定的列中該如何設置呢?
@Data public class User { @ExcelProperty(value = "姓名", index = 0) private String name; @ExcelProperty(value = "性別", index = 1) private String sex; @ExcelProperty(value = "年齡", index = 2) private Integer age; @ExcelProperty(value = "身份證", index = 4) private String cardid; }
@ExcelProperty
的index
能夠指定導出的列索引,來看下效果圖:
不少時候Excel
裏會有不少複雜的表頭,那麼如何實現呢?
@Data public class User { @ExcelProperty("姓名") private String name; @ExcelProperty("性別") private String sex; @ExcelProperty("年齡") private Integer age; @ExcelProperty("身份證") private String cardid; @ExcelProperty({"普通高等學校全日制教育", "學歷"}) private String kultur; @ExcelProperty({"普通高等學校全日制教育", "學位"}) private String degree; @ExcelProperty({"普通高等學校全日制教育", "專業"}) private String major; @ExcelProperty({"普通高等學校全日制教育", "得到學歷時間"}) private String graduatetime; @ExcelProperty({"普通高等學校全日制教育", "畢業院校"}) private String school; }
很簡單再也不細說,直接來看效果圖:
咱們上面都是生成新的數據寫到Excel
,若是說如今有一個模板文件,就像下面這種:
模板文件裏面已經有一條數據了,那咱們怎麼在後面添加數據呢?
其實很簡單:
String templateName = "C:\\Users\\likun\\Desktop\\模板.xlsx"; String fileName = "C:\\Users\\likun\\Desktop\\測試.xlsx"; EasyExcel.write(fileName).withTemplate(templateName).sheet("模板").doWrite(data());
使用withTemplate(templateName)
方法傳入模板路徑就能夠了,有個地方須要注意的是:這裏的write
方法只傳文件路徑,不傳對象,若是傳了對象又會生成新的表頭,效果圖以下:
注意:EasyExcel
導出數據都是生成新的 Excel
文件,而不是在原來的文件上修改。
這裏參考官方文檔的例子:
@Data @ContentRowHeight(10) @HeadRowHeight(20) @ColumnWidth(25) public class WidthAndHeightData { @ExcelProperty("字符串標題") private String string; @ExcelProperty("日期標題") private Date date; /** * 寬度爲50 */ @ColumnWidth(50) @ExcelProperty("數字標題") private Double doubleData; }
都是加個註解的事兒,這裏再也不細說。
@ContentLoopMerge(eachRow = 2) @ExcelProperty("姓名") private String name;
@ContentLoopMerge(eachRow = 2)
表示姓名這一列每隔兩行就進行合併
效果圖:
@ContentLoopMerge
還有一個columnExtend
屬性,能夠對列進行合併
@ContentLoopMerge(eachRow = 2,columnExtend = 4) @ExcelProperty("姓名") private String name;
效果圖:
固然這些只是簡單的合併,若是須要複雜的合併能夠本身定義一個策略,具體實現能夠參考官方文檔。
有時候咱們會有一些特殊的需求,好比說咱們想給某個單元格設置下拉框,那麼咱們能夠經過自定義攔截器來實現,據圖代碼以下:
public class CustomSheetWriteHandler implements SheetWriteHandler { @Override public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(2, 2, 0, 0); DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[] {"測試1", "測試2"}); DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList); writeSheetHolder.getSheet().addValidationData(dataValidation); } }
咱們須要定義一個攔截器實現SheetWriteHandler
方法,而後重寫攔截方法,在afterSheetCreate
方法裏面對第二行第一列的單元格設置下拉框,而後只要註冊上去就能夠了:
.registerWriteHandler(new CustomSheetWriteHandler())
效果圖:
還有一個常見的業務需求就是模板填充,網上大部分都是簡單的填充,今天來看一下複雜模板的填充,下面是模板:
要想使用EasyExcel
填充模板,咱們須要在添加佔位符{字段名},表格的須要用{自定義名稱.字段名},來簡單看下代碼:
首先咱們須要爲表格定義一個簡歷對象:
@Data public class WorkHistory { private String ubegintime; private String uendtime; private String uworkcomp; private String uworkdesc; }
接下來開始填充數據:
@Test public void test() { // 生成Excel路徑 String filePath = "C:\\Users\\likun\\Desktop\\測試.xlsx"; String templatePath = "C:\\Users\\likun\\Desktop\\模板.xlsx"; ExcelWriter excelWriter = EasyExcel.write(filePath).withTemplate(templatePath).build(); WriteSheet writeSheet = EasyExcel.writerSheet().build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); // 填充數據 Map<String, Object> map = new HashMap<>(64); map.put("uname", "張三"); map.put("usex", "男"); map.put("ubirthday", "2020.10.01"); map.put("ucardid", "440582xxxxxxxx"); map.put("umarriage", "未婚"); map.put("unation", "漢族"); map.put("unative", "廣東xxxx"); map.put("ubirthplace", "廣東xxxx"); map.put("upolity", "團員"); map.put("uworktime", "2020.05.15"); map.put("uhealth", "良好"); excelWriter.fill(map, writeSheet); excelWriter.fill(new FillWrapper("data1", data1()), fillConfig, writeSheet); // 別忘記關閉流 excelWriter.finish(); } private List<WorkHistory> data1() { List<WorkHistory> list = new ArrayList<>(); WorkHistory workHistory; for (int i = 1; i <= 3; i++) { workHistory = new WorkHistory(); workHistory.setUbegintime("2020.05.01"); workHistory.setUendtime("2020.05.01"); workHistory.setUworkcomp("xxx公司"); workHistory.setUworkdesc("後勤"); list.add(workHistory); } return list; }
填充數據主要是下面兩行代碼:
excelWriter.fill(map, writeSheet); excelWriter.fill(new FillWrapper("data1", data1()), fillConfig, writeSheet)
上面是填充字段,下面是填充咱們的表格,注意這裏data1
的名字要和模板裏面的名字同樣。
forceNewRow(Boolean.TRUE)
表明表格每次都會從新生成新的一行,而不是使用下面的空行。
看下填充的效果圖:
能夠看到數據已經填充進去了,可是表格單元格格式不符合咱們的預期效果,雖然 EasyExcel
也提供了自定義策略來合併單元格,可是由於是經過回調方法觸發,很差控制,所以咱們這裏使用原生的 Apache POI
來實現:
...... FileInputStream inputStream = new FileInputStream(new File(filePath)); XSSFWorkbook workbook = new XSSFWorkbook(inputStream); XSSFSheet sheet = workbook.getSheetAt(0); // 合併列 sheet.addMergedRegion(new CellRangeAddress(8, 8, 1, 2)); sheet.addMergedRegion(new CellRangeAddress(8, 8, 3, 4)); sheet.addMergedRegion(new CellRangeAddress(8, 8, 5, 9)); sheet.addMergedRegion(new CellRangeAddress(8, 8, 10, 11)); sheet.addMergedRegion(new CellRangeAddress(9, 9, 1, 2)); sheet.addMergedRegion(new CellRangeAddress(9, 9, 3, 4)); sheet.addMergedRegion(new CellRangeAddress(9, 9, 5, 9)); sheet.addMergedRegion(new CellRangeAddress(9, 9, 10, 11)); // 合併行 sheet.addMergedRegion(new CellRangeAddress(6, 9, 0, 0)); String mergeExcelPath="C:\\Users\\likun\\Desktop\\合併單元格.xlsx"; FileOutputStream outputStream = new FileOutputStream(mergeExcelPath); workbook.write(outputStream); outputStream.flush();
核心代碼是就是
sheet.addMergedRegion(new CellRangeAddress(row1, row2, col1, col2));
來看下效果圖吧:
能夠看到單元格已經合併了,如今就是合併後沒有邊框,固然也有提供API供咱們使用,
RegionUtil.setBorderBottom(BorderStyle.THIN, new CellRangeAddress(8, 8, 1, 2), sheet);
能夠看到單元格已經設置了邊框,至於其它的請大夥自行設置,這邊只作個簡單演示。
EasyExcel
也支持頭像導出,可是隻能插入到一個單元格里面,所以咱們仍是用原生API來插入頭像:
// 轉換成流 ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream(); BufferedImage bufferImg = ImageIO.read(new File("C:\\Users\\likun\\Pictures\\頭像\\1.jpg")); ImageIO.write(bufferImg, "jpg", byteArrayOut); XSSFDrawing patriarch = sheet.createDrawingPatriarch(); XSSFClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) 11, 2, (short) 12, 6); anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE); patriarch.createPicture(anchor, workbook.addPicture(byteArrayOut.toByteArray(), HSSFWorkbook.PICTURE_TYPE_JPEG));
只要用XSSFClientAnchor
配置好參數,就能在指定的位置插入圖片。前四個參數是偏移量,默認爲0就能夠了,後四個就是圖片邊緣的單元格位置,具體細節這裏再也不細說。
new XSSFClientAnchor(0, 0, 0, 0, (short) 11, 2, (short) 12, 6);
效果圖:
先來看下如何從Excel
讀取數據,首先定義一個監聽器繼承 AnalysisEventListener
類:
@EqualsAndHashCode(callSuper = true) @Data public class ExcelListener extends AnalysisEventListener<Object> { private static final Logger LOGGER = LoggerFactory.getLogger(ExcelListener.class); /** * 自定義用於暫時存儲data */ private List<JSONObject> dataList = new ArrayList<>(); /** * 導入表頭 */ private Map<String, Integer> importHeads = new HashMap<>(16); /** * 這個每一條數據解析都會來調用 */ @Override public void invoke(Object data, AnalysisContext context) { String headStr = JSON.toJSONString(data); dataList.add(JSONObject.parseObject(headStr)); } /** * 這裏會一行行的返回頭 */ @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { for (Integer key : headMap.keySet()) { if (importHeads.containsKey(headMap.get(key))) { continue; } importHeads.put(headMap.get(key), key); } } /** * 全部數據解析完成了 都會來調用 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { LOGGER.info("Excel解析完畢"); } }
當解析每一條數據時都會調用invoke
方法,invokeHeadMap
方法會返回咱們的表格頭,當全部數據都解析完畢時最後會調用doAfterAllAnalysed
方法。
上面代碼是我項目裏面用的,大家也能夠根據本身需求編寫,上面用JSONObject集合來存放Excel中每一條數據,用一個Map
存放咱們的表格頭。
那麼有了監聽器以後該如何使用呢?
這裏有個很重要的點就是 監聽器不能被spring管理,要每次讀取excel都要new.
看下如何讀取前端發送過來的Excel
文件:
@PostMapping("upload") @ResponseBody public String upload(MultipartFile file) throws IOException { ExcelListener excelListener = new ExcelListener(); EasyExcel.read(file.getInputStream(), excelListener).sheet().doRead(); ...... }
只要調用read
方法就能夠讀取數據,那麼接下來只要去拿到數據就能夠了。
好比讀取表格頭數據:
Map<String, Integer> importHeads = excelListener.getImportHeads();
或者讀取數據集合
List<JSONObject> dataList = excelListener.getDataList();
固然咱們也能夠根據文件路徑去讀取
@Test public void test() { // 生成Excel路徑 String fileName = "C:\\Users\\likun\\Desktop\\測試.xlsx"; ExcelListener excelListener = new ExcelListener(); EasyExcel.read(fileName, excelListener).sheet().doRead(); // 表格頭數據 Map<String, Integer> importHeads = excelListener.getImportHeads(); System.out.println(importHeads); // 每一行數據 List<JSONObject> dataList = excelListener.getDat![image]aList(); for (JSONObject object : dataList) { System.out.println(object); } }
這是咱們要讀取的Excel
數據
來看下讀取到的數據:
上面的讀取是不使用對象的讀取方式,也有使用對象去讀取的方式,由於和上面導出的差很少這裏就再也不展開描述沒若是有須要的同窗能夠參考官方文檔
代碼已上傳Github:https://github.com/chenwuguii/wugui
今天有關Java操做Excel
的知識點就暫時到這裏,若是有什麼不對的地方請多多指教!