封装PoiExcelUtils

news2024/11/26 0:41:29
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

为什么要封装PoiExcelUtils

前面两篇其实基本已经完成了需求,只不过存在两个问题:

  • Controller层代码太臃肿了
  • 复用性差

特别是复用性,几乎为0,如果TeacherController也有导出需求,同样的一坨代码还要再写一遍,所以我们有必要抽取通用代码,尝试封装一个PoiExcelUtils。

通常来说,封装工具类的前提是“出现重复代码了”。为了能观察到重复代码,我们可以比对导入Teacher和导入Student的代码。

假设学生表是这样的:

而老师表是这样的:

相比学生表,有两点变化:

  • 数据不再是顶着左上角,而是各自空了一格
  • 字段变了,字段数量也不同

如果把两份导入代码都写出来,大概是这样的:

有4处地方问题要解决:

  • rowIndex、cellIndex:数据从哪行哪列开始读,每份Excel可能不同
  • 如何抽取通用代码,自动为各种POJO设置值(难点一)
  • 抽取Excel文件输入流
  • 解决单元格和POJO字段的映射顺序(难点二)

封装导入代码

第一步:抽取rowIndex、cellIndex

前两个很简单,直接抽取到形参即可,大家自己做。大概类似这样:

public void importExcel(Integer rowIndex, Integer cellIndex) throws Exception;

第二步:反射设置字段值

也就是如何抽取通用代码,自动为各种POJO设置值。

但凡通用代码,必然是对重复代码/重复操作的抽取。如果我们要抽取一段通用的代码一个POJO的不同字段设置值,那么必然是通过一个循环语句(for循环或其他)。

这个结论怎么推导出来的?

因为Teacher可能5个字段,Student6个字段,Worker7个字段。如果要用通用代码为它们各自的字段赋值,那么必然是一个for循环,不然无法解决字段个数不同的问题。也就是5个字段的POJO循环5次,6个字段的POJO循环6次,以此类推。

但for循环本身对被迭代的对象有个隐性要求:每一次操作的对象都是相同/相似的。因为for循环的定义就是:重复N次相同操作。

也就是说,我们如果要写一个for循环对所有POJO的所有字段进行遍历,比如这样:

for (字段 : POJO){
    字段.set(getCellValue());
}

这要求所有POJO类型相同的,而且POJO内部的所有字段类型也是相同的。

于是我们得到了一个悖论:有差异的POJO们希望抽取通用代码对自己进行无差别迭代,而通用代码却要求迭代对象是无差异的。

我们分析一下为什么会出现这种悖论:

  • 首先,横向比较来看这几个POJO类型是不同的,Teacher、Student、Worker
  • 其次,纵向比较来看,同一个POJO的字段类型也是不同的,Integer age、String name

不同POJO、甚至同一个POJO的不同字段都无法用一个for搞定,似乎只能逐个手写:

Teacher字段1.set(getCellValue());
Teacher字段2.set(getCellValue());
Teacher字段3.set(getCellValue());

Student字段1.set(getCellValue());
Student字段2.set(getCellValue());
Student字段3.set(getCellValue());

Worker字段1.set(getCellValue());
Worker字段2.set(getCellValue());
Worker字段3.set(getCellValue());

那有没有可能找到POJO与POJO之间、字段与字段之间的共性内容,从而在更高的抽象层面上谋求统一呢?比如,我是中国人,他是日本人,我们不同国籍,不是同一个民族。但是如果站在更高的角度,我们都是地球人。

这给了我们启发,Teacher、Student、Worker向上抽象可以得到Class对象,而Integer age、String name向上抽象可以得到Field对象。

分析到这里,反射就呼之欲出了。

public void method(Class pojoClass) {
    Object pojo = pojoClass.newInstance();
 	Field[] fields = pojoClass.getDeclaredFields();
    for (field : fields) {
        field.set(pojo, getCellValue())
    }   
}

初步动手实践一下:

public class MyUtils {
    public static void main(String[] args) throws Exception {
        MyUtils myUtil = new MyUtils();
        // 假设我们要读取student_info.xlsx,由于标题和表头各占一行,所以RowStartIndex=2,而数据紧贴左边,所以cellStartIndex=0
        myUtil.importExcel(2, 0, Student.class);
    }


    /**
     * 第二步,抽取Class、Field
     *
     * @param RowStartIndex  从哪行开始读取(从0开始)
     * @param cellStartIndex 从那列开始读取(从0开始)
     * @param pojoClass      要操作的类型
     * @throws Exception
     */
    public void importExcel(Integer RowStartIndex, Integer cellStartIndex, Class pojoClass) throws Exception {
        // 获取工作薄
        XSSFWorkbook workbook = new XSSFWorkbook("/Users/bravo1988/Desktop/student_info.xlsx");
        // 获取工作表。一个工作薄中可能有多个工作表,比如sheet1 sheet2,可以根据下标,也可以根据sheet名称。这里根据下标即可。
        XSSFSheet sheet = workbook.getSheetAt(0);
        // 得到Pojo所有字段
        Field[] fields = pojoClass.getDeclaredFields();
        List<Object> excelDataList = new ArrayList<>();
        // 收集每一行数据,设置到Model中(跳过表头)
        for (int i = RowStartIndex; i <= sheet.getLastRowNum(); i++) {
            XSSFRow row = sheet.getRow(i);
            // 把单元格数据转为当前字段的类型,并设置
            Object pojo = pojoClass.newInstance();
            // 遍历单元格,为pojo字段赋值
            for (int j = cellStartIndex; j < row.getLastCellNum(); j++) {
                // 获取单元格的值
                XSSFCell cell = row.getCell(j);
                /**
                 * 这里假设单元格顺序和Pojo顺序一致,所以在这个for循环中,row.getCell(j)单元格对应fields[j - cellIndex]字段
                 * fields是从1开始的(id不设置),所以要j - cellIndex,这样单元格和Pojo字段才是匹配的。
                 */
                Field field = fields[j - cellStartIndex + 1];
                field.setAccessible(true);
                field.set(pojo, convertAttrType(field, cell));
            }

            excelDataList.add(pojo);
        }
        excelDataList.forEach(System.out::println);
    }

    /**
     * 类型转换 将 cell单元格数据类型 转为 Java类型
     * <p>
     * 这里其实分两步:
     * 1.通过getValue()方法得到cell对应的Java类型的字符串类型,比如Date,getValue返回的不是Date类型,而是Date的格式化字符串
     * 2.判断Pojo当前字段是什么类型,把getValue()得到的字符串往该类型转
     *
     * @param field
     * @param cell
     * @return
     * @throws Exception
     */
    private Object convertAttrType(Field field, Cell cell) throws Exception {
        Class<?> fieldType = field.getType();
        if (String.class.isAssignableFrom(fieldType)) {
            return getValue(cell);
        } else if (Date.class.isAssignableFrom(fieldType)) {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell));
        } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
            return Integer.parseInt(getValue(cell));
        } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
            return Double.parseDouble(getValue(cell));
        } else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
            return Boolean.valueOf(getValue(cell));
        } else if (BigDecimal.class.isAssignableFrom(fieldType)) {
            return new BigDecimal(getValue(cell));
        } else {
            return null;
        }
    }

    /**
     * 提供POI数据类型 --> Java数据类型的转换
     * 由于本方法返回值设为String,所以不管转换后是什么Java类型,都要以String格式返回
     * 所以Date会被格式化为yyyy-MM-dd HH:mm:ss
     * 后面根据需要自己另外转换
     *
     * @param cell
     * @return
     */
    private String getValue(Cell cell) {
        if (cell == null) {
            return "";
        }
        switch (cell.getCellType()) {
            case STRING:
                return cell.getRichStringCellValue().getString().trim();
            case NUMERIC:
                if (DateUtil.isCellDateFormatted(cell)) {
                    // DateUtil是POI内部提供的日期工具类,可以把原本是日期类型的NUMERIC转为Java的Data类型
                    Date javaDate = DateUtil.getJavaDate(cell.getNumericCellValue());
                    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(javaDate);
                } else {
                    // 无论Excel中是58还是58.0,数值类型在POI中最终都被解读为double。这里的解决办法是通过BigDecimal先把Double先转成字符串,如果是.0结尾,把.0去掉
                    String strCell = "";
                    Double num = cell.getNumericCellValue();
                    BigDecimal bd = new BigDecimal(num.toString());
                    if (bd != null) {
                        strCell = bd.toPlainString();
                    }
                    // 去除 浮点型 自动加的 .0
                    if (strCell.endsWith(".0")) {
                        strCell = strCell.substring(0, strCell.indexOf("."));
                    }
                    return strCell;
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            default:
                return "";
        }
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class Student {
        private Long id;
        private String name;
        private Integer age;
        private String address;
        private Date birthday;
        private Double height;
        private Boolean isMainlandChina;
    }
}

输出结果:

第三步:抽取Excel文件流

我们发现,从Student换成Teacher后,我们需要进到MyUtil代码中里把Excel文件路径改成teacher_info.xlsx,这不符合开闭原则。我们已经知道WorkBook可以接收InputStream,所以第三步就是抽取Excel文件流:

public class MyUtils {

    public static void main(String[] args) throws Exception {
        MyUtils myUtil = new MyUtils();
        FileInputStream fileInputStream = new FileInputStream(new File("/Users/bravo1988/Desktop/student_info.xlsx"));
        // 通过努力,我们再次把“变化的因素”提取出去了,现在MyUtils内部的代码越来越稳定了,稳定意味着通用...
        myUtil.importExcel(fileInputStream, 2, 0, Student.class);
    }

    /**
     * 第三步,抽取Excel文件流
     *
     * @param inputStream    要导入的Excel文件流
     * @param rowStartIndex  从哪行开始读取(从0开始)
     * @param cellStartIndex 从那列开始读取(从0开始)
     * @param pojoClass      操作的类型
     * @throws Exception
     */
    public void importExcel(InputStream inputStream, Integer rowStartIndex, Integer cellStartIndex, Class<?> pojoClass) throws Exception {
        // 【本次改变】通过输入流构造工作簿,由外界传入!!!
        XSSFWorkbook workbook = new XSSFWorkbook(inputStream);
        // 获取工作表。一个工作薄中可能有多个工作表,比如sheet1 sheet2,可以根据下标,也可以根据sheet名称。这里根据下标即可。
        XSSFSheet sheet = workbook.getSheetAt(0);
        // 省略...
    }

    // 省略其他方法...

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class Student {
        private Long id;
        private String name;
        private Integer age;
        private String address;
        private Date birthday;
        private Double height;
        private Boolean isMainlandChina;
    }
}

第四步:引入注解标注顺序

抽取到这一步,其实已经差不多了,至于POJO字段和单元格的映射顺序问题,个人觉得其实无所谓,就默认单元格顺序和字段顺序一致也未尝不可。但客户的Excel表是五花八门的,Student表的单元格可能是按顺序来的,而Teacher表的单元格则可能是反着来的。那么我们反射的代码就不再通用,如果不更改setter的顺序,整个赋值就颠倒了。

以面向对象的思维考虑一下,单元格的哪个值应该设置给哪个字段谁最清楚呢?

答案是,POJO自己。

比如Student的name字段,它要“记得”Excel中哪个单元格的值是自己的。你可以把Excel表中单元格的数据们想象成老婆组,POJO的字段们是老公组。到时候遍历单元格数据时,比如到了朱丽叶时,就喊一句:朱丽叶的老公是谁?然后POJO就把罗密欧送过去即可。此时我们需要一个标记,把POJO字段和单元格绑定起来。这很像JPA/通用Mapper的@Column注解,所以我们也模仿一下。

自定义注解ExcelAttribute:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAttribute {
    /**
     * 对应的列名称
     */
    String value() default "";

    /**
     * 列序号
     */
    int sort();

    // 字段类型对应的格式(大家自己可以试着扩展)
    String format() default "";
}
public class MyUtils {

    public static void main(String[] args) throws Exception {
        MyUtils myUtil = new MyUtils();
        FileInputStream fileInputStream = new FileInputStream(new File("/Users/bravo1988/Desktop/student_info.xlsx"));
        // 传入Excel文件流,从文件中读取数据并返回List
        List<Student> students = myUtil.importExcel(fileInputStream, 2, 0, Student.class);
        students.forEach(System.out::println);
    }

    /**
     * 第四步,引入注解绑定字段与单元格的映射关系
     *
     * @param inputStream    要导入的Excel文件流
     * @param rowStartIndex  从哪行开始读取(从0开始)
     * @param cellStartIndex 从那列开始读取(从0开始)
     * @param pojoClass      操作的Class
     * @param <T>            操作的类型
     * @return
     * @throws Exception
     */
    public <T> List<T> importExcel(InputStream inputStream, Integer rowStartIndex, Integer cellStartIndex, Class<T> pojoClass) throws Exception {
        // 获取工作薄
        XSSFWorkbook workbook = new XSSFWorkbook(inputStream);
        // 获取工作表。一个工作薄中可能有多个工作表,比如sheet1 sheet2,可以根据下标,也可以根据sheet名称。这里根据下标即可。
        XSSFSheet sheet = workbook.getSheetAt(0);

        // 得到Pojo所有字段
        Field[] fields = pojoClass.getDeclaredFields();

        List<T> excelDataList = new ArrayList<>();

        // 收集每一行数据,设置到Model中(跳过表头)
        for (int i = rowStartIndex; i <= sheet.getLastRowNum(); i++) {
            XSSFRow row = sheet.getRow(i);
            // 把单元格数据转为当前字段的类型,并设置
            T pojo = pojoClass.newInstance();
            // 遍历单元格(老婆组),为pojo字段赋值
            for (int j = cellStartIndex; j < row.getLastCellNum(); j++) {
                // 获取单元格的值
                XSSFCell cell = row.getCell(j);
                // 开始从老公组找出罗密欧
                for (Field field : fields) {
                    // 遍历,找到与当前单元格匹配的字段,取出单元格的值,把值设置给该字段
                    if (field.isAnnotationPresent(ExcelAttribute.class) && field.getAnnotation(ExcelAttribute.class).sort() == j) {
                        field.setAccessible(true);
                        // 把朱丽叶配给罗密欧
                        field.set(pojo, convertAttrType(field, cell));
                    }
                }

            }

            excelDataList.add(pojo);
        }

        return excelDataList;
    }

    /**
     * 类型转换 将 cell单元格数据类型 转为 Java类型
     * <p>
     * 这里其实分两步:
     * 1.通过getValue()方法得到cell对应的Java类型的字符串类型,比如Date,getValue返回的不是Date类型,而是Date的格式化字符串
     * 2.判断Pojo当前字段是什么类型,把getValue()得到的字符串往该类型转
     *
     * @param field
     * @param cell
     * @return
     * @throws Exception
     */
    private Object convertAttrType(Field field, Cell cell) throws Exception {
        Class<?> fieldType = field.getType();
        if (String.class.isAssignableFrom(fieldType)) {
            return getValue(cell);
        } else if (Date.class.isAssignableFrom(fieldType)) {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell));
        } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
            return Integer.parseInt(getValue(cell));
        } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
            return Double.parseDouble(getValue(cell));
        } else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
            return Boolean.valueOf(getValue(cell));
        } else if (BigDecimal.class.isAssignableFrom(fieldType)) {
            return new BigDecimal(getValue(cell));
        } else {
            return null;
        }
    }

    /**
     * 提供POI数据类型 --> Java数据类型的转换
     * 由于本方法返回值设为String,所以不管转换后是什么Java类型,都要以String格式返回
     * 所以Date会被格式化为yyyy-MM-dd HH:mm:ss
     * 后面根据需要自己另外转换
     *
     * @param cell
     * @return
     */
    private String getValue(Cell cell) {
        if (cell == null) {
            return "";
        }

        switch (cell.getCellType()) {
            case STRING:
                return cell.getRichStringCellValue().getString().trim();
            case NUMERIC:
                if (DateUtil.isCellDateFormatted(cell)) {
                    // DateUtil是POI内部提供的日期工具类,可以把原本是日期类型的NUMERIC转为Java的Data类型
                    Date javaDate = DateUtil.getJavaDate(cell.getNumericCellValue());
                    String dateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(javaDate);
                    return dateString;
                } else {
                    // 无论Excel中是58还是58.0,数值类型在POI中最终都被解读为Double。这里的解决办法是通过BigDecimal先把Double先转成字符串,如果是.0结尾,把.0去掉
                    String strCell = "";
                    Double num = cell.getNumericCellValue();
                    BigDecimal bd = new BigDecimal(num.toString());
                    if (bd != null) {
                        strCell = bd.toPlainString();
                    }
                    // 去除 浮点型 自动加的 .0
                    if (strCell.endsWith(".0")) {
                        strCell = strCell.substring(0, strCell.indexOf("."));
                    }
                    return strCell;
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            default:
                return "";
        }
    }


    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class Student {
        private Long id;
        @ExcelAttribute(sort = 0, value = "姓名")
        private String name;
        @ExcelAttribute(sort = 1, value = "年龄")
        private Integer age;
        @ExcelAttribute(sort = 2, value = "住址")
        private String address;
        @ExcelAttribute(sort = 3, value = "生日")
        private Date birthday;
        @ExcelAttribute(sort = 4, value = "身高")
        private Double height;
        @ExcelAttribute(sort = 5, value = "是否来自大陆")
        private Boolean isMainlandChina;
    }
}

上面的代码好在哪呢?虽然Excel字段顺序变动时仍然要改变注解的顺序,但代码却不用改了,同一套setter可以复用到所有的Excel上。

封装导出代码

有了封装导入代码的经验,封装导出代码就很简单了,这里直接贴代码:

public class MyUtils {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * @param dataList            要导出的数据
     * @param pojoClass           要操作的类型
     * @param templateInputStream Excel模板输入流
     * @param response            response响应,用来导出Excel文件
     * @param excelName           指定导出文件名
     * @param rowIndex            模板数据起始行
     * @param cellIndex           模板数据起始列
     * @throws IOException
     * @throws IllegalAccessException
     */
    public void exportExcel(List<T> dataList, Class<T> pojoClass, InputStream templateInputStream, HttpServletResponse response, 
                            String excelName, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {

        // 读取模板
        XSSFWorkbook workbook = new XSSFWorkbook(templateInputStream);
        // 获取模板sheet,默认第一张sheet
        XSSFSheet sheet = workbook.getSheetAt(0);
        // 从指定行收集单元格样式,方便复用,类似格式刷
        CellStyle[] templateStyles = getTemplateStyles(rowIndex, cellIndex, sheet);
        // 得到所有字段
        Field[] fields = pojoClass.getDeclaredFields();

        // 创建单元格,并设置样式和数据
        for (int i = 0; i < dataList.size(); i++) {
            Object pojo = dataList.get(i);
            XSSFRow row = sheet.createRow(i + rowIndex);
            // 为当前行创建单元格(创建老婆组)
            for (int k = cellIndex; k < templateStyles.length + cellIndex; k++) {
                // 当前新建了朱丽叶,已经就位
                XSSFCell cell = row.createCell(k);
                // 找到朱丽叶的化妆盒,给朱丽叶化妆
                cell.setCellStyle(templateStyles[k - cellIndex]);
                // 遍历字段(老公组),找到罗密欧
                for (Field field : fields) {
                    if (field.isAnnotationPresent(ExcelAttribute.class) && field.getAnnotation(ExcelAttribute.class).sort() == k - cellIndex) {
                        field.setAccessible(true);
                        // 把罗密欧给朱丽叶
                        mappingValue(field, cell, pojo);
                    }
                }

            }
        }

        // 通过response响应
        String fileName = new String(excelName.getBytes("UTF-8"), "ISO-8859-1");
        response.setContentType("application/octet-stream");
        response.setHeader("content-disposition", "attachment;filename=" + fileName);
        response.setHeader("filename", fileName);
        workbook.write(response.getOutputStream());
        workbook.close();
        logger.info("导出成功!");
    }

    /**
     * 根据字段类型强制转为字段数据,并设置给cell
     *
     * @param field
     * @param cell
     * @param pojo
     * @throws IllegalAccessException
     */
    private void mappingValue(Field field, Cell cell, Object pojo) throws IllegalAccessException {
        Class<?> fieldType = field.getType();
        if (Date.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Date) field.get(pojo));
        } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Integer) field.get(pojo));
        } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Double) field.get(pojo));
        } else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Boolean) field.get(pojo));
        } else if (BigDecimal.class.isAssignableFrom(fieldType)) {
            cell.setCellValue(((BigDecimal) field.get(pojo)).doubleValue());
        } else {
            cell.setCellValue((String) field.get(pojo));
        }
    }


    /**
     * 收集Excel模板的样式,方便对新建单元格复用,相当于打造一把格式刷
     *
     * @param rowIndex
     * @param cellIndex
     * @param sheet
     * @return
     */
    private CellStyle[] getTemplateStyles(Integer rowIndex, Integer cellIndex, XSSFSheet sheet) {
        XSSFRow dataTemplateRow = sheet.getRow(rowIndex);
        CellStyle[] cellStyles = new CellStyle[dataTemplateRow.getLastCellNum() - cellIndex];
        for (int i = 0; i < cellStyles.length; i++) {
            cellStyles[i] = dataTemplateRow.getCell(i + cellIndex).getCellStyle();
        }
        return cellStyles;
    }

}

有兴趣的话,可以放在上一篇的Controller中测试一下。

代码优化

上面对POI的封装只能说基本满足要求,但还有很大的优化空间:

  • 方法参数太多
  • 无论导入还是导出,都出现了for循环三层嵌套,效率很低

把公共参数提取到构造器中

来看一下导入导出两个方法中有哪些公共的参数:

public class MyUtils {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * @param dataList            要导出的数据
     * @param pojoClass           要操作的类型
     * @param templateInputStream Excel模板输入流
     * @param response            response响应,用来导出Excel文件
     * @param excelName           指定导出文件名
     * @param rowIndex            模板数据起始行
     * @param cellIndex           模板数据起始列
     * @throws IOException
     * @throws IllegalAccessException
     */
    public void exportExcel(List<T> dataList, Class<T> pojoClass, InputStream templateInputStream, HttpServletResponse response, 
                            String excelName, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {
    }
    
    /**
     * @param inputStream    要导入的Excel文件流
     * @param rowStartIndex  从哪行开始读取(从0开始)
     * @param cellStartIndex 从那列开始读取(从0开始)
     * @param pojoClass      操作的Class
     * @param <T>            操作的类型
     * @return
     * @throws Exception
     */
    public <T> List<T> importExcel(InputStream inputStream, Integer rowStartIndex, Integer cellStartIndex, Class<T> pojoClass) throws Exception {
    }

两个方法看起来相同的参数挺多的,但是含义有些不同。比如都是rowIndex,exportExcel()中指的是模板的样式位置,而importExcel()中指的是要读取的数据位置,所以从语义上不适合等同处理,所以最终我打算只抽取Class,也就是外部在new工具类时要指定Class。

用Map索引替代for遍历

Map代替for循环遍历算是一种很简单高效的方式,我们在《实用小算法》中已经讨论过,有兴趣的同学可以去看看,详细比较了几种小算法的优劣。

PoiExcelUtils完整代码

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAttribute {
    /**
     * 对应的列名称
     */
    String value() default "";

    /**
     * 列序号
     */
    int sort();

    // 字段类型对应的格式(大家自己可以试着扩展)
    String format() default "";
}
public class PoiExcelUtils<T> {
    /**
     * 与本次导出相关的POJO类型
     */
    private Class<T> clazz;

    /**
     * POJO字段的Map形式,key是字段上ExcelAttribute注解的sort值
     */
    private Map<Integer, Field> fieldMap = new HashMap<>();

    /**
     * 构造器,明确POJO的Class类型
     *
     * @param clazz
     */
    public PoiExcelUtils(Class<T> clazz) {
        this.clazz = clazz;
        // 得到所有字段,并用Map为字段建立索引
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(ExcelAttribute.class)) {
                fieldMap.put(field.getAnnotation(ExcelAttribute.class).sort(), field);
            }
        }
    }

    /**
     * 按模板导出到网络
     * 输入Excel模板+数据,导出Excel表格
     *
     * @param dataList  要导出的数据
     * @param excelName 指定本次导出的Excel表名
     * @param is        Excel模板,InputStream流
     * @param rowIndex  从模板哪一行开始拷贝样式
     * @param cellIndex 从模板哪一列开始拷贝样式
     * @param response  response响应
     * @throws IOException
     * @throws IllegalAccessException
     */
    public void exportExcelWithTemplate(List<T> dataList, String excelName, InputStream is, Integer rowIndex, Integer cellIndex, HttpServletResponse response) throws IOException, IllegalAccessException {
        // 把数据封装到Excel
        XSSFWorkbook workbook = mapData2ExcelWithTemplate(dataList, is, rowIndex, cellIndex);

        //===============response响应导出Excel=================
        String fileName = new String(excelName.getBytes("UTF-8"), "ISO-8859-1");
        response.setContentType("application/octet-stream");
        response.setHeader("content-disposition", "attachment;filename=" + fileName);
        response.setHeader("filename", fileName);
        workbook.write(response.getOutputStream());
        workbook.close();
    }

    /**
     * 按模板导出到本地
     * 输入Excel模板+数据+Excel保存路径,导出Excel表格
     *
     * @param dataList  要导出的数据
     * @param is        Excel模板,InputStream流
     * @param pathName  本地输出路径(比如/users/document/student_info.xlsx)
     * @param rowIndex  从模板哪一行开始拷贝样式
     * @param cellIndex 从模板哪一列开始拷贝样式
     * @throws IOException
     * @throws IllegalAccessException
     */
    public void exportExcelToLocalWithTemplate(List<T> dataList, InputStream is, String pathName, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {
        // 把数据封装到Excel
        XSSFWorkbook workbook = mapData2ExcelWithTemplate(dataList, is, rowIndex, cellIndex);

        //===============导出Excel到本地=================
        FileOutputStream fileOutputStream = new FileOutputStream(pathName);
        workbook.write(fileOutputStream);
    }

    /**
     * Excel导出到网络
     *
     * @param dataList  要导出的数据
     * @param excelName 指定本次导出的Excel表名
     * @param sheetName sheet名称
     * @param response  response响应
     * @throws IllegalAccessException
     * @throws IOException
     */
    public void exportExcel(List<T> dataList, String excelName, String sheetName, HttpServletResponse response) throws IllegalAccessException, IOException {
        XSSFWorkbook workbook = mapData2Excel(dataList, sheetName);

        //===============response响应导出Excel=================
        String fileName = new String(excelName.getBytes("UTF-8"), "ISO-8859-1");
        response.setContentType("application/octet-stream");
        response.setHeader("content-disposition", "attachment;filename=" + fileName);
        response.setHeader("filename", fileName);
        workbook.write(response.getOutputStream());
        workbook.close();
    }


    /**
     * Excel导出到本地
     *
     * @param dataList  要导出的数据
     * @param pathName  本地输出路径(比如/users/document/student_info.xlsx)
     * @param sheetName sheet名称
     * @throws IllegalAccessException
     * @throws IOException
     */
    public void exportExcelToLocal(List<T> dataList, String pathName, String sheetName) throws IllegalAccessException, IOException {
        XSSFWorkbook workbook = mapData2Excel(dataList, sheetName);

        //===============导出Excel到本地=================
        FileOutputStream fileOutputStream = new FileOutputStream(pathName);
        workbook.write(fileOutputStream);
    }

    /**
     * Excel导入
     *
     * @param inputStream 要导入的Excel表
     * @param rowIndex    从哪一行开始读取
     * @param cellIndex   从哪一列开始读取
     * @return
     * @throws Exception
     */
    public List<T> importExcel(InputStream inputStream, Integer rowIndex, Integer cellIndex) throws Exception {
        //================准备要导入的Excel=================
        XSSFWorkbook workbook = new XSSFWorkbook(inputStream);
        XSSFSheet sheet = workbook.getSheetAt(0);

        List<T> dataList = new ArrayList<>();

        //================从Excel读取数据,并设置给pojoList================
        for (int i = rowIndex; i <= sheet.getLastRowNum(); i++) {
            XSSFRow row = sheet.getRow(i);
            // 遍历单元格(老婆组)
            T pojo = (T) clazz.newInstance();
            for (int j = cellIndex; j < row.getLastCellNum(); j++) {
                // 获取单元格的值
                XSSFCell cell = row.getCell(j);
                // 当前是朱丽叶,已经就位了,罗密欧在哪?
                Object value = getValue(cell);
                // 根据索引快速从老公组找出罗密欧
                Field field = fieldMap.get(j - cellIndex);
                // 把朱丽叶配给罗密欧(把单元格数据赋值给字段)
                field.setAccessible(true);
                field.set(pojo, convertAttrType(field, cell));
            }

            dataList.add(pojo);
        }

        return dataList;
    }


    //======================== private =======================

    /**
     * 把查询得到的数据封装到Excel
     *
     * @param dataList
     * @return
     * @throws IllegalAccessException
     */
    private XSSFWorkbook mapData2Excel(List<T> dataList, String sheetName) throws IllegalAccessException {
        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet(sheetName);

        //=============创建表头===============
        XSSFRow row = sheet.createRow(0);
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            if (field.isAnnotationPresent(ExcelAttribute.class)) {
                row.createCell(i).setCellValue(field.getAnnotation(ExcelAttribute.class).value());
            }
        }

        //=========创建单元格,并设置数据(跳过表头)=======
        int size = fieldMap.keySet().size();
        for (int i = 0, length = dataList.size(); i < length; i++) {
            T pojo = dataList.get(i);
            row = sheet.createRow(i + 1);
            for (int k = 0; k < size; k++) {
                XSSFCell cell = row.createCell(k);
                Field field = fieldMap.get(k);
                field.setAccessible(true);
                mappingValue(field, cell, pojo);
            }
        }
        return workbook;
    }

    /**
     * 把查询得到的数据【按指定的模板样式】封装到Excel
     *
     * @param dataList
     * @param is
     * @param rowIndex
     * @param cellIndex
     * @return
     * @throws IOException
     * @throws IllegalAccessException
     */
    private XSSFWorkbook mapData2ExcelWithTemplate(List<T> dataList, InputStream is, Integer rowIndex, Integer cellIndex) throws IOException, IllegalAccessException {
        //================得到模板=================
        XSSFWorkbook workbook = new XSSFWorkbook(is);
        XSSFSheet sheet = workbook.getSheetAt(0);

        //=============从模板抽取样式===============
        CellStyle[] templateStyles = getTemplateStyles(rowIndex, cellIndex, sheet);

        //=========创建单元格,并设置样式和数据=======
        for (int i = 0; i < dataList.size(); i++) {
            T pojo = dataList.get(i);
            XSSFRow row = sheet.createRow(i + rowIndex);
            // 循环创建单元格(创建老婆组)
            for (int k = cellIndex; k < templateStyles.length + cellIndex; k++) {
                // 新建单元格:朱丽叶
                XSSFCell cell = row.createCell(k);
                // 为单元格设置样式:找到朱丽叶的化妆盒,给朱丽叶化妆
                cell.setCellStyle(templateStyles[k - cellIndex]);
                // 根据索引快速从老公组找出罗密欧
                Field field = fieldMap.get(k - cellIndex);
                // 把罗密欧给朱丽叶(把字段的数据赋值给单元格)
                field.setAccessible(true);
                mappingValue(field, cell, pojo);
            }
        }
        return workbook;
    }


    /**
     * 把字段Field的值设置给单元格Cell
     * Filed.get()得到的数据类型是Object,而cell.setCellValue()要求是具体的数据类
     * 因此需要判断字段类型,并把数据转换为原来的真是类型
     *
     * @param field
     * @param cell
     * @param pojo
     * @throws IllegalAccessException
     */
    private void mappingValue(Field field, Cell cell, Object pojo) throws IllegalAccessException {
        Class<?> fieldType = field.getType();
        if (Date.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Date) field.get(pojo));
        } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Integer) field.get(pojo));
        } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Double) field.get(pojo));
        } else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
            cell.setCellValue((Boolean) field.get(pojo));
        } else if (BigDecimal.class.isAssignableFrom(fieldType)) {
            cell.setCellValue(((BigDecimal) field.get(pojo)).doubleValue());
        } else {
            cell.setCellValue((String) field.get(pojo));
        }
    }


    /**
     * 收集Excel模板的样式,方便对新建单元格复用,相当于打造一把格式刷
     *
     * @param rowIndex
     * @param cellIndex
     * @param sheet
     * @return
     */
    private CellStyle[] getTemplateStyles(Integer rowIndex, Integer cellIndex, XSSFSheet sheet) {
        XSSFRow dataTemplateRow = sheet.getRow(rowIndex);
        CellStyle[] cellStyles = new CellStyle[dataTemplateRow.getLastCellNum() - cellIndex];
        for (int i = 0; i < cellStyles.length; i++) {
            cellStyles[i] = dataTemplateRow.getCell(i + cellIndex).getCellStyle();
        }
        return cellStyles;
    }

    /**
     * 类型转换 将 cell单元格数据类型 转为 Java类型
     * <p>
     * 这里其实分两步:
     * 1.通过getValue()方法得到cell对应的Java类型的字符串类型,比如Date,getValue返回的不是Date类型,而是Date的格式化字符串
     * 2.判断Pojo当前字段是什么类型,把getValue()得到的字符串往该类型转
     *
     * @param field
     * @param cell
     * @return
     * @throws Exception
     */
    private Object convertAttrType(Field field, Cell cell) throws Exception {
        Class<?> fieldType = field.getType();
        if (String.class.isAssignableFrom(fieldType)) {
            return getValue(cell);
        } else if (Date.class.isAssignableFrom(fieldType)) {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell));
        } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
            return Integer.parseInt(getValue(cell));
        } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
            return Double.parseDouble(getValue(cell));
        } else if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
            return Boolean.valueOf(getValue(cell));
        } else if (BigDecimal.class.isAssignableFrom(fieldType)) {
            return new BigDecimal(getValue(cell));
        } else {
            return null;
        }
    }

    /**
     * 提供POI数据类型 --> Java数据类型的转换
     * 由于本方法返回值设为String,所以不管转换后是什么Java类型,都要以String格式返回
     * 所以Date会被格式化为yyyy-MM-dd HH:mm:ss
     * 后面根据需要自己另外转换,详见{@link PoiExcelUtils#convertAttrType(Field, Cell)}
     *
     * @param cell
     * @return
     */
    private String getValue(Cell cell) {
        if (cell == null) {
            return "";
        }

        switch (cell.getCellType()) {
            case STRING:
                return cell.getRichStringCellValue().getString().trim();
            case NUMERIC:
                if (DateUtil.isCellDateFormatted(cell)) {
                    // DateUtil是POI内部提供的日期工具类,可以把原本是日期类型的NUMERIC转为Java的Data类型
                    Date javaDate = DateUtil.getJavaDate(cell.getNumericCellValue());
                    String dateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(javaDate);
                    return dateString;
                } else {
                    // 无论Excel中是58还是58.0,数值类型在POI中最终都被解读为Double。这里的解决办法是通过BigDecimal先把Double先转成字符串,如果是.0结尾,把.0去掉
                    String strCell = "";
                    Double num = cell.getNumericCellValue();
                    BigDecimal bd = new BigDecimal(num.toString());
                    if (bd != null) {
                        strCell = bd.toPlainString();
                    }
                    // 去除 浮点型 自动加的 .0
                    if (strCell.endsWith(".0")) {
                        strCell = strCell.substring(0, strCell.indexOf("."));
                    }
                    return strCell;
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            default:
                return "";
        }
    }

}

测试

@RestController
public class ExcelController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @GetMapping("/exportExcel")
    public void exportExcel(HttpServletResponse response, HttpServletRequest request) throws Exception {
        // 模拟从数据库查询数据
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student(1L, "周深(web导出)", 28, "贵州", new SimpleDateFormat("yyyy-MM-dd").parse("1992-9-29"), 161.0, true));
        studentList.add(new Student(2L, "李健(web导出)", 46, "哈尔滨", new SimpleDateFormat("yyyy-MM-dd").parse("1974-9-23"), 174.5, true));
        studentList.add(new Student(3L, "周星驰(web导出)", 58, "香港", new SimpleDateFormat("yyyy-MM-dd").parse("1962-6-22"), 174.0, false));

        // 导出数据
        PoiExcelUtils<Student> poiExcelUtils = new PoiExcelUtils<>(Student.class);
        FileInputStream excelTemplateInputStream = new FileInputStream(new File("/Users/bravo1988/Desktop/student_info.xlsx"));
        poiExcelUtils.exportExcelWithTemplate(studentList, "学生信息表.xlsx", excelTemplateInputStream, 2, 0, response);
        logger.info("导出成功!");
    }

    @PostMapping("/importExcel")
    public Map<String, Object> importExcel(MultipartFile file) throws Exception {
        PoiExcelUtils<Student> poiExcelUtils = new PoiExcelUtils<>(Student.class);
        List<Student> studentList = poiExcelUtils.importExcel(file.getInputStream(), 2, 0);

        saveToDB(studentList);
        logger.info("导入{}成功!", file.getOriginalFilename());

        // 这里用Map偷懒了,实际项目中可以封装Result实体类返回
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("data", studentList);
        result.put("msg", "success");
        return result;
    }

    private void saveToDB(List<Student> studentList) {
        if (CollectionUtils.isEmpty(studentList)) {
            return;
        }
        // 直接打印,模拟插入数据库
        studentList.forEach(System.out::println);
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Student {
        private Long id;
        @ExcelAttribute(sort = 0, value = "姓名")
        private String name;
        @ExcelAttribute(sort = 1, value = "年龄")
        private Integer age;
        @ExcelAttribute(sort = 2, value = "住址")
        private String address;
        @ExcelAttribute(sort = 3, value = "生日")
        private Date birthday;
        @ExcelAttribute(sort = 4, value = "身高")
        private Double height;
        @ExcelAttribute(sort = 5, value = "是否来自大陆")
        private Boolean isMainlandChina;
    }

}

总结

上面的工具类封装具有一定难度,尽量理解即可。

网络上还有一种封装方式,需要调用者自己在外部组装好各个字段对应的Map传入Util。但个人认为POI本身效率就不高,所以这点性能提升可有可无,用起来还麻烦。

至此,对POI的学习告一段落,请不要在生产环境使用PoiExcelUtils(可能发生OOM),推荐使用EasyExcel。

 

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1288529.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python实现FA萤火虫优化算法优化卷积神经网络回归模型(CNN回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 萤火虫算法&#xff08;Fire-fly algorithm&#xff0c;FA&#xff09;由剑桥大学Yang于2009年提出 , …

基于c++版本的数据结构改-python栈和队列思维总结

##栈部分-&#xff08;叠猫猫&#xff09; ##抽象数据类型栈的定义&#xff1a;是一种遵循先入后出的逻辑的线性数据结构。 换种方式去理解这种数据结构如果我们在一摞盘子中取到下面的盘子&#xff0c;我们首先要把最上面的盘子依次拿走&#xff0c;才可以继续拿下面的盘子&…

python+django企业物流配送管理系统a3t2w

本物流管理系统采用python技术&#xff0c;Mysql数据库开发&#xff0c;充分保证了系统稳定性、完整性。 物流管理系统的设计与实现的设计思想如下&#xff1a; 1、操作简单方便、系统界面安全良、简单明了的页面布局、方便查询物流管理系统相关信息。 2、即时可见&#xff1…

Ubuntu18.04 本地安装CVAT标注工具

写在前面&#xff1a; 1、如果直接clone最新版本的cvat&#xff0c;python版本最好安装3.8的&#xff0c;因为其中部分代码的语法只有高版本的python才可以支持。 2、安装完成以后本地登陆可能出现"cannot connect to cvat server"的错误&#xff0c;可以从Cannot …

全面解决Error: Uncaught SyntaxError: Invalid Unicode escape sequence

是因为.js文件中的路径转义&#xff08;\&#xff09;错误&#xff0c;可能是windows内的相对路径放到linux中有问题 直接看图&#xff1a; (上面是修改后的&#xff0c;下面的则是原来的) 解决方式&#xff1a; 先在报错浏览器按f12打开调试&#xff0c;选择console窗口查看…

c++函数模板STL详解

函数模板 函数模板语法 所谓函数模板&#xff0c;实际上是建立一个通用函数&#xff0c;其函数类型和形参类型不具体指定&#xff0c;用一个虚拟的类型来代表。这个通用函数就称为函数模板。 凡是函数体相同的函数都可以用这个模板来代替&#xff0c;不必定义多个函数&#xf…

pre标签展示代码块

pre样式 添加背景色、边框、以及调整了字体大小。 pre { border: 1px solid #999; page-break-inside: avoid; display: block; padding: 3px 3px 2px; margin: 0 0 10px; font-size: 13px; line-height: 20px; word-break: break-all; word-wrap: break-word; /* white-space:…

​HTML代码混淆技术:原理、应用和实现方法详解

​HTML代码混淆技术&#xff1a;原理、应用和实现方法详解 HTML代码混淆是一种常用的反爬虫技术&#xff0c;它可以有效地防止爬虫对网站数据的抓取。本文将详细介绍HTML代码混淆技术的原理、应用以及实现方法&#xff0c;帮助大家更好地了解和运用这一技术。 一、HTML代码混淆…

C++ 指针进阶

目录 一、字符指针 二、指针数组 三、数组指针 数组指针的定义 &数组名 与 数组名 数组指针的使用 四、数组参数 一维数组传参 二维数组传参 五、指针参数 一级指针传参 二级指针传参 六、函数指针 七、函数指针数组 八、指向函数指针数组的指针 九、回调函…

stm32项目(11)——基于stm32的俄罗斯方块游戏机

1.功能设计 使用stm32f103zet6平台&#xff0c;以及一块LCD屏幕&#xff0c;实现了一个俄罗斯方块游戏机。可以用按键调整方块的位置、还可以控制方块下降的速度&#xff01; 2.视频演示 俄罗斯方块 3.俄罗斯方块发展史 俄罗斯方块是一种经典的拼图游戏&#xff0c;由苏联俄罗…

隧道施工废水工艺设备需要哪些

隧道施工废水工艺设备是保障隧道施工过程中废水处理的关键装备。它们能够有效处理施工废水中的悬浮物、悬浮油、重金属等污染物&#xff0c;确保废水排放符合相关环保标准。以下是隧道施工废水工艺设备常见的几种类型&#xff1a; 1. 隧道施工废水沉淀池&#xff1a;沉淀池是废…

销售经理应该具备哪些能力?

销售经理应该具备哪些能力&#xff1f; 俗话说火车跑的快&#xff0c;全靠车头带&#xff0c;这句话虽然有些片面&#xff0c;但是也说明作为团队直接领导的销售经理担当者重要的角色&#xff0c;他们不仅要学会管理自我&#xff0c;更重要的是要管理团队&#xff0c;激发他人…

【开源】基于Vue和SpringBoot的衣物搭配系统

项目编号&#xff1a; S 016 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S016&#xff0c;文末获取源码。} 项目编号&#xff1a;S016&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 衣物档案模块2.2 衣物搭配模块2.3 衣…

数字孪生 LNG 终端,海上液化天然气三维可视化

液化天然气 (Liquefied Natural Gas&#xff0c;简称 LNG) 在能源转型过程中被广泛认可为相对较清洁的能源选择。相对于传统的煤炭和石油燃料&#xff0c;LNG 的燃烧过程产生的二氧化碳 (CO2) 排放较低。LNG 的燃烧释放的二氧化碳排放较少&#xff0c;因此对应对气候变化和减少…

深度学习在单线性回归方程中的应用--TensorFlow实战详解

深度学习在单线性回归方程中的应用–TensorFlow实战详解 文章目录 深度学习在单线性回归方程中的应用--TensorFlow实战详解1、人工智能<-->机器学习<-->深度学习2、线性回归方程3、TensorFlow实战解决单线性回归问题人工数据集生成构建模型训练模型定义损失函数定义…

制作木制纹理的黄鹤楼3D模型

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 黄鹤楼主楼为四边套八边形体、钢筋混凝土框架仿木结构&#xff0c;从…

react新旧生命周期钩子

以下的内容根据尚硅谷整理。 旧生命钩子 辅助理解&#xff1a; 红色框&#xff1a;挂载时生命钩子蓝色框&#xff1a;更新时生命钩子绿色框&#xff1a;卸载时生命钩子 挂载时 如图所示&#xff0c;我们可以看到&#xff0c;在组件第一次挂载时会经历&#xff1a; 构造器&a…

智能优化算法应用:基于堆优化算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于堆优化算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于堆优化算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.堆优化算法4.实验参数设定5.算法结果6.参考文献7.…

ABCDE类网络的划分及保留网段

根据IP地址的分类&#xff0c;IP地址被分为A、B、C、D和E五类。下面是对ABCDE类网络的划分及保留网段的详细描述&#xff1a; A类网络&#xff1a;范围从1.0.0.0到127.0.0.0&#xff0c;网络地址的最高位必须是“0”&#xff0c;可用的A类网络有127个&#xff0c;每个网络能容…

16、XSS——会话管理

文章目录 一、web会话管理概述1.1 会话管理1.2 为什么需要会话管理&#xff1f;1.3 常见的web应用会话管理的方式 二、会话管理方式2.1 基于server端的session的管理方式2.2 cookie-based的管理方式2.3 token-based的管理方式 三、安全问题 一、web会话管理概述 1.1 会话管理 …