当前位置: 首页 > news >正文

【EasyExcel实践】万能导出,一个接口导出多张表以及任意字段(可指定字段顺序)-简化升级版

文章目录

  • 前言
  • 正文
    • 一、项目简介
    • 二、核心代码
      • 2.1 pom.xml 依赖配置
      • 2.2 ExcelHeadMapFactory
      • 2.3 ExcelDataLinkedHashMap
      • 2.4 自定义注解 ExcelExportBean
      • 2.5 自定义注解 ExcelColumnTitle
      • 2.6 建造器接口 Builder
      • 2.7 表格工具类 ExcelUtils
      • 2.8 GsonUtil
      • 2.9 模版类 ExportDynamicColumnTemplate
      • 2.10 模版建造器 ExportDynamicColumnTemplateBuilder
    • 三、控制器&调试Bean的定义
      • 3.1 StudentDemo
      • 3.2 NameAndFactoryDemo
      • 3.3 控制器 ExcelDemoController
    • 四、启动类
    • 五、测试

前言

关于万能导出,前一阵子写过一个,功能是实现了的。
就是在使用时,感觉如果需要导出的页面比较多,那就会出现比较多的重复代码。就想着优化+简化一下,能够更方便的使用。

原版代码仓库和这一版的代码仓库相同:
https://gitee.com/fengsoshuai/excel-demo

原版中额外增加了转换器,枚举转换等功能,但是总觉得会让整体功能变复杂,所以在这一版中就去掉了。如果有兴趣看看的话,可以切换到master分支查看。

简化升级的这一版的代码分支是:simple-dynamic-column-export

正文

一、项目简介

本次简化升级,本着对使用者友好的目的去实现的。

另外,本文会粘贴全部代码!!

在这里插入图片描述

在使用上的简化体现在真正导出时,只需要几行代码,就能实现功能。可以让你更加专注于业务参数的组装。而且代码比较简洁。
举个例子:

@GetMapping("/exportDy")
public String exportDy(@RequestParam("table") List<String> table, HttpServletResponse response) throws IOException {// 构造导出模版ExportDynamicColumnTemplate<StudentDemo> dynamicColumnTemplate = new ExportDynamicColumnTemplateBuilder<>(StudentDemo.class).columnNameList(table).build();// 制造假数据dynamicColumnTemplate.appendExportDataList(studentDemos());// 转换为excel的字节数组byte[] bytes = dynamicColumnTemplate.toExcelByteArray();// 响应到webString fileName = System.currentTimeMillis() + ".xlsx";response.setHeader("Content-disposition", "attachment;filename=" + fileName);response.setContentType("application/x-msdownload");response.setCharacterEncoding("utf-8");response.getOutputStream().write(bytes);response.getOutputStream().flush();return "success";
}

简化后的代码,只需要3步操作:

  1. 根据导出bean,使用建造器生成模版实例;
  2. 给模版实例中填充业务数据;
  3. 填充完数据后,将数据转换为excel格式的字节数组;

当我们抡完这三板斧之后,剩下的就是将字节数组响应到web导出。

二、核心代码

2.1 pom.xml 依赖配置

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.2.0.RELEASE</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.11</version><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></exclusion></exclusions></dependency><!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.10.1</version></dependency>
</dependencies>

2.2 ExcelHeadMapFactory

表格头映射工厂,提供了注册表格头和获取表格头配置的静态方法。

package org.feng.export.factory;import org.feng.export.system.ExcelColumnTitle;
import org.feng.export.system.ExcelExportBean;import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;/*** 表格头映射工厂** @author feng*/
public class ExcelHeadMapFactory {/*** 全局表头名映射,key内部变量的变量名和中文名映射为value*/private static final Map<Class<?>, Map<String, String>> HEAD_NAME_MAP = new HashMap<>();public static void addHeadClass(Class<?> headClass) {if (!headClass.isAnnotationPresent(ExcelExportBean.class)) {throw new RuntimeException("HeadClass必须使用注解ExcelExportBean");}HEAD_NAME_MAP.put(headClass, mapToPrepareHead(headClass));}public static Map<String, String> getHeadMap(Class<?> headClass) {return HEAD_NAME_MAP.get(headClass);}public static boolean containsHeadClass(Class<?> headClass) {return HEAD_NAME_MAP.containsKey(headClass);}private static Map<String, String> mapToPrepareHead(Class<?> excelHeadClass) {Map<String, String> namedMap = new HashMap<>();Field[] declaredFields = excelHeadClass.getDeclaredFields();for (Field declaredField : declaredFields) {boolean annotationPresent = declaredField.isAnnotationPresent(ExcelColumnTitle.class);if (annotationPresent) {ExcelColumnTitle excelProperty = declaredField.getAnnotation(ExcelColumnTitle.class);String chineseFieldName = excelProperty.value();// 保存字段名和中文变量名namedMap.put(declaredField.getName(), chineseFieldName);}}return namedMap;}
}

2.3 ExcelDataLinkedHashMap

自定义LinkedHashMap,用于实现字段的顺序,以及动态字段展示。
只提供put 和 putAll方法的实现。想要别的put方法的话,读者请自行实现。

package org.feng.export.system;import org.springframework.util.CollectionUtils;import java.util.*;
import java.util.stream.Collectors;/*** 表格数据专用的map,带顺序,而且初始化的时候,依据指定的表头变量字段名确定导出数据的顺序** @author feng*/
public class ExcelDataLinkedHashMap extends LinkedHashMap<String, Object> {private static final long serialVersionUID = -8554095999151235982L;/*** 头部字段名缓存*/private final Set<String> headColumnNamesCache;/*** 指定被忽略的列名*/private final Set<String> ignoreColumnSet;/*** ExcelDataLinkedHashMap构造器** @param headColumnNames 表头字段变量名,例如:[name,studentNo,age,className]*/public ExcelDataLinkedHashMap(List<Object> headColumnNames, String... ignoreColumns) {if (ignoreColumns != null && ignoreColumns.length > 0) {ignoreColumnSet = new HashSet<>(Arrays.asList(ignoreColumns));} else {ignoreColumnSet = Collections.emptySet();}// 字段名去重List<String> headColumnStringNames = headColumnNames.stream().distinct().map(Object::toString).collect(Collectors.toList());// 构建字段名缓存this.headColumnNamesCache = new HashSet<>(headColumnStringNames);// 指定列数据排列顺序for (String headColumnName : headColumnStringNames) {this.put(headColumnName, null);}}@Overridepublic Object put(String key, Object value) {// 只保存字段名缓存中的key以及valueif (headColumnNamesCache.contains(key)) {// 设置了被忽略的列,进行判断处理,匹配到了就不保存if (!CollectionUtils.isEmpty(ignoreColumnSet) && ignoreColumnSet.contains(key)) {return null;}return super.put(key, value);}return null;}@Overridepublic void putAll(Map<? extends String, ?> map) {map.forEach(this::put);}
}

2.4 自定义注解 ExcelExportBean

package org.feng.export.system;import java.lang.annotation.*;/*** 指定表格导出的bean** @author feng*/
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelExportBean {
}

2.5 自定义注解 ExcelColumnTitle

package org.feng.export.system;import java.lang.annotation.*;/*** 列名标题注解,标注列的标题** @author feng*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumnTitle {String value();
}

2.6 建造器接口 Builder

package org.feng.export.util;/*** 建造器接口** @author feng*/
public interface Builder<T> {T build();
}

2.7 表格工具类 ExcelUtils

提供将数据写入表格的静态方法。

package org.feng.export.util;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.WriteSheet;import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;/*** excel工具类** @author feng*/
public class ExcelUtils {/*** 导出数据(单sheet)** @param exportData key 是sheet名称,value是每个sheet里面的数据,支持自定义表头*/public static byte[] easyOut(List<Map<String, Object>> exportData) {return easyOut("Sheet", exportData);}/*** 导出数据(单sheet)** @param exportData key 是sheet名称,value是每个sheet里面的数据,支持自定义表头*/public static byte[] easyOut(String sheetName, List<Map<String, Object>> exportData) {return easyOut(Collections.singletonMap(sheetName, exportData));}/*** 导出数据(多sheet)** @param exportData key 是sheet名称,value是每个sheet里面的数据,可以自定义*/public static byte[] easyOut(Map<String, List<Map<String, Object>>> exportData) {// 导出数据ByteArrayOutputStream out = new ByteArrayOutputStream();com.alibaba.excel.ExcelWriter excelWriter = EasyExcel.write(out).build();int i = 0;for (Map.Entry<String, List<Map<String, Object>>> entry : exportData.entrySet()) {WriteSheet writeSheet = EasyExcel.writerSheet(i, entry.getKey()).head(head(entry.getValue().get(0))).build();i++;excelWriter.write(data(entry.getValue(), true), writeSheet);}excelWriter.finish();return out.toByteArray();}private static List<List<String>> head(Map<String, Object> cellData) {List<List<String>> head = new ArrayList<>();for (String key : cellData.keySet()) {head.add(Collections.singletonList(key));}return head;}private static List<List<Object>> data(List<Map<String, Object>> sheetData, boolean skipHead) {List<List<Object>> data = new ArrayList<>();for (int i = 0; i < sheetData.size(); i++) {if (i == 0 && skipHead) {continue;}data.add(new ArrayList<>(sheetData.get(i).values()));}return data;}
}

2.8 GsonUtil

gson工具类,提供json处理、转换的静态方法。

package org.feng.export.util;import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;import java.util.Map;/*** gson工具类** @version v1.0* @author: fengjinsong* @date: 2023年08月26日 15时00分*/
public class GsonUtil {/*** 不会序列化空字段的Gson对象*/private static final Gson GSON = new GsonBuilder().create();/*** 可以序列化空字段的Gson对象*/private static final Gson GSON_WITH_NULL = new GsonBuilder().serializeNulls().create();private static final Gson GSON_WITH_DISABLE_HTML_ESCAPING = new GsonBuilder().disableHtmlEscaping().create();public static Gson gson() {return GSON;}/*** 转换对象为json字符串(不会序列化空字段)** @param object 目标对象* @return 一个json字符串*/public static String toJson(Object object) {return GSON.toJson(object);}/*** 转换对象为json字符串(可以序列化空字段)** @param object 目标对象* @return 一个json字符串*/public static String toJsonWithNull(Object object) {return GSON_WITH_NULL.toJson(object);}/*** 转换对象为json字符串(禁止html转义)** @param object 目标对象* @return 一个json字符串*/public static String toJsonWithDisableTtmlEscaping(Object object) {return GSON_WITH_DISABLE_HTML_ESCAPING.toJson(object);}public static <T> T fromJson(String jsonStr, Class<T> clazz) {return GSON.fromJson(jsonStr, clazz);}public static Map<String, String> toStringMap(String jsonStr) {return GSON.fromJson(jsonStr, new TypeToken<Map<String, String>>() {}.getType());}/*** 校验字符串是否是一个json格式* <br> 注意:{@code "{}"} 也是符合条件的json** @param jsonStr 目标字符串* @return true表示目标是一个正确的json格式*/public static boolean validateJson(String jsonStr) {JsonElement jsonElement;try {jsonElement = JsonParser.parseString(jsonStr);} catch (Exception e) {return false;}if (jsonElement == null) {return false;}return jsonElement.isJsonObject();}
}

2.9 模版类 ExportDynamicColumnTemplate

package org.feng.export;import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.feng.export.factory.ExcelHeadMapFactory;
import org.feng.export.system.ExcelDataLinkedHashMap;
import org.feng.export.util.ExcelUtils;
import org.feng.export.util.GsonUtil;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;import java.util.*;/*** 导出动态列模版** @author feng*/
@Slf4j
@Getter
public class ExportDynamicColumnTemplate<T> {/*** 导出的bean类型*/private Class<T> exportBean;/*** 导出的数据*/private List<Map<String, Object>> exportData;/*** 当前表头映射*/private Map<String, Object> currentHeadMap;/*** 真正导出的列名*/private List<Object> realExportColumnNameList;/*** 是否转换前打印导出数据(含表头)*/@Setterprivate boolean logExportData;/*** 是否转换前打印实际的表头信息*/@Setterprivate boolean logCurrentHead;ExportDynamicColumnTemplate(Class<T> exportBean, List<String> columnNameList) {this(exportBean, columnNameList, false, true);}ExportDynamicColumnTemplate(Class<T> exportBean, List<String> columnNameList, boolean logExportData, boolean logCurrentHead) {init(exportBean, columnNameList);this.logExportData = logExportData;this.logCurrentHead = logCurrentHead;}/*** 追加导出数据** @param data          数据* @param ignoreColumns 设置忽略的列名*/public void appendExportData(T data, String... ignoreColumns) {Objects.requireNonNull(data);ExcelDataLinkedHashMap dataMap = new ExcelDataLinkedHashMap(realExportColumnNameList, ignoreColumns);exportData.add(dataMap);String dataJson = GsonUtil.toJson(data);dataMap.putAll(GsonUtil.toStringMap(dataJson));}/*** 追加导出数据列表** @param dataList      数据列表* @param ignoreColumns 设置忽略的列名*/public void appendExportDataList(List<T> dataList, String... ignoreColumns) {if (CollectionUtils.isEmpty(dataList)) {return;}dataList.forEach(data -> appendExportData(data, ignoreColumns));}/*** 将当前需要导出的数据转换为表格的字节数组** @return 字节数组*/public byte[] toExcelByteArray() {return toExcelByteArray("Sheet");}/*** 将当前需要导出的数据转换为表格的字节数组** @param sheetName 指定sheet名* @return 字节数组*/public byte[] toExcelByteArray(String sheetName) {if (logExportData) {logExportData();}if (logCurrentHead) {log.info("导出数据表头信息:{}", currentHeadMap);}return ExcelUtils.easyOut(sheetName, exportData);}private void logExportData() {for (int i = 0; i < exportData.size(); i++) {Map<String, Object> data = exportData.get(i);log.info("导出数据[{}]:{}", i, data);}}/*** 初始化导出模版信息,主要是表头信息** @param exportBean     导出bean的类型* @param columnNameList 需要导出的字段名列表*/private void init(Class<T> exportBean, List<String> columnNameList) {this.exportBean = exportBean;exportData = new ArrayList<>();currentHeadMap = new LinkedHashMap<>();// 获取代码配置的表头信息if (!ExcelHeadMapFactory.containsHeadClass(exportBean)) {ExcelHeadMapFactory.addHeadClass(exportBean);}Map<String, String> headMap = ExcelHeadMapFactory.getHeadMap(exportBean);Assert.notEmpty(headMap, "表头不能为空,请检查exportBean的类型");// 初始化真正的表头信息,过滤无效配置或找不到的列名realExportColumnNameList = new ArrayList<>();for (String field : columnNameList) {String fieldChineseName = headMap.get(field);if (StringUtils.isEmpty(fieldChineseName)) {log.info("代码配置的导出表头不完整,不存在字段:{}", field);} else {currentHeadMap.put(fieldChineseName, field);realExportColumnNameList.add(field);}}// 记录当前的表头信息exportData.add(currentHeadMap);}
}

2.10 模版建造器 ExportDynamicColumnTemplateBuilder

用于生成模版实例。

package org.feng.export;import lombok.AccessLevel;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.feng.export.system.ExcelExportBean;
import org.feng.export.util.Builder;
import org.springframework.util.Assert;import java.util.List;/*** 导出动态列模版建造器** @author feng*/
@Accessors(chain = true, fluent = true)
@Setter
public class ExportDynamicColumnTemplateBuilder<T> implements Builder<ExportDynamicColumnTemplate<T>> {/*** 字段名列表*/private List<String> columnNameList;/*** 导出的bean类型*/@Setter(AccessLevel.NONE)private final Class<T> exportBean;/*** 是否转换前打印导出数据(含表头)*/private Boolean logExportData;/*** 是否转换前打印实际的表头信息*/private Boolean logCurrentHead;public ExportDynamicColumnTemplateBuilder(Class<T> exportBean) {if (!exportBean.isAnnotationPresent(ExcelExportBean.class)) {throw new RuntimeException("ExportBean必须使用注解ExcelExportBean");}this.exportBean = exportBean;}@Overridepublic ExportDynamicColumnTemplate<T> build() {check();ExportDynamicColumnTemplate<T> template = new ExportDynamicColumnTemplate<>(exportBean, columnNameList);if(logCurrentHead != null) {template.setLogCurrentHead(logCurrentHead);}if(logExportData != null) {template.setLogExportData(logExportData);}return template;}private void check() {Assert.notNull(exportBean, "导出的实例类型不能为空");Assert.notEmpty(columnNameList, "字段名列表不能为空");}
}

三、控制器&调试Bean的定义

这一部分是非核心代码,属于对核心代码使用的一种演示。读者可以按照对应的写法来实现功能。

注意:导出的bean定义,需要使用注解 ExcelExportBean,其中的字段需要使用注解ExcelColumnTitle

3.1 StudentDemo

package org.feng.headbean;import org.feng.export.system.ExcelColumnTitle;
import lombok.Data;
import org.feng.export.system.ExcelExportBean;/*** 学生demo导出bean** @author feng*/
@Data
@ExcelExportBean
public class StudentDemo {@ExcelColumnTitle("姓名")private String name;@ExcelColumnTitle("年龄")private String age;@ExcelColumnTitle("性别")private String sex;@ExcelColumnTitle("学号")private String studentNo;@ExcelColumnTitle("班级")private String className;
}

3.2 NameAndFactoryDemo

该类对本次演示无实际意义,保留着是因为,需要演示加载多个配置表头的实例时的写法。
具体的可以查看启动类ExcelDemoApplication中的内容。

package org.feng.headbean;import org.feng.export.system.ExcelColumnTitle;
import lombok.Data;
import org.feng.export.system.ExcelExportBean;/*** TODO** @author feng*/
@Data
@ExcelExportBean
public class NameAndFactoryDemo {@ExcelColumnTitle("名字")private String name;@ExcelColumnTitle("工厂")private String factory;
}

3.3 控制器 ExcelDemoController

package org.feng.controller;import org.feng.export.ExportDynamicColumnTemplate;
import org.feng.export.ExportDynamicColumnTemplateBuilder;
import org.feng.headbean.StudentDemo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;@Controller
@RequestMapping("/excel")
public class ExcelDemoController {@GetMapping("/exportDy")public String exportDy(@RequestParam("table") List<String> table, HttpServletResponse response) throws IOException {// 构造导出模版ExportDynamicColumnTemplate<StudentDemo> dynamicColumnTemplate = new ExportDynamicColumnTemplateBuilder<>(StudentDemo.class).columnNameList(table).build();// 制造假数据dynamicColumnTemplate.appendExportDataList(studentDemos());// 转换为excel的字节数组byte[] bytes = dynamicColumnTemplate.toExcelByteArray();// 响应到webString fileName = System.currentTimeMillis() + ".xlsx";response.setHeader("Content-disposition", "attachment;filename=" + fileName);response.setContentType("application/x-msdownload");response.setCharacterEncoding("utf-8");response.getOutputStream().write(bytes);response.getOutputStream().flush();return "success";}private List<StudentDemo> studentDemos() {List<StudentDemo> studentDemos = new ArrayList<>();for (int i = 0; i < 5; i++) {StudentDemo studentDemo = new StudentDemo();studentDemo.setStudentNo(100 + "-" + i);studentDemo.setAge(String.valueOf(20 + i));studentDemo.setSex(i > 2 ? "男" : "女");studentDemo.setClassName("一班");studentDemo.setName("小米" +(i+1));studentDemos.add(studentDemo);}return studentDemos;}
}

四、启动类

启动项目时,加载代码配置信息。

package org.feng;import org.feng.export.factory.ExcelHeadMapFactory;
import org.feng.headbean.NameAndFactoryDemo;
import org.feng.headbean.StudentDemo;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;import java.util.ArrayList;
import java.util.List;@SpringBootApplication
public class ExcelDemoApplication implements CommandLineRunner {public static void main(String[] args) {SpringApplication.run(ExcelDemoApplication.class, args);}@Overridepublic void run(String... args) throws Exception {List<Class<?>> needRegisterExcelHeadClassList = new ArrayList<>();needRegisterExcelHeadClassList.add(NameAndFactoryDemo.class);needRegisterExcelHeadClassList.add(StudentDemo.class);needRegisterExcelHeadClassList.forEach(ExcelHeadMapFactory::addHeadClass);}
}

五、测试

在谷歌浏览器访问:
http://localhost:8080/excel/exportDy?table=name,className,studentNo

会下载得到这样的文件:
在这里插入图片描述

http://www.lryc.cn/news/264425.html

相关文章:

  • 解决 Hive 外部表分隔符问题的实用指南
  • 一文学会 Apache Zeppelin
  • ROS学习笔记(七)---参数服务器
  • 【RTOS学习】源码分析(信号量和互斥量 事件组 任务通知)
  • 1316:【例4.6】数的计数(Noip2001) 代码+解析
  • 征集倒计时 | 2023年卓越影响力榜单-第四届中国产业创新奖报名即将截止
  • vue的语法模板与数据绑定的说明
  • VueCron使用方法
  • SpringBlade export-user SQL 注入漏洞复现
  • 结构体的一些补充知识
  • 20V升26V 600mA升压型LED驱动芯片,PWM调光芯片-AH1160
  • 如何在Go中制作HTTP服务器
  • Linux笔记---系统信息
  • 最新版android stuido加上namespace
  • Wireshark基础及捕获技巧
  • Windows下Navicat15.0连接Oracle11g报ORA-28547解决
  • 21 Vue3中使用v-for遍历对象数组
  • 深入理解Java自定义异常与全局异常处理 @RestControllerAdvice
  • h5页面跳转微信小程序(最简单的方法|URL Scheme)
  • 智能优化算法应用:基于非洲秃鹫算法3D无线传感器网络(WSN)覆盖优化 - 附代码
  • 持续集成交付CICD:Jenkins使用GitLab共享库实现前端项目镜像构建
  • SQL server 数据库 SQL语句高级用法
  • wavlink 路由器 多处前台RCE漏洞复现
  • 互联网中的商品超卖问题及其解决方案:Java中Redis结合UUID的应用
  • mysql:查看线程缓存中的线程数量
  • 线性表,也是Java中数组的知识点!
  • java使用面向对象实现图书管理系统
  • 2023 英特尔On技术创新大会直播 |我感受到的“芯”魅力
  • Atium Designer 23 全新功能-丝印制备,解决DFM问题
  • Nginx快速入门:安装目录结构详解及核心配置解读(二)