前言
关于万能导出,前一阵子写过一个,功能是实现了的。
就是在使用时,感觉如果需要导出的页面比较多,那就会出现比较多的重复代码。就想着优化+简化一下,能够更方便的使用。
原版代码仓库和这一版的代码仓库相同:
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();
// 响应到web
String 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步操作:
- 根据导出bean,使用建造器生成模版实例;
- 给模版实例中填充业务数据;
- 填充完数据后,将数据转换为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);
}
}
@Override
public Object put(String key, Object value) {
// 只保存字段名缓存中的key以及value
if (headColumnNamesCache.contains(key)) {
// 设置了被忽略的列,进行判断处理,匹配到了就不保存
if (!CollectionUtils.isEmpty(ignoreColumnSet) && ignoreColumnSet.contains(key)) {
return null;
}
return super.put(key, value);
}
return null;
}
@Override
public 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;
/**
* 是否转换前打印导出数据(含表头)
*/
@Setter
private boolean logExportData;
/**
* 是否转换前打印实际的表头信息
*/
@Setter
private 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;
}
@Override
public 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();
// 响应到web
String 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);
}
@Override
public 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
会下载得到这样的文件: