针对若依框架微服务版本学习
若依导入导出功能的具体使用详见:后台手册 | RuoYi
1.导出逻辑:
导出文件的逻辑是先创建一个临时文件,等待前端请求下载结束后马上删除这个临时文件。但是有些下载插件,例如迅雷(他们是二次下载),这个时候文件已经删除,会导致异常,找不到文件。可强制把所有的导出都改成流的形式返回给前端,不采用临时文件的方法,具体方法后台手册都有(目前下载的代码为流的形式)
2.代码详解
export 导出的入口函数
1.controller层
调用userService.selectUserList(user)方法查询数据,util.exportExcel()方法以流的形式返回前端文件
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@RequiresPermissions("system:user:export")
@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user)
{
List<SysUser> list = userService.selectUserList(user);
// 创建 ExcelUtil<SysUser>对象,入参为 SysUser.class
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
util.exportExcel(response, list, "用户数据");
}
2.调用ExcelUtil类
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @param response 返回数据
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @param title 标题
* @return 结果
*/
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String title)
{
//告诉浏览器或客户端,响应的内容是一个 Excel 文件(XLSX 格式)通常用于文件下载
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
//设置 HTTP 响应的字符编码为 UTF-8
response.setCharacterEncoding("utf-8");
//调用init方法对数据处理
this.init(list, sheetName, title, Type.EXPORT);
//对list数据源将其里面的数据导入到excel表单
exportExcel(response);
}
1.init方法
public void init(List<T> list, String sheetName, String title, Type type)
{
// 需要导出的数据交给list
if (list == null)
{
list = new ArrayList<T>();
}
this.list = list;
// 生成execl的sheet名称
this.sheetName = sheetName;
// 类型(0:导出导入;1:仅导出;2:仅导入)
this.type = type;
//赋值标题
this.title = title;
// 主要完成对 List<Object[]> fields 属性的赋值。
createExcelField();
// 创建一个 Workbook 工作簿对象
createWorkbook();
//创建excel第一行标题
createTitle();
//创建对象的子列表名称
createSubHead();
}
1.createExcelField();
private void createExcelField()
{
//获取字段注解信息
this.fields = getFields();
//通过 Java 的 Stream API 对 fields 进行排序
this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
//根据注解获取最大行高
this.maxHeight = getRowHeight();
}
/**
* 获取字段注解信息
*/
public List<Object[]> getFields()
{
List<Object[]> fields = new ArrayList<Object[]>();
List<Field> tempFields = new ArrayList<>();
//获得传入实体类的父类的所有声明字段
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
//获得传入实体类所有声明字段
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
//是否需要自定义显示属性列showColumn设置,详见官网文档
if (StringUtils.isNotEmpty(includeFields))
{
//遍历临时字段集合
for (Field field : tempFields)
{
//判断字段是否在自定义显示属性列中
//判断字段是否有@Excels注解
if (ArrayUtils.contains(this.includeFields, field.getName()) || field.isAnnotationPresent(Excels.class))
{
// 添加字段信息
addField(fields, field);
}
}
}
//hideColumn()自定义隐藏Excel中列属性
else if (StringUtils.isNotEmpty(excludeFields))
{
for (Field field : tempFields)
{
if (!ArrayUtils.contains(this.excludeFields, field.getName()))
{
addField(fields, field);
}
}
}
else
{
for (Field field : tempFields)
{
addField(fields, field);
}
}
return fields;
}
/**
* 添加字段信息
*/
public void addField(List<Object[]> fields, Field field)
{
// 单注解
if (field.isAnnotationPresent(Excel.class))
{
//获得注解实例
Excel attr = field.getAnnotation(Excel.class);
if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
{
//创建一个包含两个元素的数组,其中数组的类型是 Object,元素类型会根据它们的实际类型而定
fields.add(new Object[] { field, attr });
}
//判断field的类型是否是collection集合类型
if (Collection.class.isAssignableFrom(field.getType()))
{
//获得对象的子列表方法
subMethod = getSubMethod(field.getName(), clazz);
// getGenericType()方法获取某个字段的泛型类型的方法
ParameterizedType pt = (ParameterizedType) field.getGenericType();
//getActualTypeArguments()方法从一个泛型类型中获取实际的类型参数,并将其转换为 Class<?> 类型
Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
//通过FieldUtils.getFieldsListWithAnnotationf()方法中反射,找到类subClass中所有带有Excel注解的字段,并将它们存储在 subFields 变量
this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
}
}
// 多注解
if (field.isAnnotationPresent(Excels.class))
{
Excels attrs = field.getAnnotation(Excels.class);
Excel[] excels = attrs.value();
for (Excel attr : excels)
{
if (StringUtils.isNotEmpty(includeFields))
{
if (ArrayUtils.contains(this.includeFields, field.getName() + "." + attr.targetAttr())
&& (attr != null && (attr.type() == Type.ALL || attr.type() == type)))
{
fields.add(new Object[] { field, attr });
}
}
else
{
if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr())
&& (attr != null && (attr.type() == Type.ALL || attr.type() == type)))
{
fields.add(new Object[] { field, attr });
}
}
}
}
}
/**
* 获取对象的子列表方法
*
* @param name 名称
* @param pojoClass 类对象
* @return 子列表方法
*
*/
//通过字段名(name)获取给定类(pojoClass)的 getter 方法。
// 这个方法根据字段名生成对应的 getter 方法名称,然后使用反射来查找该方法
public Method getSubMethod(String name, Class<?> pojoClass)
{
//将字段名 name 转换为标准的 getter 方法名
StringBuffer getMethodName = new StringBuffer("get");
//将字段的第一个字母转为大写
getMethodName.append(name.substring(0, 1).toUpperCase());
//提取 name 字符串从索引位置 1 开始到字符串结束的部分
getMethodName.append(name.substring(1));
Method method = null;
try
{
//pojoClass.getMethod() 方法通过反射查找指定的方法。
// getMethodName.toString() 返回的是动态生成的 getter 方法名
//new Class[] {} 表示该方法不接受任何参数(即没有输入参数)
method = pojoClass.getMethod(getMethodName.toString(), new Class[] {});
}
catch (Exception e)
{
log.error("获取对象异常{}", e.getMessage());
}
return method;
}
1.clazz.getSuperclass().getDeclaredFields()
clazz:是一个 Class 类型的对象,它表示一个 Java 类。
getSuperclass():是 Class 类中的方法,返回该类的父类(即继承自哪个类)。如果该类没有父类(例如 Object 类),则返回 null。
getDeclaredFields():是 Class 类中的方法,返回该类所有声明的字段,包括私有字段和保护字段等(不包括继承的字段)。
例如:Dog类继承Animal类
clazz.getSuperclass() 获取的是 Dog 类的父类 Animal 类。
getDeclaredFields() 获取了 Animal 类中声明的字段(name、age 和 species),它们的访问修饰符分别是 private、protected 和 public。
getDeclaredFields() 不会返回父类继承的字段,如果需要获取父类的字段(包括继承的),可以使用 getFields()。
getDeclaredFields() 回的是类中所有声明的字段,包括 private、protected、public 和默认访问权限(包私有)的字段。Java的private字段访问权限是受保护的,即使反射获取到私有字段,也无法直接访问它,需要使用 setAccessible(true) 来允许访问该字段
2.public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
ava 反射 API 中的方法,用于检查某个字段是否被特定的注解所标注。它属于 java.lang.reflect.Field 类。通过该方法,你可以判断某个字段是否使用了某个注解
对于反射操作非常有用,尤其是在需要处理动态注解或者特定配置时
3.public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
field.getAnnotation 是 Java 反射 API 中 java.lang.reflect.Field 类的一个方法,用于获取指定字段上的特定注解实例。与 isAnnotationPresent 方法不同,getAnnotation 返回注解的实例,如果字段上没有该注解,则返回 null。
4.public boolean isAssignableFrom(Class<?> cls)
Java 中 Class 类的一个方法,用于判断某个类或接口的对象是否可以赋值给另一个类或接口的变量。它在反射或类型检查的场景中非常常见。
检查当前类是否可以接受传入类的实例(即目标类的对象能否赋值给当前类的变量)
参数:
cls:要与当前类进行比较的类或接口的 Class 对象。
返回值:
如果当前类可以接受 cls 类型的实例(即 cls 类型的对象可以赋值给当前类的引用变量),则返回 true。
否则,返回 false。
用于在运行时进行类型兼容性检查的工具,特别适用于需要动态判断类型关系的情况
类型检查:检查某个类或接口是否可以赋值给另一种类型的变量。
反射:在使用反射时,可能需要验证某个对象是否可以赋给特定类型的变量。
动态类型检查:在运行时检查一个对象能否安全地被强制转换为另一类型,避免出现 ClassCastException报错。
5.ParameterizedType pt = (ParameterizedType) field.getGenericType();
getGenericType() 是用于获取某个字段(Field 对象)的泛型类型的方法。返回一个 Type 对象
常见的返回类型:
原始类型: 如果字段没有泛型,getGenericType() 返回的 Type 是该字段的原始类型(例如 int, String 等)。
ParameterizedType: 如果字段的类型是一个带有泛型的类型(如 List<String>),getGenericType() 返回的是 ParameterizedType,它表示该类型的具体泛型参数。
GenericArrayType: 如果字段是泛型数组类型(如 T[] 或 List<String>[]),则返回 GenericArrayType。
ParameterizedType:ParameterizedType 是 Type 接口的一个实现,表示带有类型参数的泛型类型
2.createWorkbook()
/**
* 创建一个工作簿
*/
public void createWorkbook()
{
// 创建一个SXSSFWorkbook工作簿,最大行数为500
this.wb = new SXSSFWorkbook(500);
// 在工作簿中创建一个工作表(Sheet)
this.sheet = wb.createSheet();
// 设置工作表的名称
wb.setSheetName(0, sheetName);
// 创建样式
this.styles = createStyles(wb);
}
1.SXSSFWorkbook类
SXSSFWorkbook
是Apache POI
库中的一个类,专门用于处理大型 Excel 文件。它支持在内存中流式写入数据,而不是将整个工作簿保存在内存中,因此可以处理非常大的文件,而不容易导致内存溢出。采用“滚动式写入”技术,将数据按需写入磁盘500
作为构造函数的参数表示在内存中同时保留的最大行数。在这个例子中,它设置每次最多将 500 行加载到内存中,超出 500 行的数据将被写入磁盘。
3.createTitle()
/**
* 创建excel第一行标题
*/
public void createTitle()
{
if (StringUtils.isNotEmpty(title))
{
//计算标题行的最后一列的列号,默认是 fields 列表的大小减去 1
int titleLastCol = this.fields.size() - 1;
//判断是否有对象的子属性列表
if (isSubList())
{
// 如果有子属性列表,标题的最后一列索引要加上子属性的大小
titleLastCol = titleLastCol + subFields.size() - 1;
}
//创建一个新的行,作为标题行
Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
// 设置标题行的高度为30磅
titleRow.setHeightInPoints(30);
//在标题行的第一个单元格(列索引为 0)创建一个单元格对象。这是标题文本所在的单元格
Cell titleCell = titleRow.createCell(0);
//设置标题单元格的样式
titleCell.setCellStyle(styles.get("title"));
//设置标题单元格的内容为 title
titleCell.setCellValue(title);
//合并单元格:通过 sheet.addMergedRegion() 方法合并标题行的单元格
sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, titleLastCol));
}
}
4.createSubHead()
/**
* 创建对象的子列表名称
*/
public void createSubHead()
{
if (isSubList())
{
//创建一个新行,作为子表头。rownum 是当前的行号,表示标题行之后的行
Row subRow = sheet.createRow(rownum);
int column = 0;
//计算子字段的大小
int subFieldSize = subFields != null ? subFields.size() : 0;
//遍历fields数组。每一项包含一个 Field 对象和一个 Excel 注解对象
for (Object[] objects : fields)
{
//获取字段对象
Field field = (Field) objects[0];
//获取与该字段相关的Excel注解,通常用于指定字段如何映射到Excel表格的列,比如列名、颜色等。
Excel attr = (Excel) objects[1];
//检查当前字段是否为 Collection 类型(如 List, Set 等)
if (Collection.class.isAssignableFrom(field.getType()))
{
//为子表头行的当前列创建一个单元格
Cell cell = subRow.createCell(column);
//将 Excel 注解中的 name 值设置为单元格的内容
cell.setCellValue(attr.name());
//设置单元格样式
cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
//如果子字段的数量大于 1,意味着该字段需要跨越多个列,因此需要合并单元格
if (subFieldSize > 1)
{
//创建一个单元格范围对象
CellRangeAddress cellAddress = new CellRangeAddress(rownum, rownum, column, column + subFieldSize - 1);
//将合并区域添加到表格中
sheet.addMergedRegion(cellAddress);
}
//更新列索引,跳过已合并的子字段列
column += subFieldSize;
}
else
{
Cell cell = subRow.createCell(column++);
cell.setCellValue(attr.name());
cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
}
}
rownum++;
}
}
2.exportExcel(HttpServletResponse response)
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @return 结果
*/
public void exportExcel(HttpServletResponse response)
{
try
{
writeSheet();//将数据写入Excel表单
//将工作簿写入HttpServletResponse输出流,以便客户端下载
wb.write(response.getOutputStream());
}
catch (Exception e)
{
log.error("导出Excel异常{}", e.getMessage());
}
finally
{
IOUtils.closeQuietly(wb);
}
}
/**
* 创建写入数据到Sheet
*/
public void writeSheet()
{
// 取出一共有多少个sheet.
//Math.ceil() 计算总页数,并确保至少有一个工作表(即 Math.max(1, ...))
//list.size() 是数据列表的大小,sheetSize 是每个工作表的最大行数。
int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
for (int index = 0; index < sheetNo; index++)
{
// 创建新的工作表
createSheet(sheetNo, index);
// 产生一行
Row row = sheet.createRow(rownum);
// 初始化列索引
int column = 0;
// 写入各个字段的列头名称
for (Object[] os : fields)
{
Field field = (Field) os[0]; // 获取字段对象
Excel excel = (Excel) os[1]; // 获取Excel注解
if (Collection.class.isAssignableFrom(field.getType()))
{
for (Field subField : subFields)
{
// 获取子字段的Excel注解
Excel subExcel = subField.getAnnotation(Excel.class);
// 创建子字段表头单元格
this.createHeadCell(subExcel, row, column++);
}
}
else // 如果字段不是集合类型
{
// 创建普通字段的表头单元格
this.createHeadCell(excel, row, column++);
}
}
//判断type类型是导出
if (Type.EXPORT.equals(type))
{
// 填充数据到Excel行
fillExcelData(index, row);
// 添加统计行
addStatisticsRow();
}
}
}
/**
* 填充excel数据
*
* @param index 序号
* @param row 单元格行
*/
//@SuppressWarnings 注解用于抑制编译器产生的警告信息
//unchecked 消除 "类型不安全" 警告
@SuppressWarnings("unchecked")
public void fillExcelData(int index, Row row)
{
//根据当前工作表的索引和每个工作表的最大行数,确定当前工作表需要填充的数据范围。
int startNo = index * sheetSize;
int endNo = Math.min(startNo + sheetSize, list.size());
int currentRowNum = rownum + 1; // 从标题行后开始
//遍历数据列表并填充 Excel 行
for (int i = startNo; i < endNo; i++)
{
row = sheet.createRow(currentRowNum);
T vo = (T) list.get(i);
int column = 0;
//获取子列表最大数
int maxSubListSize = getCurrentMaxSubListSize(vo);
for (Object[] os : fields)
{
Field field = (Field) os[0];
Excel excel = (Excel) os[1];
if (Collection.class.isAssignableFrom(field.getType()))
{
try
{
//如果字段是集合类型(如 List 或 Set),首先通过 getTargetValue() 方法获取该字段的值(即集合对象)
Collection<?> subList = (Collection<?>) getTargetValue(vo, field, excel);
//如果集合不为空,遍历集合中的每个元素 subVo(即子对象),并为每个子对象创建一个子行 subRow
if (subList != null && !subList.isEmpty())
{
int subIndex = 0;
for (Object subVo : subList)
{
Row subRow = sheet.getRow(currentRowNum + subIndex);
if (subRow == null)
{
subRow = sheet.createRow(currentRowNum + subIndex);
}
//在子行中,遍历子字段 subFields,为每个子字段创建一个单元格。
int subColumn = column;
for (Field subField : subFields)
{
Excel subExcel = subField.getAnnotation(Excel.class);
//调用 addCell() 方法填充每个子字段的值到 Excel 单元格中
addCell(subExcel, subRow, (T) subVo, subField, subColumn++);
}
subIndex++;
}
//column 变量在处理完一个集合字段后增加,跳到下一个字段的位置
column += subFields.size();
}
}
catch (Exception e)
{
log.error("填充集合数据失败", e);
}
}
else
{
// 创建单元格并设置值
addCell(excel, row, vo, field, column);
if (maxSubListSize > 1 && excel.needMerge())
{
//如果该字段需要合并单元格(excel.needMerge() 返回 true),并且该字段有多个子集合项(maxSubListSize > 1),
// 则通过 sheet.addMergedRegion() 方法合并相应的单元格。
sheet.addMergedRegion(new CellRangeAddress(currentRowNum, currentRowNum + maxSubListSize - 1, column, column));
}
column++;
}
}
//更新当前行号
currentRowNum += maxSubListSize;
}
}
总结:以上是若依导出功能的关键代码,理解的可能不太全面,仅供个人参考