Boost Your SpringBoot Apps with EasyExcel: Fast, Low-Memory Excel Import/Export
This article introduces Alibaba's EasyExcel library, demonstrates how to integrate it into SpringBoot, and provides step‑by‑step code examples for simple export, import, and complex one‑to‑many export using custom merge strategies, highlighting its performance advantages over EasyPoi.
Operating Excel import/export is a common requirement; EasyExcel is a high‑performance, low‑memory Alibaba open‑source tool that outperforms EasyPoi for large data sets.
EasyExcel Introduction
EasyExcel is an Alibaba open‑source Excel import/export library known for speed, low memory consumption, and ease of use. It already has over 22k stars on GitHub, indicating its popularity.
EasyExcel can read a 75 MB file (460 000 rows × 25 columns) using only 64 MB of memory in 20 seconds; the ultra‑fast mode is even quicker.
Integration
Integrating EasyExcel into a SpringBoot project is straightforward—just add a single Maven dependency.
<code><!-- EasyExcel dependency -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
</code>Usage
EasyExcel and EasyPoi share a similar annotation‑driven usage model. Below are examples of simple single‑table export, import, and a complex one‑to‑many export.
Simple Export
Exporting member information demonstrates EasyExcel's basic export capability.
First, create a member entity class
Memberand annotate its fields.
<code>/**
* Shopping member
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
@ExcelProperty("ID")
@ColumnWidth(10)
private Long id;
@ExcelProperty("用户名")
@ColumnWidth(20)
private String username;
@ExcelIgnore
private String password;
@ExcelProperty("昵称")
@ColumnWidth(20)
private String nickname;
@ExcelProperty("出生日期")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
@ExcelProperty("手机号")
@ColumnWidth(20)
private String phone;
@ExcelIgnore
private String icon;
@ExcelProperty(value = "性别", converter = GenderConverter.class)
@ColumnWidth(10)
private Integer gender;
}
</code>The core annotations are:
@ExcelProperty – sets the column header (value) and optional converter.
@ColumnWidth – defines column width.
@DateTimeFormat – formats date fields.
To map enum values (e.g., 0→"男", 1→"女"), implement a custom converter
GenderConverter.
<code>/**
* Excel gender converter
*/
public class GenderConverter implements Converter<Integer> {
@Override
public Class<?> supportJavaTypeKey() { return Integer.class; }
@Override
public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; }
@Override
public Integer convertToJavaData(ReadConverterContext<?> context) throws Exception {
String cellStr = context.getReadCellData().getStringValue();
if (StrUtil.isEmpty(cellStr)) return null;
if ("男".equals(cellStr)) return 0;
if ("女".equals(cellStr)) return 1;
return null;
}
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {
Integer cellValue = context.getValue();
if (cellValue == null) return new WriteCellData<>("");
if (cellValue == 0) return new WriteCellData<>("男");
if (cellValue == 1) return new WriteCellData<>("女");
return new WriteCellData<>("");
}
}
</code>Add a controller endpoint to export the member list and set the response headers for file download.
<code>@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel import/export demo")
@RequestMapping("/easyExcel")
public class EasyExcelController {
@SneakyThrows(IOException.class)
@ApiOperation(value = "Export member list Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(HttpServletResponse response) {
setExcelRespProp(response, "会员列表");
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
EasyExcel.write(response.getOutputStream())
.head(Member.class)
.excelType(ExcelTypeEnum.XLSX)
.sheet("会员列表")
.doWrite(memberList);
}
private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
}
</code>Test the endpoint via Swagger; click the download button in the response to obtain the Excel file.
After download, you will see a standard Excel file containing the exported data.
Simple Import
Importing member information shows EasyExcel's import capability.
<code>@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel import/export demo")
@RequestMapping("/easyExcel")
public class EasyExcelController {
@SneakyThrows
@ApiOperation("Import member list from Excel")
@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
@ResponseBody
public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
List<Member> memberList = EasyExcel.read(file.getInputStream())
.head(Member.class)
.sheet()
.doReadSync();
return CommonResult.success(memberList);
}
}
</code>Test the import endpoint in Swagger by uploading the previously exported Excel file; the parsed data will be returned.
Complex Export (One‑to‑Many)
EasyExcel can also handle complex exports such as an order list with nested product information.
Using EasyPoi
EasyPoi supports nested objects out of the box via the
@ExcelCollectionannotation.
Finding a Solution
Since EasyExcel does not natively support one‑to‑many export, we search the project's issues for a suitable approach.
Search the issues for "一对多" to find discussions about custom merge strategies.
Solution Idea
By flattening nested data and merging cells where the order ID is identical, we can achieve a one‑to‑many export.
Implementation Process
Create a flat export DTO
OrderDatathat contains order and product fields, using
@ExcelPropertywith array values for multi‑level headers and
@CustomMergeto mark mergeable columns.
<code>@Data
@EqualsAndHashCode(callSuper = false)
public class OrderData {
@ExcelProperty("订单ID")
@ColumnWidth(10)
@CustomMerge(needMerge = true, isPk = true)
private String id;
@ExcelProperty("订单编码")
@ColumnWidth(20)
@CustomMerge(needMerge = true)
private String orderSn;
@ExcelProperty("创建时间")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
@CustomMerge(needMerge = true)
private Date createTime;
@ExcelProperty("收货地址")
@CustomMerge(needMerge = true)
@ColumnWidth(20)
private String receiverAddress;
@ExcelProperty({"商品信息", "商品编码"})
@ColumnWidth(20)
private String productSn;
@ExcelProperty({"商品信息", "商品名称"})
@ColumnWidth(20)
private String name;
@ExcelProperty({"商品信息", "商品标题"})
@ColumnWidth(30)
private String subTitle;
@ExcelProperty({"商品信息", "品牌名称"})
@ColumnWidth(20)
private String brandName;
@ExcelProperty({"商品信息", "商品价格"})
@ColumnWidth(20)
private BigDecimal price;
@ExcelProperty({"商品信息", "商品数量"})
@ColumnWidth(20)
private Integer count;
}
</code>Convert the original
Orderlist to a list of
OrderDataby flattening each product.
<code>private List<OrderData> convert(List<Order> orderList) {
List<OrderData> result = new ArrayList<>();
for (Order order : orderList) {
for (Product product : order.getProductList()) {
OrderData orderData = new OrderData();
BeanUtil.copyProperties(product, orderData);
BeanUtil.copyProperties(order, orderData);
result.add(orderData);
}
}
return result;
}
</code>Define the custom annotation
CustomMergeto indicate merge requirements.
<code>@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {
/** Whether the cell needs merging */
boolean needMerge() default false;
/** Whether this field is the primary key for merging */
boolean isPk() default false;
}
</code>Implement a merge strategy
CustomMergeStrategythat merges cells sharing the same primary‑key value.
<code>public class CustomMergeStrategy implements RowWriteHandler {
private Integer pkIndex;
private List<Integer> needMergeColumnIndex = new ArrayList<>();
private Class<?> elementType;
public CustomMergeStrategy(Class<?> elementType) { this.elementType = elementType; }
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
Row row, Integer relativeRowIndex, Boolean isHead) {
if (isHead) return;
Sheet sheet = writeSheetHolder.getSheet();
if (pkIndex == null) lazyInit(writeSheetHolder);
if (row.getRowNum() <= 1) return;
Row lastRow = sheet.getRow(row.getRowNum() - 1);
if (lastRow.getCell(pkIndex).getStringCellValue()
.equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
for (Integer colIdx : needMergeColumnIndex) {
CellRangeAddress range = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(), colIdx, colIdx);
sheet.addMergedRegionUnsafe(range);
}
}
}
private void lazyInit(WriteSheetHolder writeSheetHolder) {
Sheet sheet = writeSheetHolder.getSheet();
Row titleRow = sheet.getRow(0);
Field[] fields = elementType.getDeclaredFields();
for (Field field : fields) {
ExcelProperty excelProp = field.getAnnotation(ExcelProperty.class);
if (excelProp == null) continue;
CustomMerge customMerge = field.getAnnotation(CustomMerge.class);
if (customMerge == null) continue;
for (int i = 0; i < fields.length; i++) {
Cell cell = titleRow.getCell(i);
if (cell == null) continue;
if (excelProp.value()[0].equalsIgnoreCase(cell.getStringCellValue())) {
if (customMerge.isPk()) pkIndex = i;
if (customMerge.needMerge()) needMergeColumnIndex.add(i);
}
}
}
if (pkIndex == null) throw new IllegalStateException("@CustomMerge must specify a primary key");
}
}
</code>Add a controller endpoint to export the order list using the custom merge strategy.
<code>@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel import/export demo")
@RequestMapping("/easyExcel")
public class EasyExcelController {
@SneakyThrows
@ApiOperation("Export order list Excel")
@RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)
public void exportOrderList(HttpServletResponse response) {
List<Order> orderList = getOrderList();
List<OrderData> orderDataList = convert(orderList);
setExcelRespProp(response, "订单列表");
EasyExcel.write(response.getOutputStream())
.head(OrderData.class)
.registerWriteHandler(new CustomMergeStrategy(OrderData.class))
.excelType(ExcelTypeEnum.XLSX)
.sheet("订单列表")
.doWrite(orderDataList);
}
// setExcelRespProp same as earlier
}
</code>Test the endpoint via Swagger; the generated Excel file shows merged order ID cells for each product.
Other Uses
For deeper usage, refer to the official EasyExcel demo repository.
Summary
EasyExcel offers convenient, high‑performance Excel import/export for Java applications. While simple one‑to‑many exports require custom merge logic, the library excels with large data volumes compared to EasyPoi, which is more feature‑rich for smaller datasets.
References
Project repository: https://github.com/alibaba/easyexcel
Official documentation: https://www.yuque.com/easyexcel/doc/easyexcel
Source code for this tutorial: https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-easyexcel
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.