封装PoiExcelUtils

作者简介:大家好,我是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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Openfire CVE-2023-32315(metasploit版)

step1&#xff1a;用docker搭建环境 Step2&#xff1a;docker查看映射端口 Step3&#xff1a;打开mysql和apache2&#xff0c;访问特定端口&#xff0c;然后靶标应用。 Step4&#xff1a;用metasploit进行攻击&#xff1a; 首先&#xff0c;打开metasploit&#xff0c;然…

Java (JDK 21) 调用 OpenCV (4.8.0)

Java 调用 OpenCV 一.OpenCV 下载和安装二.创建 Java Maven 项目三.其他测试 一.OpenCV 下载和安装 Open CV 官网 可以下载编译好的包&#xff0c;也可以下载源码自行编译 双击安装 opencv-4.8.0-windows.exe 默认为当前目录 安装即解压缩 根据系统位数选择 将 x64 目录下 op…

数据结构 | 查漏补缺之求叶子结点,分离链接法、最小生成树、DFS、BFS

求叶子结点的个数 参考博文&#xff1a; 树中的叶子结点的个数 计算方法_求树的叶子节点个数-CSDN博客 分离链接法 参考博文 数据结构和算法——哈希查找冲突处理方法&#xff08;开放地址法-线性探测、平方探测、双散列探测、再散列&#xff0c;分离链接法&#xff09;_线性…

Linux存储软件栈到底有多复杂,存储软件栈全景概览

从今天开始&#xff0c;我们将介绍一下Linux的存储软件栈&#xff0c;也称为IO栈。今天我们先从整体上了解一下Linux的整个IO栈&#xff0c;后续再深入介绍每个组件。我们很难说清楚Linux的存储软件栈到底有多复杂&#xff0c;但thomas-krenn的一张图可能可以给大家一个比较直观…

堆的相关时间复杂度计算(C语言)

目录 前言 建堆的时间复杂度 向上调整建堆的时间复杂度 向下调整建堆的时间复杂度 维护堆的时间复杂度 top K问题的时间复杂度 前言 在前面的三篇文章中我们成功的实现了堆排序的升降序以及基于堆的top K问题&#xff0c;现在我们来解决它们的时间复杂度问题 建堆的时间…

查看Linux的Ubuntu的版本

我的Ubuntu版本是 Jammy x86_64&#xff0c;即 Ubuntu 22.04.3 LTS&#xff0c;代号为"Jammy Jellyfish"&#xff0c;架构是 x86_64&#xff08;64位&#xff09;。

UEC++ 探索虚幻5笔记 day11

虚幻5.2.1探索 项目目录探索 C工程一定不能是中文路径&#xff0c;中文项目名&#xff0c;最好全部不要用中文&#xff0c;蓝图项目可以是中文浅浅创建一个空项目&#xff0c;讲解一下之前UE4没有讲解的项目目录文件的分布组成 .vs&#xff1a;文件夹一般是项目编译缓存文件夹…

linux安装mysql5.7(一遍过)

之前安装的时候遇到了很多问题&#xff0c;浪费了一些时间。整理出这份教程&#xff0c;照着做基本一遍过。 这是安装包: 链接&#xff1a;https://pan.baidu.com/s/1gBuQBjA4R5qRYZKPKN3uXw?pwd1nuz 1.下载安装包&#xff0c;上传到linux。我这里就放到downloads目录下面…

游戏盾的防御原理以及为什么程序类型更适合接入游戏盾。

游戏盾是一种专门用于游戏服务器的安全防护服务&#xff0c;旨在抵御各种网络攻击。它的原理主要包括以下几个方面&#xff1a; 流量清洗和过滤&#xff1a;游戏盾会对进入游戏服务器的流量进行实时监测、分析和过滤。它通过识别恶意流量和攻击流量&#xff0c;过滤掉其中的攻击…

多种混沌映射初始化种群MATLAB代码(开源代码)

五种混沌映射改进鲸鱼算法&#xff08;自行切换&#xff09;&#xff1a; clc clear close allSearchAgents_no30; % Number of search agents %种群数量Function_nameF1; % Name of the test function that can be from F1 to F23 (Table 1,2,3 in the paper) %使用方程的名…

【java设计模式】——代理设计模式,两种举例说明

代理设计模式 1.介绍 Spring 框架中AOP底层使用动态代理设计模式。通过学习动态代理设计模式可以很好的理解Spring框架AOP底层 代理模式&#xff08;Proxy&#xff09;是GoF23种设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象&#xff0c;而是通过调用代理&am…

浅谈https

1.网络传输的安全性 http 协议&#xff1a;不安全&#xff0c;未加密https 协议&#xff1a;安全&#xff0c;对请求报文和响应报文做加密 2.对称加密与非对称加密 2.1 对称加密 特点&#xff1a; 加解密使用 相同 秘钥 高效&#xff0c;适用于大量数据的加密场景 算法公开&a…

深入了解Java Duration类,对时间的精细操作

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概6000多字&#xff0c;预计阅读时间长需要5分钟。本篇文章的实战性、理论性较强&#xff0c;是一篇质量分数较高的技术干货文章&#x…

Linux安装mysq 8.0服务端和客户端(亲测保成功)

1. 查看当前是否有已经安装好的mysql,先卸载 # 命令 rpm -qa|grep -i mysql# 结果显示 mysql-community-libs-5.7.42-1.el7.x86_64 mysql-community-common-5.7.42-1.el7.x86_64 mysql-community-libs-compat-5.7.42-1.el7.x86_64 mysql57-community-release-el7-10.noarch my…

perl脚本获取Windows系统常用路径信息

windows系统常用的路径,比如临时目录、资源文件夹、字体保存目录、应用程序数据存放目录等等。在日常操作的时候寻找略有不便。这里用perl写一个脚本&#xff0c;并把这些目录信息格式化为json&#xff0c;方便查找。如下是perl代码&#xff1a; #! /usr/bin/perl use v5.14; …

中国科技巨头:集体变革中 /华为鸿蒙 4:功能、模型、生态三大升级 |魔法半周报

我有魔法✨为你劈开信息大海❗ 高效获取AIGC的热门事件&#x1f525;&#xff0c;更新AIGC的最新动态&#xff0c;生成相应的魔法简报&#xff0c;节省阅读时间&#x1f47b; &#x1f525;资讯预览 中国科技巨头&#xff1a;集体变革中 华为鸿蒙 4&#xff1a;功能、模型、生…

2002-2021年全国各地级市环境规制18个相关指标数据

2002-2021年全国各地级市环境规制18个相关指标数据 1、时间&#xff1a;2002-2021年 2、来源&#xff1a;城市年鉴 3、指标&#xff1a;行政区划代码、地区、年份、工业二氧化硫排放量(吨)、工业烟粉尘排放量(吨)、工业废水排放量(万吨)、工业废水排放达标量(万吨)、工业二氧…

华为OD机试 - 攀登者1(Java JS Python C)

题目描述 攀登者喜欢寻找各种地图,并且尝试攀登到最高的山峰。 地图表示为一维数组,数组的索引代表水平位置,数组的元素代表相对海拔高度。其中数组元素0代表地面。 例如:[0,1,2,4,3,1,0,0,1,2,3,1,2,1,0],代表如下图所示的地图,地图中有两个山脉位置分别为 1,2,3,4,5…

SpringSecurity6 | 修改默认用户

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 修改默认用户 📚个人知识库 :知识库,欢迎大家访问 学习参考…

一款带键扫的LED驱动电路

一、基本概述 TM1628A是一种带键盘扫描接口的LED&#xff08;发光二极管显示器&#xff09;驱动控制专用IC,内部集成有MCU数字接口、数据锁存器、LED 驱动、键盘扫描等电路。本产品质量可靠、稳定性好、抗干扰能力强。主要适用于家电设备(智能热水器、微波炉、洗衣机、空调、电…