导出(若依框架)

导出(若依框架)

分析用户界面,以用户列表的导出为例

在这里插入图片描述

导出

前端代码

​ 点击导出按钮,触发函数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);
    }
}
  • 12
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值