导出(若依框架)
分析用户界面,以用户列表的导出为例。
导出
前端代码
点击导出按钮,触发函数handleExport,在该函数中调用exportUser,exportUser执行完毕后,再调用download方法下载。
exportUser执行完成后,后端会生成临时文件execl。再调用download下载该文件。
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams;
this.$confirm('是否确认导出所有用户数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportUser(queryParams);
}).then(response => {
this.download(response.msg);
})
},
// 导出用户
export function exportUser(query) {
return request({
url: '/system/user/export',
method: 'get',
params: query
})
}
// 通用下载方法
export function download(fileName) {
window.location.href = baseURL + "/common/download?fileName=" + encodeURI(fileName) + "&delete=" + true;
}
后端代码
完成导出共发起了两次请求。分别是生成文件和下载文件。
技术:反射,注解
文件生成
主要分析以下几个方法。方法调用层级关系
export 导出的入口函数
调用userService.selectUserList方法查询需要导出的数据,再调用util.exportExcel生成execl文件。
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:user:export')")
@GetMapping("/export")
public AjaxResult export(SysUser user)
{
List<SysUser> list = userService.selectUserList(user);
// 创建 ExcelUtil<SysUser>对象,入参为 SysUser.class。
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
return util.exportExcel(list, "用户数据");
}
exportExcel
在exportExcel方法中调用了init方法和exportExcel方法
public AjaxResult exportExcel(List<T> list, String sheetName)
{
this.init(list, sheetName, Type.EXPORT);
return exportExcel();
}
init
在init中调用了createExcelField方法,主要完成对ExcelUtil类中的fields属性赋值。
public void init(List<T> list, String sheetName, Type type)
{
if (list == null)
{
list = new ArrayList<T>();
}
this.list = list; // 需要导出的数据交给list
this.sheetName = sheetName; // 生成execl的sheet名称
this.type = type; // 类型(0:导出导入;1:仅导出;2:仅导入)
createExcelField(); // 主要完成对 List<Object[]> fields 属性的赋值。
createWorkbook(); // 创建 Workbook对象 Workbook wb = new SXSSFWorkbook(500)
}
createExcelField
该方法执行完成后,完成了对ExcelUtil对象中的LIst<Object[]> fields属性的赋值。fields存放了导出的信息。
在object[]数组,object[0]存放了java.lang.reflect.Field对象, object[1]存放了注解com.ruoyi.common.annotation.Excel对象。
从object[0]可以获取到字段名称等信息。 从object[1]中可以获取到导出到execl中的名称以及对该字段的值作何处理(如格式化)等信息。
private void createExcelField()
{
this.fields = new ArrayList<Object[]>();
List<Field> tempFields = new ArrayList<>();
// clazz属性是创建ExcelUtil对象时,完成了对该属性的赋值。以用户导出为例,class=SysUser.class。
// 获取该类和其父类的属性字段,存入到tempFields集合中。
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
// 遍历字段,过滤出符合规律的字段。
// 规律:1. 如果该字段有注解@Excel,则将字段对象,和注解对象封装到object[]数组中,并add到fields集合中。
// 规律:2. 如果该字段有@Excels注解,则从该注解对象中获取Excel[]数组进行遍历。并将字段对象和Excel对象封装后,add到 // fields集合中。
for (Field field : tempFields)
{
// 单注解
if (field.isAnnotationPresent(Excel.class))
{
// this.fields.add(new Object[] { field, attr });
putToField(field, field.getAnnotation(Excel.class));
}
// 多注解
if (field.isAnnotationPresent(Excels.class))
{
Excels attrs = field.getAnnotation(Excels.class);
Excel[] excels = attrs.value();
for (Excel excel : excels)
{
putToField(field, excel);
}
}
}
this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
this.maxHeight = getRowHeight();
}
exportExcel 完成execl文件的生成
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @return 结果
*/
public AjaxResult exportExcel()
{
OutputStream out = null;
try
{
// 算出一共有多少个sheet.
// list.size 需要导出的数据条数。 sheetSize = 65536
double sheetNo = Math.ceil(list.size() / sheetSize);
for (int index = 0; index <= sheetNo; index++)
{
// 创建sheet页
createSheet(sheetNo, index);
// 产生一行,表头
Row row = sheet.createRow(0);
int column = 0;
// 写入各个字段的列头名称
// 遍历fields集合,该集合的元素为object[]类型。从os[1]中获取注解对象,创建表头信息。
for (Object[] os : fields)
{
Excel excel = (Excel) os[1];
// 创建单元格,并赋值,完成表头的创建
this.createCell(excel, row, column++);
}
// 如果为导出类型,调用fillExcelData方法填充excel数据。
if (Type.EXPORT.equals(type))
{
// 填充数据
fillExcelData(index, row);
addStatisticsRow();
}
}
// 生成文件名称
String filename = encodingFilename(sheetName);
// 生成的文件路径在application.yml配置 ( profile: D:/ruoyi/uploadPath)
out = new FileOutputStream(getAbsoluteFile(filename));
// 生成execl文件,此时生成的文件在服务端。
wb.write(out);
// 将生成的文件名称封装到AjaxResult对象中
return AjaxResult.success(filename);
}
catch (Exception e)
{
log.error("导出Excel异常{}", e.getMessage());
throw new CustomException("导出Excel失败,请联系网站管理员!");
}
finally
{
// 省略
}
}
fillExcelData 完成execl数据填充
/**
* 填充excel数据
*
* @param index 序号
* @param row 单元格行
*/
public void fillExcelData(int index, Row row)
{
// 以第一个sheet页为例 index = 0, 常量sheetSize=65536
// startNo是数据开始下标
int startNo = index * sheetSize;
// endNo-1是数据结束下标
int endNo = Math.min(startNo + sheetSize, list.size());
for (int i = startNo; i < endNo; i++)
{
// 创建行对象,每一个sheet页从第二行开始,第一行为标题行。
row = sheet.createRow(i + 1 - startNo);
// 得到导出对象.
T vo = (T) list.get(i);
int column = 0;
// 遍历fields
for (Object[] os : fields)
{
Field field = (Field) os[0];
Excel excel = (Excel) os[1];
// 设置实体类私有属性可访问
field.setAccessible(true);
// 将导出信息 和 数据对象 execl的行对象 交由 addCell处理。
this.addCell(excel, row, vo, field, column++);
}
}
}
addCell 完成对行记录的填充。
创建单元格,填充单元格内容。
/**
* 添加单元格
*/
public Cell addCell(Excel attr, Row row, T vo, Field field, int column)
{
Cell cell = null;
try
{
// 设置行高
row.setHeight(maxHeight);
// 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
if (attr.isExport())
{
// 创建cell
cell = row.createCell(column);
int align = attr.align().value();
cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : "")));
// 用于读取对象中的属性
// 通过Object o = field.get(vo); 得到字段的属性对象。
// 如果注解属性targetAttr为空,直接返回o.
// 若果不为空,该字段是否有小数点为标准再做处理
// 该方法详解见下文。
Object value = getTargetValue(vo, field, attr);
// 字段的日期格式
String dateFormat = attr.dateFormat();
// 读取内容转表达式 (如: 0=男,1=女,2=未知)
String readConverterExp = attr.readConverterExp();
// 分隔符,读取字符串组内容
String separator = attr.separator();
// 字典类型 (如: sys_user_sex)
String dictType = attr.dictType();
// 根据字段的处理策略。填充单元格。
if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value))
{
cell.setCellValue(DateUtils.parseDateToStr(dateFormat, (Date) value));
}
else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value))
{
cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
}
else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value))
{
cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator));
}
else if (value instanceof BigDecimal && -1 != attr.scale())
{
cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString());
}
else
{
// 设置列类型
setCellVo(value, attr, cell);
}
addStatisticsData(column, Convert.toStr(value), attr);
}
}
catch (Exception e)
{
log.error("导出Excel失败{}", e);
}
return cell;
}
getTargetValue 获取bean中的属性值
/**
* 获取bean中的属性值
*
* @param vo 实体对象
* @param field 字段
* @param excel 注解
* @return 最终的属性值
* @throws Exception
*/
private Object getTargetValue(T vo, Field field, Excel excel) throws Exception
{
// 通过反射获取字段的值。该值有可能是其他类的对象。
Object o = field.get(vo);
// 如果 excel.targetAttr()不为空,则说明o是其他类中的一个对象。
if (StringUtils.isNotEmpty(excel.targetAttr()))
{
// 获取注解 targetAttr属性(另一个类中的属性名称,支持多级获取,以小数点隔开)
String target = excel.targetAttr();
if (target.indexOf(".") > -1)
{
// 如果该属性有小数点,分割为数组遍历。
// 多级获取逻辑
// 举例说明:A类中持有B类的对象,B类中持有C类的对象。 导出A类数据时,需要导出C类的一个属性值
// 可以使用.隔开。
String[] targets = target.split("[.]");
for (String name : targets)
{
o = getValue(o, name);
}
}
else
{
// 如果不包含小数点,(o, target) o为其他类对象,target为该类中的字段名称值。
// getValue 是通过o.getClass获取class对象,通过target字段名称从class中获取到Filed对象。进而得到filed字段值
// 例子:SysUser#dept字段。
o = getValue(o, target);
}
}
return o;
}
/**
* 以类的属性的get方法方法形式获取值
*
* @param o
* @param name
* @return value
* @throws Exception
*/
private Object getValue(Object o, String name) throws Exception
{
if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name))
{
Class<?> clazz = o.getClass();
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
o = field.get(o);
}
return o;
}
小结
自定义注解,描述Bean字段在Execl的表现形式。(比如,字段的值否需要格式化,字段对应到表格中的列名称等等)
使用List<object[]> 存入类的Filed对象和注解对象。(比如导出的字段有10个,则该集合大小为10)
导出执行的大致逻辑:
根据注解信息完成List<object[]>集合的赋值。
根据导出的数据量,计算需要导出的sheet页。针对每一个sheet页进行处理
创建表头:遍历List<object[]>集合,通过object[1]得到字段的注解信息,创建sheet页表头。
填充数据: 根据sheet页数,计算对应的数据范围,循环创建行对象,根据循环下标,在数据集合中获取改行对应的数据对象。行中创建单元格对象,接着遍历List<object[]>集合,通过object[0]获取数据对象中属性值,通过object[1]>获取对该值的处理策略。处理完毕后,将值填充到单元格中。
文件下载
文件生成后,将生成的文件名称返回到前端,客户端在调用download方法,向后端发起下载请求。
fileDownload下载入口方法
/**
* 通用下载请求
*
* @param fileName 文件名称
* @param delete 是否删除
*/
@GetMapping("common/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
{
try
{
if (!FileUtils.checkAllowDownload(fileName))
{
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
}
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
// 获取下载路劲
String filePath = RuoYiConfig.getDownloadPath() + fileName;
// 设置ContentTyp="application/octet-stream" 通用Mime类型
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 设置Content-disposition
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
// 下载后是否删除该文件
if (delete)
{
FileUtils.deleteFile(filePath);
}
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}