项目背景:
有一个导出excel的需求,要求导出部分固定列和部分动态列,固定列使用字段写死,动态列使用list集合存放
成果展示:
思路:
简单说就是一个行转列的处理
1. 使用easypoi的注解方式进行导出,固定列部分使用 @Excel标注
2. 动态列使用一个List集合,用 @ExcelCollection 标注,里面的每一项就是每一个动态列标题,一个字段作为表头名称,一个字段作为对应的数据
代码:
需要的maven依赖及版本
<!--easypoi-->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.4.0</version>
</dependency>
<!--commons-beanutils-->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<!--cglib,动态向实体类添加字段-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
</dependency>
举个栗子:
模拟自己的业务逻辑,组装好需要的数据格式调用工具类中的方法即可
public void exportTest(HttpServletResponse response) {
ExportDemo demo1 = new ExportDemo();
// 固定列
demo1.setDepartName("住建局");
demo1.setResolutionRate("82%");
demo1.setDealCount(204);
demo1.setTotalEvaCount(62);
demo1.setValidEvaCount(7);
// 动态列
List<ExcelPoiUtils.Demo.Type> types = new ArrayList<>();
ExcelPoiUtils.Demo.Type type1 = new ExcelPoiUtils.Demo.Type();
type1.setTypeName("民生满意度");
type1.setScore("35分");
ExcelPoiUtils.Demo.Type type2 = new ExcelPoiUtils.Demo.Type();
type2.setTypeName("卫生治理");
type2.setScore("46分");
ExcelPoiUtils.Demo.Type type3 = new ExcelPoiUtils.Demo.Type();
type3.setTypeName("公共安全");
type3.setScore("52分");
ExcelPoiUtils.Demo.Type type4 = new ExcelPoiUtils.Demo.Type();
type4.setTypeName("绿化面积");
type4.setScore("65分");
types.add(type1);
types.add(type2);
types.add(type3);
types.add(type4);
demo1.setTypes(types);
ExportDemo demo2 = new ExportDemo();
demo2.setDepartName("民政局");
demo2.setResolutionRate("62%");
demo2.setDealCount(9661);
demo2.setTotalEvaCount(560);
demo2.setValidEvaCount(80000);
List<ExcelPoiUtils.Demo.Type> types2 = new ArrayList<>();
ExcelPoiUtils.Demo.Type typeA = new ExcelPoiUtils.Demo.Type();
typeA.setTypeName("民生满意度");
typeA.setScore("102分");
ExcelPoiUtils.Demo.Type typeB = new ExcelPoiUtils.Demo.Type();
typeB.setTypeName("卫生治理");
typeB.setScore("60分");
ExcelPoiUtils.Demo.Type typeC = new ExcelPoiUtils.Demo.Type();
typeC.setTypeName("公共安全");
typeC.setScore("4分");
ExcelPoiUtils.Demo.Type typeD = new ExcelPoiUtils.Demo.Type();
typeD.setTypeName("绿化面积");
typeD.setScore("88分");
types2.add(typeA);
types2.add(typeB);
types2.add(typeC);
types2.add(typeD);
demo2.setTypes(types2);
List<ExportDemo> list = new ArrayList<>();
list.add(demo1);
list.add(demo2);
// 动态标头名称字段
final String headerName = "typeName";
// 动态标头值字段
final String headerValue = "score";
try {
ExcelPoiUtils.dynamicExport(response, System.currentTimeMillis() + "", "我是标题行!",
"sheet名称", list, headerName, headerValue);
} catch (Exception e) {
log.error("导出错误,", e);
throw new RuntimeException(e);
}
}
实体类:
package com.github.face.user.entity;
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelCollection;
import com.github.face.utils.ExcelPoiUtils;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 动态列导出测试
*
* @author wangcl
*/
@Data
public class ExportDemo implements Serializable {
/**
* 部门名称
*/
@Excel(name = "部门", width = 20)
private String departName;
/**
* 处理事件数
*/
@Excel(name = "处理事件数", width = 20)
private Integer dealCount = 0;
/**
* 全部评价数
*/
@Excel(name = "全部评价数", width = 20)
private Integer totalEvaCount = 0;
/**
* 有效评价数
*/
@Excel(name = "有效评价数", width = 20)
private Integer validEvaCount = 0;
/**
* 解决率
*/
@Excel(name = "解决率", width = 20)
private String resolutionRate;
/**
* 评价类型及分数
*/
@ExcelCollection(name = "评价类型")
private List<ExcelPoiUtils.Demo.Type> types;
/**
* 评价类型及分数对象
*/
@Data
static class Type implements Serializable {
/**
* 评价类型id
*/
private Long typeId;
/**
* 评价类型名称(动态标题名称,此处 name = "typeName"可以随便填,以方法调用时传入的为准)
*/
@Excel(name = "typeName", width = 20)
private String typeName;
/**
* 评价类型对应分数(动态标题内容,此处 name = "score"可以随便填,以方法调用时传入的为准)
*/
@Excel(name = "score", width = 20)
private String score = "0.00";
}
}
动态导出类:
处理动态表头和动态列字段的对应关系
package com.github.face.utils;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelCollection;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.annotation.AnnotationUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* 使用easyPoi的动态列导出
* <br/>固定列使用@Excel标识,动态列使用@ExcelCollection标识的List集合,手动传入集合内作为动态表头名称和值的字段名
*
* @author wangcl
*/
@Slf4j
public class ExcelPoiUtils {
/**
* 使用注解的动态列导出,实体类必须按照规定格式
* <br/>通过反射实现,注意方法效率
*
* @param response 响应
* @param fileName 文件名
* @param title 标题
* @param sheetName sheet名称
* @param dataList 导出数据
* @param headerName 动态列标题
* @param headerValue 动态列值
*/
public static <T> void dynamicExport(HttpServletResponse response, String fileName, String title, String sheetName,
List<T> dataList, String headerName, String headerValue) throws Exception {
Assert.notEmpty(dataList, "没有需要导出的数据");
List<ExcelExportEntity> entityList = new ArrayList<>();
List<Object> list = new ArrayList<>();
for (T t : dataList) {
// Step1:处理标题
Field[] fields = t.getClass().getDeclaredFields();
int index = 0;
Map<String, Object> map = new HashMap<>();
for (Field field : fields) {
field.setAccessible(true);
Excel excel = field.getAnnotation(Excel.class);
ExcelCollection excelCollection = field.getAnnotation(ExcelCollection.class);
// 固定导出列
if (excel != null) {
Object name = AnnotationUtil.getAnnotationValue(field, Excel.class, "name");
// 转换注解修饰的对象
ExcelExportEntity entity = convert(field, name.toString(), field.getName(), index);
index++;
entityList.add(entity);
}
// 自定义导出列,含有@ExcelCollection并且是List
else if (excelCollection != null && field.getType().getName().equals(List.class.getName())) {
Object object;
object = field.get(t);
List<?> dynamicColl = (List<?>) object;
for (Object arr : dynamicColl) {
String key = null;
String val = null;
Field[] typeFields = arr.getClass().getDeclaredFields();
for (Field typeField : typeFields) {
typeField.setAccessible(true);
String fieldName = typeField.getName();
Excel excelItem = typeField.getAnnotation(Excel.class);
if (excelItem != null) {
if (headerName.equals(fieldName)) {
Object value;
try {
value = typeField.get(arr);
key = value.toString();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
// 转换注解修饰的对象
ExcelExportEntity entity = convert(typeField, value.toString(), value.toString(), index);
index++;
entityList.add(entity);
} else if (headerValue.equals(fieldName)) {
Object value;
try {
value = typeField.get(arr);
val = value.toString();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
map.put(key, val);
}
}
}
// Step2:处理数据,将动态列添加到实体类
Object object = ReflectKit.getObject(t, map);
list.add(object);
}
log.info(JSONObject.toJSONString(list));
entityList = entityList.stream().filter(distinctByKey(ExcelExportEntity::getName)).collect(Collectors.toList());
downloadExcelEntityDynamic(response, entityList, list, fileName, title, sheetName);
}
/**
* 根据指定字段去重
*/
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
/**
* 动态表头导出
*
* @param response 响应
* @param entityList 表头列表
* @param list 导出数据
* @param fileName 文件名
* @param title 标题
* @param sheetName sheet名称
*/
public static void downloadExcelEntityDynamic(HttpServletResponse response, List<ExcelExportEntity> entityList,
Collection<?> list, String fileName, String title,
String sheetName) throws Exception {
makeResponse(response, fileName);
ExportParams exportParams;
if (StringUtils.hasText(title)) {
exportParams = new ExportParams(title, sheetName);
} else {
exportParams = new ExportParams();
exportParams.setSheetName(sheetName);
}
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, list);
ServletOutputStream output = response.getOutputStream();
BufferedOutputStream bufferedOutPut = new BufferedOutputStream(output);
workbook.write(bufferedOutPut);
bufferedOutPut.flush();
bufferedOutPut.close();
output.close();
}
public static void makeResponse(HttpServletResponse response, String fileName) {
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
/**
* 将@Excel修饰的字段转为ExcelExportEntity
*/
private static ExcelExportEntity convert(Field typeField, String name, String key, int index) {
Map<String, Object> annotationValueMap = AnnotationUtil.getAnnotationValueMap(typeField, Excel.class);
ExcelExportEntity entity = JSONObject.parseObject(JSONObject.toJSONBytes(annotationValueMap), ExcelExportEntity.class);
// 字段名和@Excel的name一致,视为动态表头列
entity.setName(name);
entity.setKey(key);
entity.setOrderNum(index);
return entity;
}
}
反射处理类:
使用cglib动态向实体类内添加字段,添加的字段为需要动态添加的表头和对应的值
package com.github.face.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtilsBean;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* 反射获取动态对象
*
* @author wangcl
*/
@Slf4j
public class ReflectKit {
/**
* 获取添加动态字段的新对象
*
* @param dest 原始对象
* @param newValueMap 要添加的字段和值
*/
public static Object getObject(Object dest, Map<String, Object> newValueMap) throws
InvocationTargetException, IllegalAccessException {
PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
//1.获取原对象的字段数组
PropertyDescriptor[] descriptorArr = propertyUtilsBean.getPropertyDescriptors(dest);
//2.遍历原对象的字段数组,并将其封装到Map
Map<String, Class> oldKeyMap = new HashMap<>(4);
for (PropertyDescriptor it : descriptorArr) {
if (!"class".equalsIgnoreCase(it.getName())) {
oldKeyMap.put(it.getName(), it.getPropertyType());
newValueMap.put(it.getName(), it.getReadMethod().invoke(dest));
}
}
//3.将扩展字段Map合并到原字段Map中
newValueMap.forEach((k, v) -> oldKeyMap.put(k, v.getClass()));
//4.根据新的字段组合生成子类对象
DynamicBean dynamicBean = new DynamicBean(dest.getClass(), oldKeyMap);
//5.放回合并后的属性集合
newValueMap.forEach((k, v) -> {
try {
dynamicBean.setValue(k, v);
} catch (Exception e) {
log.error("动态添加字段【值】出错", e);
}
});
return dynamicBean.getTarget();
}
}
动态代理类:
package com.github.face.utils;
import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;
import java.util.Map;
/**
* 动态代理类
*
* @author wangcl
*/
public class DynamicBean {
/**
* 目标对象
*/
private Object target;
/**
* 属性集合
*/
private BeanMap beanMap;
public DynamicBean(Class superclass, Map<String, Class> propertyMap) {
this.target = generateBean(superclass, propertyMap);
this.beanMap = BeanMap.create(this.target);
}
/**
* bean 添加属性和值
*
* @param property
* @param value
*/
public void setValue(String property, Object value) {
beanMap.put(property, value);
}
/**
* 获取属性值
*
* @param property
* @return
*/
public Object getValue(String property) {
return beanMap.get(property);
}
/**
* 获取对象
*
* @return
*/
public Object getTarget() {
return this.target;
}
/**
* 根据属性生成对象
*
* @param superclass
* @param propertyMap
* @return
*/
private Object generateBean(Class superclass, Map<String, Class> propertyMap) {
BeanGenerator generator = new BeanGenerator();
if (null != superclass) {
generator.setSuperclass(superclass);
}
BeanGenerator.addProperties(generator, propertyMap);
Object object = generator.create();
return object;
}
}
提示:
jdk9以上默认关闭反射,需要手动开启,需要手动添加启动参数
针对本项目: --add-opens java.base/java.lang=ALL-UNNAMED
针对jdk9以上版本的反射: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED
idea开启方式如下
没有测试过接口效率,有错误的地方欢迎大家指正
标签:java,String,import,导出,param,add,new,easyPoi,动态 From: https://blog.csdn.net/qq_40603183/article/details/137143714