前言
置信当初很多搞后端的同学大部分做的都是后盾管理系统,那么管理系统就必定免不了 Excel
的导出导入性能,明天咱们就来介绍一下 Java
如何实现 Excel
的导入导出性能。
Java畛域解析,生成Excel比拟有名的框架有Apache poi,Jxl等,但他们都存在一个重大的问题就是十分的耗内存,如果你的零碎并发量不大的话可能还行,然而一旦并发上来后肯定会OOM或者JVM频繁的full gc.
EasyExcel
是阿里巴巴开源的一个excel解决框架,以应用简略,节俭内存著称,明天咱们来应用阿里巴巴开源的EasyExcel
框架来实现Excel
的导入导出性能。
官网文档:EasyExcel
本文次要有以下几个知识点:
从Excel读取数据
导出数据到Excel
Excel模板填充
注释
首先第一步得先导入EasyExcel
的Jar包
<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.
接下来看看如何导出数据到到Excel
中,有两种写法,一种是不创建对象的写入,另一种是依据对象写入。
- 不创建对象的写入
@SpringBootTestclass 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()
用来放每一行的数据。
看下效果图:
如果想设置主动列宽能够这样子:
EasyExcel.write(fileName).head(head()).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet("模板").doWrite(dataList());
效果图:
- 依据对象写入
接下来是依据对象导入Excel
,首先咱们要定义一个对象:
@Datapublic class User { @ExcelProperty("姓名") private String name; @ExcelProperty("性别") private String sex; @ExcelProperty("年龄") private Integer age; @ExcelProperty("身份证") private String cardid;}
应用@ExcelProperty
注解来指定题目名称
@SpringBootTestclass 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;
写入指定的列
如果咱们想导出数据到指定的列中该如何设置呢?
@Datapublic 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
里会有很多简单的表头,那么如何实现呢?
@Datapublic 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())
效果图:
Excel模板填充
还有一个常见的业务需要就是模板填充,网上大部分都是简略的填充,明天来看一下简单模板的填充,上面是模板:
要想应用EasyExcel
填充模板,咱们须要在增加占位符{字段名},表格的须要用{自定义名称.字段名},来简略看下代码:
首先咱们须要为表格定义一个简历对象:
@Datapublic 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读取数据
先来看下如何从Excel
读取数据,首先定义一个监听器继承 AnalysisEventListener
类:
@EqualsAndHashCode(callSuper = true)@Datapublic 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
的知识点就临时到这里,如果有什么不对的中央请多多指教!