RuoYi-Vue框架的Excel学习总结

Excel案例

Poi简介

Apache POI [1] 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。

HSSF一读写Microsoft Excel XLS

xSSF一读写Microsoft Excel OOXML XLSX

HWPF一读写Microsoft Word Doc

HSLF一提供读写Microsoft PowerPoint

Java有三个类可使用

HSSFWorkbook:是操作Excel2003以前(包括2003)的版本,扩展名是.xls;

XSSFWorkbook:是操作Excel2007后的版本,扩展名是.xlsx;

SXSSFWorkbook:是操作Excel2007后的版本,扩展名是.xlsx;

主要内容

Poi封装的对象

  1. XSSFWorkbook;工作簿(即整个excel文件)
  2. XSSFSheet:工作表(即excel文件的一个选项卡,选项卡在可视化界面下方)
  3. Row:行(即一整行)
  4. Cell:单元格(即一格)

从Excel文件读取数据

步骤
  1. 创建工作簿
  2. 获取工作表
  3. 遍历工作表获得行对象
  4. 遍历行对象获取单元格对象
  5. 获得单元格中的值
案例
public class Demo01 {
    public static void main(String[] args) throws IOException {
        //1. 获取工作簿
        XSSFWorkbook sheets = new XSSFWorkbook("C:\\Users\\L\\Desktop\\the test.xlsx");


        //2. 获取工作表
        //sheets.getSheet(); 根据选项卡名称,即工作表名称
        //根据索引获取工作表
        XSSFSheet sheetAt = sheets.getSheetAt(0);

        //3.1. 获取行
        for (Row cells : sheetAt) {
            //4.1. 获取单元格
            for (Cell cell : cells) {
                //获取单元格中的值
                String value = cell.getStringCellValue();
                System.out.println(value);
            }
        }

        //5. 释放资源
        sheets.close();
    }
}

其中第三步和第四步可以用普通for循环(主要是需要获取索引)

        //3.2. 获取行
        //获取最后一行索引
        int lastRowNum = sheetAt.getLastRowNum();
        for (int i = 0; i<lastRowNum; i++) {
            //获取当前行数据
            XSSFRow row = sheetAt.getRow(i);
            if (row != null) {
                //获取当前行最后一个单元格索引
                short lastCellNum = row.getLastCellNum();
                for (int j = 0; j < lastCellNum; j++) {
                    //获取当前党员格数据
                    XSSFCell cell = row.getCell(j);
                    if (cell != null) {
                        String stringCellValue = cell.getStringCellValue();
                        System.out.println(stringCellValue);
                    }
                }
            }
        }

向Excel文件写入数据

步骤
  1. 创建一个Excel文件
  2. 创建工作表
  3. 创建行
  4. 创建单元格赋值
  5. 通过输出流将对象下载到磁盘
案例
public class Demo02 {
    public static void main(String[] args) throws IOException {
        //1. 创建工作薄
        XSSFWorkbook sheets = new XSSFWorkbook();

        //2. 创建工作表
        XSSFSheet sheet = sheets.createSheet("工作表一");

        //3. 创建行
        for (int i = 0; i < 3; i++) {
            XSSFRow row = sheet.createRow(i);
            //4. 创建单元格
            row.createCell(0).setCellValue("I" + i);
            row.createCell(1).setCellValue("Love" + i);
            row.createCell(2).setCellValue("You" + i);
        }

        //5. 创建输出流
        FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\L\\Desktop\\the hh.xlsx");

        //将内容写入文件
        sheets.write(fileOutputStream);

        //刷新
        fileOutputStream.flush();

        //6. 释放资源
        fileOutputStream.close();
        sheets.close();
        System.out.println("写入成功");

    }
}

Excel若依

简介

若依中一共有两个Excel注解,分别是@Excel@Excels;后者是前者的数组集

注解路径:/ruoyi-common/src/main/java/com/ruoyi/common/annontation(两个都在包内)

实现类路径:/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil

使用

// 单个字段导出
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT)
private Dept dept;

// 多个字段导出
@Excels({
    @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
    @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
})
private Dept dept;

ExcelUtil

路径:/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil

导出

导出就是把数据库查出的数据从集合转为Excel文件的过程(当然还要发给客户端或者直接下载下来)

init()

调用导出方法,最终都会调用init方法

作用:最终会给ExcelUtil类的几个属性赋值;分别是list,sheetName,type,title,fields,maxHeight,subMethod(依情况而定),subFields(依情况而定),wb,sheet,styles

    public void init(List<T> list, String sheetName, String title, Type type)
    {
        if (list == null)
        {
            list = new ArrayList<T>();
        }
        //list是数据列表,就是要操作的主体
        this.list = list;
        //工作表名称
        this.sheetName = sheetName;
        //导出类型(EXPORT:导出数据;IMPORT:导入模板),不过还有个枚举(ALL(0):导出导入)
        this.type = type;
        //标题,应该是表中数据第一行,会
        this.title = title;
        createExcelField();
        createWorkbook();
        createTitle();
        createSubHead();
    }
createExcelField()

作用:得到所有定义字段的信息(fields是注解列表,maxHeight是最大高度)

 private void createExcelField()
    {
     // List<Object[]> fields 注解列表;Object[]中包含某个实体类属性的信息(Field)和其注解信息
     // 实体类属性的信息:
     // 例如:
     // private static String name;
     // private static java.lang.String com.lovli.other.test.name
        this.fields = getFields();
     // 似乎是按照Excel注解信息来排序,然后重新赋值
        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
     // 获取最大行高
        this.maxHeight = getRowHeight();
    }
  1. getFields()封装了获取字段注解信息的过程

    作用:传入ExcelUtil的实体类,获取头上带有俩注解之一的属性(包括父类的),经过一点逻辑,然后返回赋值给ExcelUtil的fields属性;同时,如果实体类中有集合属性存在,也会给subMethodsubFields赋值

        public List<Object[]> getFields()
        {
            List<Object[]> fields = new ArrayList<Object[]>();
            //保存当前实体类所有的字段,包括继承的父类的字段
            List<Field> tempFields = new ArrayList<>();
            //getSuperclass() 这个方法的返回类型是Class,它返回此对象表示的实体的超类
            //getDeclaredFields() 此方法的返回类型为Field[] ,它返回一个Field对象数组,该数组表示该类的所有已声明字段(不包括继承的Fields)
            //这两行意思就是:上一行获取当前类父类的所有已声明字段,下一行获取当前类自身的所有已声明字段
            tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
            tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            for (Field field : tempFields)
            {
                //excludeFields是隐藏的属性(就是程序员要主动忽略的某个实体属性)
                //判断当前字段是否在需要隐藏属性的数组中,不在的话再进行下一步,否则就进入下一次循环
                if (!ArrayUtils.contains(this.excludeFields, field.getName()))
                {
                    //isAnnotationPresent() 指定类型的注释存在于此元素上返回true,否则false
                    //当前属性头上有没有@Excel或者@Excels注解,有继续,没有结束
                    
                    // 单注解
                    if (field.isAnnotationPresent(Excel.class))
                    {
                        //Excel 注释类的指定对象
                        Excel attr = field.getAnnotation(Excel.class);
                        if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
                        {
                            //设置权限
                            //通俗讲就是为true时,不会去检查java语言权限控制,可以获取到private标识的属性等;这里设置为true
                            field.setAccessible(true);、
                            fields.add(new Object[] { field, attr });
                        }
                        
                        //a.isAssignableFrom(b)
                        //满足下方条件其一返回true,否则返回false
                        //(1)a对象所对应类信息是b对象所对应的类信息的父类或者是父接口,简单理解即a是b的父类或接口
                        //(2)a对象所对应类信息与b对象所对应的类信息相同,简单理解即a和b为同一个类或同一个接口
                        
                        //字面意思:判断当前字段类型是不是包含在集合中,即判断是不是集合的一种
                        
                        //这个if就是如果当前字段是集合,获取跟该字段有关的方法和属性信息的集合分别赋值给subMethod和subFields
                        if (Collection.class.isAssignableFrom(field.getType()))
                        {
                            //getSubMethod() 根据字段名字(字段名字会拼接,最终是要获得的方法的名字)最终获得该类声明的公开的方法
                            //subMethod(对象的子列表方法)是成员变量,给其赋值
                            subMethod = getSubMethod(field.getName(), clazz);
                            
                            //getType():返回一个 Class 对象,它标识了此 Field 对象所表示字段的声明类型
                            //getGenericType():返回一个 Type 对象,它表示此 Field 对象所表示字段的声明类型
                            //例子:
                            //Field:name (String类型) 			   Field:type (T类型)
                            //Type:									Type:
                            //  java.lang.String					  java.lang.Object
                            //GenericType:							 GenericType:
                            //  class java.lang.String				  T
                            
                            //获得当前字段的类型
                            ParameterizedType pt = (ParameterizedType) field.getGenericType();
                            //getActualTypeArguments()[0] 从一个泛型类型中获取第一个泛型参数的类型类
                            Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
                            //返回的是字段上有指定注解的字段,返回一个字段集合赋给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 (attr != null && (attr.type() == Type.ALL || attr.type() == type))
                            {
                                field.setAccessible(true);
                                fields.add(new Object[] { field, attr });
                            }
                        }
                    }
                }
            }
            return fields;
        }
    
  2. stream()流的作用

    //1. sorted() 默认使用自然序排序, 其中的元素必须实现Comparable 接口
    //2. sorted(Comparator<? super T> comparator) :我们可以使用lambada 来创建一个Comparator 实例。可以按照升序或着降序来排序元素
    
  3. getRowHeight()遍历所有注解,找出最大行高并返回maxHeight * 20

    public short getRowHeight()
        {
            double maxHeight = 0;
        //遍历所有字段,获取其注解信息,然后找到最大的height属性,最后返回
            for (Object[] os : this.fields)
            {
                //os[1],即获取注解信息
                Excel excel = (Excel) os[1];
                maxHeight = Math.max(maxHeight, excel.height());
            }
            return (short) (maxHeight * 20);
        }
    
createWorkbook()

作用:创建一个工作薄(wb是工作薄对象xlsx版,sheet是工作表对象,styles是样式列表(map集合))

    public void createWorkbook()
    {
        this.wb = new SXSSFWorkbook(500);
        //Workbook已有的创建工作表的方法
        this.sheet = wb.createSheet();
        //给第一个(索引)工作表命名
        wb.setSheetName(0, sheetName);
        //Map<String, CellStyle> styles
        //创建所有的样式,赋值给styles
        this.styles = createStyles(wb);
    }
  1. new SXSSFWorkbook(500)创建一个工作薄对象,会限制行数为500

    //当rowAccessWindowSize达到限定值时,新一行数据的加入会引起老一行的数据刷新到硬盘
    //就是当行号超过500,达到501时,会把0的记录刷新到硬盘并从内存中删除
    public SXSSFWorkbook(int rowAccessWindowSize){}
    
  2. createStyles()创建表格样式,如某行单元格的大小、字体等

        private Map<String, CellStyle> createStyles(Workbook wb)
        {
            // 写入各条记录,每条记录对应excel表中的一行
            Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
            
            // 创建一种单元格样式,命名title存入map中
            CellStyle style = wb.createCellStyle();
            style.setAlignment(HorizontalAlignment.CENTER);
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            Font titleFont = wb.createFont();
            titleFont.setFontName("Arial");
            titleFont.setFontHeightInPoints((short) 16);
            titleFont.setBold(true);
            style.setFont(titleFont);
            styles.put("title", style);
    
            // 创建一种单元格样式,命名data存入map中
            style = wb.createCellStyle();
            style.setAlignment(HorizontalAlignment.CENTER);
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            style.setBorderRight(BorderStyle.THIN);
            style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderLeft(BorderStyle.THIN);
            style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderTop(BorderStyle.THIN);
            style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderBottom(BorderStyle.THIN);
            style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            Font dataFont = wb.createFont();
            dataFont.setFontName("Arial");
            dataFont.setFontHeightInPoints((short) 10);
            style.setFont(dataFont);
            styles.put("data", style);
    
            // 创建一种单元格样式,命名total存入map中
            style = wb.createCellStyle();
            style.setAlignment(HorizontalAlignment.CENTER);
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            Font totalFont = wb.createFont();
            totalFont.setFontName("Arial");
            totalFont.setFontHeightInPoints((short) 10);
            style.setFont(totalFont);
            styles.put("total", style);
    
            // 获取所有字段的注解,根据注解生成单元格样式,在map中key命名都是header开头
            styles.putAll(annotationHeaderStyles(wb, styles));
    
            // 同上,不过key命名都是data开头
            styles.putAll(annotationDataStyles(wb));
    
            return styles;
        }
    
    
createTitle()

作用:创建excel第一行标题

    public void createTitle()
    {
        if (StringUtils.isNotEmpty(title))
        {
            //合并后开始行数,默认为1
            subMergedFirstRowNum++;
            //合并后最后行数,默认为0
            subMergedLastRowNum++;
            //titleLastCol是字段数量的索引
            int titleLastCol = this.fields.size() - 1;
            //如果subFields有值(可以理解为,如果需要导出的实体类有集合存在)
            if (isSubList())
            {
                titleLastCol = titleLastCol + subFields.size() - 1;
            }
            //rownum始终为1或者0;创建第一行,即一般的标题行
            Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
            //设置的值永远是height属性值的30倍
            titleRow.setHeightInPoints(30);
            //创建该行索引为0的单元格
            Cell titleCell = titleRow.createCell(0);
            //从styles(一个存储了表格样式的map集合)中获取样式,赋予这个单元格
            titleCell.setCellStyle(styles.get("title"));
            //标题赋予这个单元格
            titleCell.setCellValue(title);
            //addMergedRegion合并单元格
            //CellRangeAddress(int firstRow, int lastRow, int firstCol, int lastCol)
            //这里是索引
            //起始行 结束行 起始列 结束列
            sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol));
        }
    }

createSubHead()

作用:创建对象的子列表名称

    public void createSubHead()
    {
        //判断是否有子列表
        if (isSubList())
        {
            //合并后开始行数,默认为1(此时必然不为1了)
            subMergedFirstRowNum++;
            //合并后最后行数,默认为0(此时必然不为0了)
            subMergedLastRowNum++;
            //在当前行号创建行,一般就是标题的下一行了,属性行
            Row subRow = sheet.createRow(rownum);
            int excelNum = 0;
            for (Object[] objects : fields)
            {
                Excel attr = (Excel) objects[1];
                //根据循环来创建第几个单元格,并附上名字和样式
                Cell headCell1 = subRow.createCell(excelNum);
                headCell1.setCellValue(attr.name());
                headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
                excelNum++;
            }
            int headFirstRow = excelNum - 1;
            int headLastRow = headFirstRow + subFields.size() - 1;
            //可以理解为:如果有集合存在
            if (headLastRow > headFirstRow)
            {
                //合并当前行的单元格(给集合使用的单元格)
                //看样子默认放在excel最右列了
                sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow));
            }
            rownum++;
        }
    }

exportExcel()

init()方法之后,最后都会调用该方法,该方法有两个,一个带参数,一个不带参数

作用:根据init()方法初始化的属性,来创建excel表,并填入数据

两者区别:

  1. exportExcel(HttpServletResponse response)利用servlet容器,把文件响应给客户端
  2. exportExcel()根据配置文件路径,下载文件到指定路径下
exportExcel(HttpServletResponse response)

目前框架已有的导入导出都使用的该方法

    //servlet容器针对本次请求,创建了一个response对象,然后作为参数传给controller层,然后传入该方法
	//其中封装了HTTP相应消息
	public void exportExcel(HttpServletResponse response)
    {
        try
        {
            //创建写入数据到Sheet
            writeSheet();
            //workbook的方法,用来将写入工作薄中
            //只要有一个流(getOutputStream()方法)被创建了,并且已经完成了流的输出那么servlet容器就会将response对象交给服务器。服务器将response对象中的内容做拆解响应给客户端。
            wb.write(response.getOutputStream());
        }
        catch (Exception e)
        {
            log.error("导出Excel异常{}", e.getMessage());
        }
        finally
        {
            //用来释放资源
            IOUtils.closeQuietly(wb);
        }
    }
  1. writeSheet()创建工作表做好样式并填入数据,可以说是主体了

        public void writeSheet()
        {
            // 取出一共有多少个sheet.
            // sheetSize是当前工作表的最大行数,当超过最大行数时,才会有多个工作表,否则就一个
            int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
            for (int index = 0; index < sheetNo; index++)
            {
                //该方法就是创建工作表
                //条件是:当sheetNo大于1并且index大于0时才会创建新的工作表
                //可以理解就是没有超过sheetSize限制数量的数据时,不需要创建新的工作表
                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];
                    //判断该字段是否是集合
                    //是集合就根据集合内属性创建单元格样式,填充内容;集合本身的名字是父标题
                    if (Collection.class.isAssignableFrom(field.getType()))
                    {
                        for (Field subField : subFields)
                        {
                            Excel subExcel = subField.getAnnotation(Excel.class);
                            //创建header单元格的方法
                            this.createHeadCell(subExcel, row, column++);
                        }
                    }
                    else
                    {
                        this.createHeadCell(excel, row, column++);
                    }
                }
                if (Type.EXPORT.equals(type))
                {
                    //这个方法就是填充所有数据到工作表中的方法
                    fillExcelData(index, row);
                    //创建统计列表
                    //在每一个工作表数据的最后一行的后面新建一行,用来统计该行之前的数据
                    addStatisticsRow();
                }
            }
        }
    
    
exportExcel()

目前框架内还没使用

    public AjaxResult exportExcel()
    {
        OutputStream out = null;
        try
        {
            //同上方法
            writeSheet();
            //生成文件名
            String filename = encodingFilename(sheetName);
            //创建输出流,getAbsoluteFile(filename)返回下载路径;路径可以在ruoyi-admin模块下的配置文件中更改
            out = new FileOutputStream(getAbsoluteFile(filename));
            //写入数据,并生成文件在指定路径下
            wb.write(out);
            return AjaxResult.success(filename);
        }
        catch (Exception e)
        {
            log.error("导出Excel异常{}", e.getMessage());
            throw new UtilException("导出Excel失败,请联系网站管理员!");
        }
        finally
        {
            //释放两个资源
            IOUtils.closeQuietly(wb);
            IOUtils.closeQuietly(out);
        }
    }

fillExcelData()

解释:上述方法中writeSheet()下填充所有数据到工作表中的方法

    //注解 压制警告 index 当前工作表的索引 row 当前行(应该是属性行)
	@SuppressWarnings("unchecked")
    public void fillExcelData(int index, Row row)
    {
        //开始行,最小0
        int startNo = index * sheetSize;
        //结束行
        int endNo = Math.min(startNo + sheetSize, list.size());
        //当前行,这里减startNo,是为了下面for循环,是为了不同工作表考虑;可以理解为当前当前工作表的当前行,不会超过限制行数
        int rowNo = (1 + rownum) - startNo;
        //下面就是遍历每一行,然后再遍历每一列,给每个单元格填入数据,不再解释了
        for (int i = startNo; i < endNo; i++)
        {
            rowNo = i > 1 ? rowNo + 1 : rowNo + i;
            row = sheet.createRow(rowNo);
            // 得到导出对象.
            T vo = (T) list.get(i);
            Collection<?> subList = null;
            if (isSubList())
            {
                if (isSubListValue(vo))
                {
                    subList = getListCellValue(vo);
                    subMergedLastRowNum = subMergedLastRowNum + subList.size();
                }
                else
                {
                    subMergedFirstRowNum++;
                    subMergedLastRowNum++;
                }
            }
            int column = 0;
            for (Object[] os : fields)
            {
                Field field = (Field) os[0];
                Excel excel = (Excel) os[1];
                if (Collection.class.isAssignableFrom(field.getType()) && StringUtils.isNotNull(subList))
                {
                    boolean subFirst = false;
                    for (Object obj : subList)
                    {
                        if (subFirst)
                        {
                            rowNo++;
                            row = sheet.createRow(rowNo);
                        }
                        List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class);
                        int subIndex = 0;
                        for (Field subField : subFields)
                        {
                            if (subField.isAnnotationPresent(Excel.class))
                            {
                                subField.setAccessible(true);
                                Excel attr = subField.getAnnotation(Excel.class);
                                this.addCell(attr, row, (T) obj, subField, column + subIndex);
                            }
                            subIndex++;
                        }
                        subFirst = true;
                    }
                    this.subMergedFirstRowNum = this.subMergedFirstRowNum + subList.size();
                }
                else
                {
                    this.addCell(excel, row, vo, field, column++);
                }
            }
        }
    }

导入

importExcel()

调用导入方法,最终都是调用该方法(巨长)

作用:就是把excel表格不为空的列和实体类比较;然后类型转换,把工作薄完全转成集合,最终继续其他方法,将之导入数据库

    //sheetName 表格索引名,默认为无 is 文件输入流 titleNum 标题占用行数
	public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception
    {
        //类型设置为仅导入
        this.type = Type.IMPORT;
        //根据文件输入流创建工作薄赋值给wb
        this.wb = WorkbookFactory.create(is);
        List<T> list = new ArrayList<T>();
        // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
        Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
        if (sheet == null)
        {
            throw new IOException("文件sheet不存在");
        }
        //判断传入excel是老版还是新版,老版 false 新版 true
        boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook);
        //key:图片单元格索引(1_1)
        Map<String, PictureData> pictures;
        
        //判断版本,传入不同方法,获取图片流PictureData
        if (isXSSFWorkbook)
        {
            pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb);
        }
        else
        {
            pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb);
        }
        // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
        int rows = sheet.getLastRowNum();

        if (rows > 0)
        {
            // 定义一个map用于存放excel列的序号和field.
            Map<String, Integer> cellMap = new HashMap<String, Integer>();
            // 获取表头
            Row heard = sheet.getRow(titleNum);
            // getPhysicalNumberOfCells()获取不为空的列个数
            for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
            {
                Cell cell = heard.getCell(i);
                //如果当前单元格不是空,就把单元格内容和其列索引存入cellMap
                if (StringUtils.isNotNull(cell))
                {
                    String value = this.getCellValue(heard, i).toString();
                    cellMap.put(value, i);
                }
                else
                {
                    cellMap.put(null, i);
                }
            }
            // 有数据时才处理 得到类的所有field.
            List<Object[]> fields = this.getFields();
            Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
            for (Object[] objects : fields)
            {
                Excel attr = (Excel) objects[1];
                //获取当前属性所对应的excel列的索引
                Integer column = cellMap.get(attr.name());
                if (column != null)
                {
                    fieldsMap.put(column, objects);
                }
            }
            //从头行下一行到末行遍历
            for (int i = titleNum + 1; i <= rows; i++)
            {
                // 从第2行开始取数据,默认第一行是表头.
                Row row = sheet.getRow(i);
                // 判断当前行是否是空行
                if (isRowEmpty(row))
                {
                    continue;
                }
                T entity = null;
                //遍历所有不为空的列
                for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet())
                {
                    //获取当前行当前列的单元格的值
                    Object val = this.getCellValue(row, entry.getKey());

                    // 如果不存在实例则新建.
                    entity = (entity == null ? clazz.newInstance() : entity);
                    // 从map中得到对应列的field.
                    Field field = (Field) entry.getValue()[0];
                    Excel attr = (Excel) entry.getValue()[1];
                    // 取得类型,并根据对象类型设置值.
                    Class<?> fieldType = field.getType();
                    //下面一系列判断当前列需要的数据的类型(实体类属性的类型),并给获取的单元格的值转成需要的类型
                    if (String.class == fieldType)
                    {
                        String s = Convert.toStr(val);
                        if (StringUtils.endsWith(s, ".0"))
                        {
                            //从".0"第一次出现的位置向前截取
                            val = StringUtils.substringBefore(s, ".0");
                        }
                        else
                        {
                            //如果注解有日期格式,就根据日期格式来更新字符串
                            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
                            if (StringUtils.isNotEmpty(dateFormat))
                            {
                                val = parseDateToStr(dateFormat, val);
                            }
                            else
                            {
                                val = Convert.toStr(val);
                            }
                        }
                    }
                    //isNumeric()检测变量是否为数字或数字字符串
                    else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
                    {
                        val = Convert.toInt(val);
                    }
                    else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
                    {
                        val = Convert.toLong(val);
                    }
                    else if (Double.TYPE == fieldType || Double.class == fieldType)
                    {
                        val = Convert.toDouble(val);
                    }
                    else if (Float.TYPE == fieldType || Float.class == fieldType)
                    {
                        val = Convert.toFloat(val);
                    }
                    else if (BigDecimal.class == fieldType)
                    {
                        val = Convert.toBigDecimal(val);
                    }
                    else if (Date.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            val = DateUtils.parseDate(val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    }
                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
                    {
                        val = Convert.toBool(val, false);
                    }
                    //如果当前列的类型不是null
                    if (StringUtils.isNotNull(fieldType))
                    {
                        //获取实体类属性的名字
                        String propertyName = field.getName();
                        //根据注解上的设置来更新val(targetAttr另一个类中的属性名称,支持多级获取,以小数点隔开)
                        //例子:
                        //@Excels({
                        //@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
                        //@Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
                    	//})
                        //private SysDept dept;
                        //有targetAttr就说明当前这个属性是其他实体类的,当前属性名无法在改实体类匹配的数据表中找到
                        if (StringUtils.isNotEmpty(attr.targetAttr()))
                        {
                            propertyName = field.getName() + "." + attr.targetAttr();
                        }
                        else if (StringUtils.isNotEmpty(attr.readConverterExp()))
                        {
                            val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
                        }
                        else if (StringUtils.isNotEmpty(attr.dictType()))
                        {
                            val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator());
                        }
                        else if (!attr.handler().equals(ExcelHandlerAdapter.class))
                        {
                            val = dataFormatHandlerAdapter(val, attr);
                        }
                        else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures))
                        {
                            PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey());
                            if (image == null)
                            {
                                val = "";
                            }
                            else
                            {
                                byte[] data = image.getData();
                                val = FileUtils.writeImportBytes(data);
                            }
                        }
                        //propertyName是用来找方法的,匹配函数名
                        ReflectUtils.invokeSetter(entity, propertyName, val);
                    }
                }
                //将当前行数据转为实体类后加入集合
                list.add(entity);
            }
        }
        return list;
    }

invokeSetter()

作用:如果属性需要从其他实体类中找,在这里做逻辑处理;会执行实体类中的set、get方法

    //obj 当前这个实体类对象 propertyName 属性名 value 根据属性类型转换过一次类型的单元格名称(如果类型不是基本/引用类型大概率还没转换过)
	public static <E> void invokeSetter(Object obj, String propertyName, E value)
    {
        Object object = obj;
        //如果propertyName根据.分割
        String[] names = StringUtils.split(propertyName, ".");
        for (int i = 0; i < names.length; i++)
        {
            //数组应该最多存在两个内容:因为excel的一个单元格嘛
            //属性名.targetAttr(only one),如果是多注解,另一个targetAttr在下次循环
            //如果没有targetAttr,走下面拼接set前缀的方法
            //如果有targetAttr,属性名走上面拼接get前缀的方法,targetAttr走下面拼接set前缀的方法
            if (i < names.length - 1)
            {
                String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
                //根据这个get方法名,找到这个对象中的该方法,并执行;会给该属性赋值上另一个对象(容器一般会注入,再触发一次是为了避免出现null吗)
                object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {});
            }
            else
            {
                String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
                //获取这个set方法,再获取这个方法的参数类型
                //当value有值,并且value的类型和查找的方法(实体类中的set方法)的参数类型不同时,才会转型;最终执行该set方法
                invokeMethodByName(object, setterMethodName, new Object[] { value });
            }
        }
    }

### ruoyi-vue 动态选择导出特定列到 Excel 的实现方法 在 `ruoyi-vue` 项目中,为了实现在前端根据用户的选项动态导出特定列至 Excel 文件的功能,主要涉及两个方面的工作:一是收集用户的选择;二是通过 API 请求传递这些参数并处理数据。 #### 收集用户选择 通常情况下,在页面上会有一个多选框组供用户勾选出希望导出的字段。当用户点击导出按钮时,JavaScript 或 Vue.js 将获取当前被选中的项作为数组形式的数据[^1]。 ```javascript // 假设这是用于存储所选列名的状态变量 const selectedColumns = ref([]); // 处理表单提交事件函数 function handleExport() { const columnsToExport = selectedColumns.value; // 发送请求给服务器端API... } ``` #### 构建 API 接口支持自定义列导出 服务端需要扩展现有的导出逻辑来接收来自客户端指定的列列表,并据此调整最终生成的 Excel 文档结构。这可能涉及到修改控制器层以及相应的业务逻辑部分以适应新的需求[^2]。 对于 RuoYi 框架而言,可以在原有基础上增加对传入参数的支持: - 修改 Controller 中的方法签名以便接受额外的查询字符串或 JSON 负载; - 更新 Service 层内的实现细节,确保只提取必要的数据库记录属性填充到目标工作簿里去。 具体来说,如果原来的服务只是简单遍历整个实体对象,则现在应该依据接收到的列配置过滤掉不需要的部分再继续后续操作。 ```java @PostMapping("/exportSelected") @ResponseBody public AjaxResult exportSelected(@RequestParam List<String> columnNames) throws Exception { // 获取所需数据源 List<YourEntityClass> list = yourService.selectList(); // 创建一个新的 Workbook 对象准备写入文件流 SXSSFWorkbook wb = new SXSSFWorkbook(); try (OutputStream os = new ByteArrayOutputStream()) { Sheet sheet = wb.createSheet("data"); Row headerRow = sheet.createRow(0); int cellIndex = 0; // 设置头部信息 for (String columnName : columnNames) { Cell cell = headerRow.createCell(cellIndex++); cell.setCellValue(columnName); } // 遍历每一行并将选定列的内容填入表格 for (int i = 0; i < list.size(); ++i){ YourEntityClass item = list.get(i); Row row = sheet.createRow(i + 1); cellIndex = 0; for (String colName : columnNames) { Field field = YourEntityClass.class.getDeclaredField(colName.toLowerCase()); field.setAccessible(true); Object value = field.get(item); String strValue = Objects.toString(value, ""); Cell cell = row.createCell(cellIndex++); cell.setCellValue(strValue); } } wb.write(os); return AjaxResult.success().put("file", Base64.encodeBase64String(((ByteArrayOutputStream)os).toByteArray())); } finally { wb.dispose(); } } ``` 此代码片段展示了如何基于 Spring Boot 和 Apache POI 库构建一个 RESTful Web Service 来响应带有定制化列选择条件的导出请求。注意这里假设了 Java Bean 类的名字与实际数据库字段相匹配,并且所有字段都是公开可访问的或者是可以通过反射机制设置其可见性的私有成员变量。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值