手把手教你使用easyexcel导出数据【附带源码】

cnblogs 2024-10-25 16:39:00 阅读 64

手把手教你使用easyexcel导出数据【附带源码】

SpringBoot集成easyexcel实现数据下载功能,解决常见字典转换、自适应行宽等问题,附带源码

一、前言

​项目开发过程中,免不了需要数据导出功能,常见的导出工具包有poi,easypoi,easyexcel,它们各有优缺点,简要来说:

    <li>poi:功能强大,使用起来相对复杂,大数据时可能导致内存溢出
  • easypoi:基于poi实现,功能强大,简单易用,大数据时可能导致内存溢出,小数据量时推荐
  • easyexcel:基于poi实现,性能更优,大数据量时推荐
  • 本文只介绍easyexcel,并介绍常见问题比如字典、样式的解决方案

二、业务流程

​介绍数据导出业务流程之前,先梳理下数据导出,什么是数据导出,为什么需要数据导出?

​我们都知道数据存储在数据库中,数据库中的数据可以通过前端页面或者APP呈现给使用者,其实通过excel将数据导出也是一种呈现方式,而且可以通过excel对数据做更复杂的处理。

​前面说了,excel导出和前端页面、APP一样都是一种呈现方式,所以前端页面和APP在将数据呈现给用户的过程中遇到的问题,像属性转换(数据库存储的name,要呈现给使用者名称)、字典转换(数据库存储的1/0,要呈现给使用者启用/停用)等等问题,excel导出一样会遇到。下面介绍下数据呈现必须要经过业务流程

  1. 获取需要导出的数据集合
  2. 数据属性和自然语言映射关系,将数据对象的属性转为使用者可以理解的自然语言
  3. 数据字典值和自然语言映射关系,将属性的字典值转为使用者可以理解的自然语言(非字典值,是什么值就呈现什么值)
  4. 数据样式和自然语言样式映射关系,将数据样式转为使用者可以理解的自然语言样式
  5. 设置表格样式
  6. 将数据集合按照上述映射关系和表格样式,写入到excel中
  7. 用户下载excel

三、实现

1、引入easyexcel、fastjson、lombok包

<code><!--easy excel-->

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>easyexcel</artifactId>

<version>4.0.3</version>

</dependency>

<!--fastjson-->

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>fastjson</artifactId>

<version>1.2.46</version>

</dependency>

<!--工具-->

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<version>1.18.0</version>

</dependency>

2、创建Json工具类

package com.yu.demo.tools;

import com.alibaba.fastjson.JSON;

import com.fasterxml.jackson.core.type.TypeReference;

import java.lang.reflect.Type;

import java.util.Map;

/**

* JSON工具类

*

* @author admin

*/

public abstract class JsonUtil {

private JsonUtil() {

}

public final static Type MAP_INTEGER_STRING = new TypeReference<Map<Integer, String>>() {

}.getType();

/**

* json串转Map(Map的value类型一致时使用)

*

* @param jsonString json串

* @return 对象

*/

public static <K, V> Map<K, V> json2Map(String jsonString, Type type) {

return JSON.parseObject(jsonString, type);

}

}

3、创建自定义字典转换注解

package com.yu.demo.tools;

import java.lang.annotation.*;

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface DictSource {

/**

* 字典类型主键

*/

String dictTypeId() default "";

/**

* 字典内容json串

*/

String dictContentJson() default "";

}

4、创建字典转换实现类

package com.yu.demo.tools;

import com.alibaba.excel.converters.Converter;

import com.alibaba.excel.enums.CellDataTypeEnum;

import com.alibaba.excel.metadata.GlobalConfiguration;

import com.alibaba.excel.metadata.data.WriteCellData;

import com.alibaba.excel.metadata.property.ExcelContentProperty;

import org.apache.poi.util.StringUtil;

import java.lang.reflect.Field;

import java.util.Map;

public class IntegerDictConverter implements Converter<Integer> {

@Override

public CellDataTypeEnum supportExcelTypeKey() {

return CellDataTypeEnum.STRING;

}

@Override

public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {

//属性值为空时,直接返回

if (value == null) {

//为空时的处理,与前端展示保持一致即可

return new WriteCellData<>("");

}

//获取添加@ExcelProperty注解且converter = IntegerDictConverter.class的属性

Field field = contentProperty.getField();

//获取该属性的DictConverter注解信息

DictSource dictSource = field.getAnnotation(DictSource.class);

//配置了converter = IntegerDictConverter.class的属性,但是没有添加DictSource注解的直接返回

if (dictSource == null) {

return new WriteCellData<>(String.valueOf(value));

}

//获取配置的dictTypeId

String dictTypeId = dictSource.dictTypeId();

//获取配置的dictContentJson

String dictContentJson = dictSource.dictContentJson();

//判断dictTypeId是否为空

boolean nullDictType = StringUtil.isBlank(dictTypeId);

//判断nullDictContentJson是否为空

boolean nullDictContentJson = StringUtil.isBlank(dictContentJson);

//字典配置都为空时,将属性值转为字符串直接返回

if (nullDictType && nullDictContentJson) {

return new WriteCellData<>(String.valueOf(value));

}

//优先使用dictTypeId处理转换

if (!nullDictType) {

//通过dictTypeId获取字典内容集合:List<DictContent> dictContents = dictContentService.listByDictTypeId(dictTypeId);//主键是数值的,将dictTypeId转为数值

//遍历字典内容,匹配属性值与字典值:value.equals(dictContent.getValue())

//匹配成功后获取字典名称返回:return new WriteCellData<>(dictContent.getName());

//如果没有匹配成功使用dictContentJson处理转换

}

if (!nullDictContentJson) {

Map<Integer, String> dictContentMap = JsonUtil.json2Map(dictContentJson, JsonUtil.MAP_INTEGER_STRING);

String cnName = dictContentMap.get(value);

if (StringUtil.isNotBlank(cnName)) {

return new WriteCellData<>(cnName);

}

}

//没有转换成功时使用默认属性值

return new WriteCellData<>(String.valueOf(value));

}

}

5、创建数据对象类

package com.yu.demo.web.easyexcel.entity;

import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;

import com.alibaba.excel.annotation.ExcelProperty;

import com.alibaba.excel.annotation.format.DateTimeFormat;

import com.alibaba.excel.annotation.write.style.ColumnWidth;

import com.alibaba.excel.converters.date.DateStringConverter;

import com.yu.demo.web.easyexcel.component.DictSource;

import com.yu.demo.web.easyexcel.component.IntegerDictConverter;

import lombok.Getter;

import lombok.Setter;

import lombok.ToString;

import java.util.Date;

@Setter

@Getter

@ToString

//类上添加@ExcelIgnoreUnannotated时,属性没有@ExcelProperty注解时不导出

//类上未添加@ExcelIgnoreUnannotated,属性没有@ExcelProperty注解时也导出

@ExcelIgnoreUnannotated

public class User {

/**

* 名称

*/

@ExcelProperty("名称")

private String name;

/**

* 密码

* 类添加@ExcelIgnoreUnannotated,属性未添加@ExcelProperty,不导出

*/

private String password;

/**

* 生日

* 日期样式处理

* 1.使用@DateTimeFormat设置导出样式

* 2.使用DateStringConverter处理导出

*/

@DateTimeFormat("yyyy-MM-dd HH:mm:ss")

@ExcelProperty(value = "生日", converter = DateStringConverter.class)

private Date birthday;

/**

* 性别

* 字典转换处理

*/

@ColumnWidth(7)//指定列宽度,优先级高于LongestMatchColumnWidthStyleStrategy

@ExcelProperty(value = "性别", converter = IntegerDictConverter.class)

@DictSource(dictContentJson = "{0:'女',1:'男',2:'保密'}")

private Integer sex;

}

6、创建多sheet页封装对象

package com.yu.demo.tools;

import lombok.Getter;

import lombok.Setter;

import lombok.ToString;

import java.util.List;

/**

* excel导入导出数据对象

*/

@Setter

@Getter

@ToString

public class SheetEntity<T> {

/**

* sheet页名称(导出参数)

* 可以为空,为空时,单sheet页没有名称,多sheet页序号为名称

*/

private String sheetName;

/**

* 数据类型(导入导出参数)

*/

private Class<T> head;

/**

* 数据(导出参数)

*/

private List<T> data;

}

7、创建Excel导出工具类

​导出的数据有如下三种及其说明

  • 通过全路径文件名导出,easyexcel通过全路径文件名创建文件,将数据写入文件,当路径不存在时报错,适合场景:一次导出,多次下载
  • 通过文件导出,将数据写入文件,当路径不存在报错,适合场景:一次导出,多次下载
  • 通过输出流导出,将数据写入输出流,适合场景:导出一次下载一次

package com.yu.demo.tools;

import com.alibaba.excel.EasyExcel;

import com.alibaba.excel.ExcelWriter;

import com.alibaba.excel.support.ExcelTypeEnum;

import com.alibaba.excel.write.builder.ExcelWriterBuilder;

import com.alibaba.excel.write.handler.WriteHandler;

import com.alibaba.excel.write.metadata.WriteSheet;

import com.alibaba.excel.write.metadata.style.WriteCellStyle;

import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;

import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;

import org.apache.poi.ss.usermodel.HorizontalAlignment;

import org.apache.poi.ss.usermodel.VerticalAlignment;

import org.apache.poi.util.StringUtil;

import java.io.File;

import java.io.OutputStream;

import java.util.List;

/**

* excel导入导出工具类(easyExcel实现)

* easyPoi:并发量和数据量都不大时推荐,定制化的导出支持非常的丰富

* easyExcel:高并发、大数据量时推荐

*/

public abstract class ExcelUtil {

// 设置居中对齐的样式

private static final WriteCellStyle CONTENT_WRITE_CELL_STYLE;

private static final WriteHandler HORIZONTAL_CELL_STYLE_STRATEGY;

static {

CONTENT_WRITE_CELL_STYLE = new WriteCellStyle();

//水平居中

CONTENT_WRITE_CELL_STYLE.setHorizontalAlignment(HorizontalAlignment.CENTER);

//垂直居中

CONTENT_WRITE_CELL_STYLE.setVerticalAlignment(VerticalAlignment.CENTER);

HORIZONTAL_CELL_STYLE_STRATEGY = new HorizontalCellStyleStrategy(null, CONTENT_WRITE_CELL_STYLE);

}

private ExcelUtil() {

}

/**

* 使用EasyExcel导出

*

* @param fullFileName 文件路径+文件名+后缀(文件已存在时覆盖)

* @param sheetName sheet名称(为空时使用默认值0)

* @param head 数据类型(为空时没有表头,只有数据)

* @param exportData 需要导出的数据(为空时,没有数据)

*/

public static void exportByEasyExcel(String fullFileName, String sheetName, Class<?> head, List<?> exportData) {

File targetFile = new File(fullFileName);

// 判断文件父目录是否存在

if (!targetFile.getParentFile().exists()) {

boolean mkdirResult = targetFile.getParentFile().mkdirs();

if (!mkdirResult) {

return;

}

}

ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(targetFile, head);

if (fullFileName.endsWith(ExcelTypeEnum.XLS.getValue())) {

excelWriterBuilder.excelType(ExcelTypeEnum.XLS);

} else if (fullFileName.endsWith(ExcelTypeEnum.CSV.getValue())) {

excelWriterBuilder.excelType(ExcelTypeEnum.CSV);

} else {

excelWriterBuilder.excelType(ExcelTypeEnum.XLSX);

}

excelWriterBuilder

//设置列按最大长度调整

.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())

//设置水平垂直居中

.registerWriteHandler(HORIZONTAL_CELL_STYLE_STRATEGY)

.sheet(sheetName)

.doWrite(exportData);

}

/**

* 使用EasyExcel导出

*

* @param outputStream 输出流

* @param sheetName sheet名称(为空时使用默认值0)

* @param head 数据类型(为空时没有表头,只有数据)

* @param exportData 需要导出的数据(为空时,没有数据)

*/

public static void exportByEasyExcel(OutputStream outputStream, ExcelTypeEnum excelType, String sheetName, Class<?> head, List<?> exportData) {

EasyExcel.write(outputStream, head)

.excelType(excelType)

//设置列按最大长度调整,非线程安全,每次都需要new

.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())

//设置水平垂直居中

.registerWriteHandler(HORIZONTAL_CELL_STYLE_STRATEGY)

.sheet(sheetName)

.doWrite(exportData);

}

/**

* 使用EasyExcel导出多sheet页数据

*

* @param outputStream 输出流

* @param sheetEntities 导出数据对象集合

*/

public static void exportByEasyExcel(OutputStream outputStream, ExcelTypeEnum excelType, List<SheetEntity<?>> sheetEntities) {

ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(outputStream).excelType(excelType);

writeSheets(excelWriterBuilder, sheetEntities);

}

private static void writeSheets(ExcelWriterBuilder excelWriterBuilder, List<SheetEntity<?>> sheetEntities) {

excelWriterBuilder

.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())

.registerWriteHandler(HORIZONTAL_CELL_STYLE_STRATEGY);

ExcelWriter excelWriter = excelWriterBuilder.build();

for (int i = 0; i < sheetEntities.size(); i++) {

SheetEntity<?> sheetEntity = sheetEntities.get(i);

Class<?> head = sheetEntity.getHead();

List<?> exportData = sheetEntity.getData();

String sheetName = StringUtil.isBlank(sheetEntity.getSheetName()) ? String.valueOf(i + 1) : sheetEntity.getSheetName();

WriteSheet writeSheet = EasyExcel.writerSheet(i + 1, sheetName).head(head).build();

excelWriter.write(exportData, writeSheet);

}

excelWriter.finish();

}

}

8、创建测试类

package com.yu.demo.web.easyexcel.web;

import com.alibaba.excel.support.ExcelTypeEnum;

import com.yu.demo.web.easyexcel.entity.SheetEntity;

import com.yu.demo.web.easyexcel.entity.User;

import com.yu.demo.web.easyexcel.util.ExcelUtil;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.http.HttpHeaders;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.net.URLEncoder;

import java.nio.charset.StandardCharsets;

import java.util.ArrayList;

import java.util.Date;

import java.util.List;

@RestController

@RequestMapping("user")

public class UserController {

@Value("${download.path}")

private String filePath;

private List<User> users;

private List<SheetEntity<?>> sheetEntities;

@PostConstruct

public void init() {

users = new ArrayList<>(5);

for (int i = 0; i < 5; i++) {

User user = new User();

user.setName(i + "号用户");

user.setPassword(String.valueOf(i * 1000));

user.setBirthday(new Date());

user.setSex(i % 3);

users.add(user);

}

sheetEntities = new ArrayList<>(2);

for (int i = 0; i < 2; i++) {

SheetEntity<User> sheetEntity = new SheetEntity<>();

sheetEntity.setSheetName(i + "号sheet");

sheetEntity.setHead(User.class);

sheetEntity.setData(users);

sheetEntities.add(sheetEntity);

}

}

/**

* 单sheet页通过全路径文件名导出测试接口(也可以通过文件流导出)

* 返回文件名,前端通过web路径+文件名下载文件

*/

@GetMapping("/filePath")

public String filePath() {

String fileName = "用户.xlsx";

String fullFileName = filePath + fileName;

ExcelUtil.exportByEasyExcel(fullFileName, "用户", User.class, users);

return fileName;

}

/**

* 多sheet页通过文件流导出(也可以通过全路径文件名导出)

*/

@GetMapping("/download")

public void download(HttpServletResponse response) throws IOException {

String fileName = "用户";

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

response.setCharacterEncoding(StandardCharsets.UTF_8.name());

String encodeFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");

response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=*=utf-8''" + encodeFileName + ExcelTypeEnum.XLSX.getValue());

ExcelUtil.exportByEasyExcel(response.getOutputStream(), ExcelTypeEnum.XLSX, sheetEntities);

}

}

9、application.yml中添加文件下载路径配置

download:

#配置文件下载路径

path: C:\Users\Administrator\Desktop

spring:

web:

resources:

static-locations:

#注册文件下载路径

- file:${download.path}

#系统默认配置

- classpath:/META-INF/resources/

- classpath:/resources/

- classpath:/static/

- classpath:/public/

四、接口测试

1、启动项目

启动项目

2、使用全路径文件名方式导出

    <li>访问接口地址:http://localhost:8080/user/filePath

访问接口地址

    <li>访问下载文件地址:http://localhost:8080/用户.xlsx

访问下载文件地址

3、使用文件流方式导出

    <li>访问接口地址:http://localhost:8080/user/download

访问接口地址

五、总结

    <li>使用Entity对象作为关系映射的载体,使用@ExcelProperty注解映射属性名称,并可以指定转换器、序号等信息;使用@DateTimeFormat注解和指定转换器设置时间格式;使用自定义注解@DictSource注解和指定转换器转换字典值
  1. 使用@ColumnWidth或其同目录下的其他注解、WriteHandler设置Excel样式
  2. 使用全路径文件名、文件、文件流作为数据导出的载体导出数据。
  3. SpringBoot集成easyexcel数据导出案例下载


声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。