SpringBoot implements Excel import and export performance explosion table, which is elegant enough to use

Posted by newbreed65 on Sat, 08 Jan 2022 10:41:15 +0100

 2022-01-06 12:12

The following article comes from macrozheng, the author of dream de starry sky

macrozheng.

Focus on Java technology sharing, covering SpringBoot, SpringCloud, Docker, middleware and other practical technologies. The author is Github open source project mall (40K+Star).

Hello, I'm the second brother!

Operating Excel to import and export is a very common requirement. A very useful tool was introduced earlier EasyPoi  . Some readers suggested that in the case of a large amount of data, EasyPoi occupies a large memory and its performance is not good enough. Today, I recommend a better Excel import and export tool EasyExcel. I hope it will be helpful to you!

Introduction to EasyExcel

EasyExcel is an open source Excel import and export tool from Alibaba. It has the characteristics of fast processing, small memory consumption and convenient use. It has 22k+Star on Github, which shows that it is very popular.

EasyExcel reads 75M(46W rows and 25 columns) Excel. It only needs 64M memory, takes 20s, and the speed mode can be faster!

integrate

Integrating EasyExcel in SpringBoot is very simple and requires only one dependency.

<!--EasyExcel Correlation dependency-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.0.5</version>
</dependency>

use

The use of EasyExcel and EasyPoi is very similar. They both control import and export through annotations. Next, we take the import and export of member information and order information as an example to realize the simple single table export and the complex export with one to many relationship respectively.

Simple export

We take the export of member information as an example to experience the export function of EasyExcel.

  • First, create a Member object Member to encapsulate the Member information. Here, the annotation of EasyExcel is used;

/**
 * Shopping member
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
    @ExcelProperty("ID")
    @ColumnWidth(10)
    private Long id;
    @ExcelProperty("user name")
    @ColumnWidth(20)
    private String username;
    @ExcelIgnore
    private String password;
    @ExcelProperty("nickname")
    @ColumnWidth(20)
    private String nickname;
    @ExcelProperty("date of birth")
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd")
    private Date birthday;
    @ExcelProperty("cell-phone number")
    @ColumnWidth(20)
    private String phone;
    @ExcelIgnore
    private String icon;
    @ExcelProperty(value = "Gender", converter = GenderConverter.class)
    @ColumnWidth(10)
    private Integer gender;
}
  • The above code uses the core annotation of EasyExcel. Let's understand the following respectively:

    • @ExcelProperty: core annotation. The value property can be used to set the header name, and the converter property can be used to set the type converter;

    • @ColumnWidth: used to set the width of table columns;

    • @DateTimeFormat: used to set the date conversion format.

  • In EasyExcel, if you want to convert an enumeration type to a string (for example, in the gender attribute, 0 - > male, 1 - > female), you need to customize the converter. The following is the implementation of the customized GenderConverter code;

/**
 * excel Gender converter
 * Created by macro on 2021/12/29.
 */
public class GenderConverter implements Converter<Integer> {
    @Override
    public Class<?> supportJavaTypeKey() {
        //Object property type
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        //CellData property type
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Integer convertToJavaData(ReadConverterContext<?> context) throws Exception {
        //CellData to object properties
        String cellStr = context.getReadCellData().getStringValue();
        if (StrUtil.isEmpty(cellStr)) return null;
        if ("male".equals(cellStr)) {
            return 0;
        } else if ("female".equals(cellStr)) {
            return 1;
        } else {
            return null;
        }
    }

    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {
        //Object properties to CellData
        Integer cellValue = context.getValue();
        if (cellValue == null) {
            return new WriteCellData<>("");
        }
        if (cellValue == 0) {
            return new WriteCellData<>("male");
        } else if (cellValue == 1) {
            return new WriteCellData<>("female");
        } else {
            return new WriteCellData<>("");
        }
    }
}
  • Next, we add an interface in the Controller to export the member list to excel. We also need to set the attribute of downloading Excel for the response header. The specific code is as follows;

/**
 * EasyExcel Import and export test Controller
 * Created by macro on 2021/10/12.
 */
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel Import and export tests")
@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, "Member list");
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        EasyExcel.write(response.getOutputStream())
                .head(Member.class)
                .excelType(ExcelTypeEnum.XLSX)
                .sheet("Member list")
                .doWrite(memberList);
    }
    
  /**
   * Set excel download response header attribute
   */
  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");
  }
}
  • Run the project and test the interface through Swagger. Note that the access interface in Swagger cannot be downloaded directly. You need to click the download button in the return result. Access address: http://localhost:8088/swagger-ui/

  • After downloading, check the next file. A standard Excel file has been exported.

Simple import

Next, let's take the import of member information as an example to experience the import function of EasyExcel.

  • Add an interface for importing Member information in the Controller. Note that the @ RequestPart annotation is used to modify the file upload parameters, otherwise the upload button cannot be displayed in Swagger;

/**
 * EasyExcel Import and export test Controller
 * Created by macro on 2021/10/12.
 */
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel Import and export tests")
@RequestMapping("/easyExcel")
public class EasyExcelController {
    
    @SneakyThrows
    @ApiOperation("from Excel Import member list")
    @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);
    }
}
  • Then test the interface in Swagger and select the Excel file exported before. After successful import, the parsed data will be returned.

Complex export

Of course, EasyExcel can also realize more complex export, such as exporting an order list nested with commodity information. Let's implement it below!

Implementation using EasyPoi

We have used EasyPoi to realize this function before. Because EasyPoi originally supports the export of nested objects, it can be realized directly by using the built-in @ ExcelCollection annotation, which is very convenient and in line with the object-oriented idea.

Looking for solutions

Since EasyExcel itself does not support this one to many information export, we have to implement it by ourselves. Here is a common method I often use to quickly find solutions.

We can search directly from the issues of open source projects. For example, if we search the next one to many, we will directly find whether there is an issue that can export a more elegant one to many scheme.

From the reply from the issue, we can find that the project maintainer suggests creating a user-defined merge strategy to implement it. The brother who replied has given the implementation code. Next, we will use this scheme to implement it.

Solution ideas

Why can a custom cell merge policy export one to many list information? First, let's look at Excel that tiles nested data without merging and exporting.

After reading it, we can easily understand the solution. As long as we merge the columns that need to be merged in the columns with the same order ID, we can export this one to many nested information.

Implementation process

  • First, we have to tile the original nested order commodity information and create a special export object OrderData, which contains order and commodity information. The secondary header can be realized by setting the value of @ ExcelProperty as an array;

/**
 * Order export
 * Created by macro on 2021/12/30.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class OrderData {
    @ExcelProperty(value = "order ID")
    @ColumnWidth(10)
    @CustomMerge(needMerge = true, isPk = true)
    private String id;
    @ExcelProperty(value = "Order code")
    @ColumnWidth(20)
    @CustomMerge(needMerge = true)
    private String orderSn;
    @ExcelProperty(value = "Creation time")
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd")
    @CustomMerge(needMerge = true)
    private Date createTime;
    @ExcelProperty(value = "Receiving address")
    @CustomMerge(needMerge = true)
    @ColumnWidth(20)
    private String receiverAddress;
    @ExcelProperty(value = {"Commodity information", "Commodity code"})
    @ColumnWidth(20)
    private String productSn;
    @ExcelProperty(value = {"Commodity information", "Trade name"})
    @ColumnWidth(20)
    private String name;
    @ExcelProperty(value = {"Commodity information", "Product title"})
    @ColumnWidth(30)
    private String subTitle;
    @ExcelProperty(value = {"Commodity information", "Brand name"})
    @ColumnWidth(20)
    private String brandName;
    @ExcelProperty(value = {"Commodity information", "commodity price"})
    @ColumnWidth(20)
    private BigDecimal price;
    @ExcelProperty(value = {"Commodity information", "Quantity of goods"})
    @ColumnWidth(20)
    private Integer count;
}
  • Then convert the original nested Order object list into OrderData object list;

/**
 * EasyExcel Import and export test Controller
 * Created by macro on 2021/10/12.
 */
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel Import and export tests")
@RequestMapping("/easyExcel")
public class EasyExcelController {
    private List<OrderData> convert(List<Order> orderList) {
        List<OrderData> result = new ArrayList<>();
        for (Order order : orderList) {
            List<Product> productList = order.getProductList();
            for (Product product : productList) {
                OrderData orderData = new OrderData();
                BeanUtil.copyProperties(product,orderData);
                BeanUtil.copyProperties(order,orderData);
                result.add(orderData);
            }
        }
        return result;
    }
}
  • Then create a custom annotation CustomMerge to mark which attributes need to be merged and which is the primary key;

/**
 * The user-defined annotation is used to determine whether to merge and the primary key of the merge
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {

    /**
     * Do you want to merge cells
     */
    boolean needMerge() default false;

    /**
     * Whether it is a primary key, that is, the rows with the same field are merged
     */
    boolean isPk() default false;
}
  • Then create a custom cell merge strategy class CustomMergeStrategy. When the primary keys of two columns in Excel are the same, merge the columns marked to be merged;

/**
 * Custom cell merge policy
 */
public class CustomMergeStrategy implements RowWriteHandler {
    /**
     * Primary key subscript
     */
    private Integer pkIndex;

    /**
     * Subscript set of columns to be merged
     */
    private List<Integer> needMergeColumnIndex = new ArrayList<>();

    /**
     * DTO data type
     */
    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 it is a title, return directly
        if (isHead) {
            return;
        }

        //Get current sheet
        Sheet sheet = writeSheetHolder.getSheet();

        //Get Title row
        Row titleRow = sheet.getRow(0);

        if (null == pkIndex) {
            this.lazyInit(writeSheetHolder);
        }

        //Judge whether to merge with the previous line
        //Cannot merge with header, only between data rows
        if (row.getRowNum() <= 1) {
            return;
        }
        //Get previous row data
        Row lastRow = sheet.getRow(row.getRowNum() - 1);
        //If the row and the previous row are the same type of data (judged by the primary key field), they need to be merged
        if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
            for (Integer needMerIndex : needMergeColumnIndex) {
                CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(),
                        needMerIndex, needMerIndex);
                sheet.addMergedRegionUnsafe(cellRangeAddress);
            }
        }
    }

    /**
     * Initialize the primary key subscript and the subscript of the field to be merged
     */
    private void lazyInit(WriteSheetHolder writeSheetHolder) {

        //Get current sheet
        Sheet sheet = writeSheetHolder.getSheet();

        //Get Title row
        Row titleRow = sheet.getRow(0);
        //Gets the type of DTO
        Class<?> eleType = this.elementType;

        //Get all properties of DTO
        Field[] fields = eleType.getDeclaredFields();

        //Traverse all fields. Because excel is built based on DTO fields, the number of fields > = the number of columns in Excel
        for (Field theField : fields) {
            //Get the @ ExcelProperty annotation, which is used to get the subscript of the column corresponding to the field in excel
            ExcelProperty easyExcelAnno = theField.getAnnotation(ExcelProperty.class);
            //If it is blank, it means that the field does not need to be imported into excel, and the next field will be processed directly
            if (null == easyExcelAnno) {
                continue;
            }
            //Gets a custom annotation for merging cells
            CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);

            //Default do not merge without @ CustomMerge annotation
            if (null == customMerge) {
                continue;
            }

            for (int index = 0; index < fields.length; index++) {
                Cell theCell = titleRow.getCell(index);
                //When the configuration does not require export, the returned is null. Here, make a judgment to prevent NPE
                if (null == theCell) {
                    continue;
                }
                //Match the field with the header of excel
                if (easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {
                    if (customMerge.isPk()) {
                        pkIndex = index;
                    }

                    if (customMerge.needMerge()) {
                        needMergeColumnIndex.add(index);
                    }
                }
            }
        }

        //If no primary key is specified, an exception occurs
        if (null == this.pkIndex) {
            throw new IllegalStateException("use@CustomMerge Annotations must specify a primary key");
        }

    }
}
  • Next, add an interface to export the order list in the Controller and register our custom merge strategy;

/**
 * EasyExcel Import and export test Controller
 * Created by macro on 2021/10/12.
 */
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel Import and export tests")
@RequestMapping("/easyExcel")
public class EasyExcelController {
    
    @SneakyThrows
    @ApiOperation(value = "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, "Order list");
        EasyExcel.write(response.getOutputStream())
                .head(OrderData.class)
                .registerWriteHandler(new CustomMergeStrategy(OrderData.class))
                .excelType(ExcelTypeEnum.XLSX)
                .sheet("Order list")
                .doWrite(orderDataList);
    }
}
  • Access interface test in Swagger and export Excel corresponding to order list;

  • After downloading, check the file. Since EasyExcel needs to be implemented by itself, it is much more troublesome than using EasyPoi before.

Other use

Because the official document of EasyExcel is relatively simple, if you want to use it more deeply, I suggest you take a look at the official Demo.

summary

I have experienced an EasyExcel, which is very convenient to use and has excellent performance. However, the common one to many export implementation is more complex, and the function is not as good as that EasyPoi Powerful. If the amount of data exported from Excel is small, you can use EasyPoi. If the amount of data is large and you care about performance, you'd better use EasyExcel.

reference material

  • Project address: https://github.com/alibaba/easyexcel

  • Official documents: https://www.yuque.com/easyexcel/doc/easyexcel

Silent King II

The technical article is easy to understand and humorous.

661 original content

official account

Recommended reading

Nothing makes me stay - except for purpose, even if there are roses, shade and quiet harbor on the shore, I am not a boat.

Topics: Java Spring Spring Boot