导出(若依框架)
分析用户界面,以用户列表的导出为例。
导出
前端代码
点击导出按钮,触发函数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; // 需要导出的数据交给listthis.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 = 65536double 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;// 遍历fieldsfor (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()){// 创建cellcell = 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);}
}