java创建excel(不用设置样式,支持xlsx、xls格式, 不支持合并单元格)

Java操作Excel 阅读详情

推荐:
更加规范和完整的工具:
码云-chimm.excel
码云-chimm.excel:设置单元格公式 值不对Issuse

个人学习练手的工具:java 读取excel(支持xlsx、xls格式, 支持合并单元格)

实现效果
 exr.setMarkersByRow(title1, productList, "product");
// 按行替换marker
exr.setMarkersByRow(title2, products, "category"+i);
 
 // 公式计算
exr.computeFormula(title2, "SUM", false, rowIdx, colIdx, rowIdx-5, rowIdx-1, colIdx, colIdx);
// 设置品牌
exr.setMarkersByCol(title3, brandWrapList, "${category" + i + ".brand}");
// 设置描述
exr.setMarkersByCol(title3, descWrapList, "${category" + i + ".desc}");
实现思路
  • java中调整excel样式非常麻烦。参考freemarker,提供一个excel模板,打上标记,代码替换标记内容即可
  • 约定格式
    • 对于 ${*}模板, 将被替换为java中数据指定的内容,单个单元格单个内容替换。允许一个单元格内容存在多个${*}模板
    • 对于 ${*.*}模板 我们要求一行中 ${row.a} ${row.b} ${row.c} 属于同一个数据结构,不能有其他数据结构。下一组的${row.a} ${row.b} ${row.c} 将被写入excel下一行。一个单元格只允许存在一个${row.*}模板
基本概念
  • excel文件对应的是 workbook(xls xlsx)
  • 每个workbook 中有多个 sheet
  • 每个sheet有多行 Row
  • 每行有多列 Cell
  • sheet.getLastRowNum:获取sheet最后有数据行的下标(包含空行)。最后有数据的行是第n行则返回n-1,一行数据都没有则返回-1。行遍历,下标从0开始
  • row.getLastCellNum:获取某行最后有数据的列(包含空列)。只有第一列有数据则返回1,最后有数据的列是第n列则返回n,一列数据都没有则返回-1。列遍历,下标从0开始
  • sheet.getPhysicalNumberOfRows:获取sheet中实际有数据的行数(即,非默认格式的行数),一行数据都没有则返回0
  • row.getPhysicalNumberOfCells:获取某行实际有数据的列数(即,非默认格式的列数),一列数据都没有则返回0

注意事项
1、获取的excel行数不正确解决方法
2、poi认为的有数据:一般情况,非默认格式的行或列都是有数据的。特例,行高改变,被认为该行是有数据的;列宽改变,被认为该列是无数据列。

依赖
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>
对外提供的方法
  • replaceMarker: 实现${*} 格式的模板内容替换,一个单元格允许有多个${*} 。方法会有重载,允许替换所有sheet或指定sheet
  • setMarkersByRow:替换${*.*}格式marker,批量按行替换指定属性列单元格内容。方法会有重载,允许替换所有sheet或指定sheet
  • setMarkersByCol:替换${*.*}格式marker,批量按列替换指定属性行单元格内容。方法会有重载,允许替换所有sheet或指定sheet
  • renameSheetTitle:sheet标题重命名
  • getContentLocation:获取内容在sheet中的位置。按照从上到下,从左到右的顺序遍历,返回第一个该内容所在的位置
  • getContentAllLocation:获取内容在sheet中所有位置,按照从上到下,从左到右的顺序遍历
  • computeFormula:excel平均表达式或求和表达式计算
  • getBytes:获取byte[],把数据存入文件中,实现生成新的excel文件
  • coverWriteToLocal:保存excel文件到本地
  • downExcel:通过网页导出excel文件
  • close:关闭流
内部方法
  • 循环workbook
  • 循环sheet
  • 循环row
  • java对象转map
    …
代码实现

所有代码都在一个类,方便直接使用。
完整代码及excel模板:https://gitee.com/andus_top/java-excel

测试代码
public static void main(String[] args) throws IOException, IllegalAccessException {
        ExcelWriter exr = new ExcelWriter("C:/Users/Administrator/Desktop/template.xlsx");

        /*----------------${*}marker替换示例----------------*/
        // 替换所有sheet中${year}、${quarter}、${deptName}、${recordUser}
        HashMap<String, Object> baseInfoMap = new HashMap<>();
        baseInfoMap.put("year", 2021);
        baseInfoMap.put("quarter", 3);
        baseInfoMap.put("deptName", "产品库存管理部");
        baseInfoMap.put("recordUser", "搬砖工人");
        // 替换excel中所有sheet中的模板
        exr.replaceMarker(baseInfoMap);

        String title1 = "AAA";
        String title2 = "BBB";
        String title3 = "CCC";
        baseInfoMap.put("title1", title1);
        baseInfoMap.put("title2", title2);
        baseInfoMap.put("title3", title3);
        // 可每个属性单独替换
        exr.replaceMarker(baseInfoMap);

        // sheet重命名
        exr.renameSheetTitle("title1", title1);
        exr.renameSheetTitle("title2", title2);
        exr.renameSheetTitle("title3", title3);

        // 初始化参数。
        List<Product> productList = initData();

        /*----------------sheet1 模板简单使用----------------*/
        exr.setMarkersByRow(title1, productList, "product");


        /*----------------sheet2 marker替换(按行追加数据)与公式计算----------------*/
        /**
         * 申明:ExcelWriter.setMarkersByRow及其重载方法,不支持跨行合并单元格处理。见ExcelWriter.copyRowMerge方法fixme部分。
         * 所以,我们约定:
         * 1、约定每个category对应的品牌个数为5个,且模板中需要把5列创建好,可以多余5列,但是不能少于。
         * 2、每个category下对应的品牌个数不一致的话,公式计算时可能会有问题。
         * 3、合并单元格问题,可根据工具类ExcelWriter.copyRowMerge方法中fixme部分,再结合业务进行修改
         * 4、公式计算问题,求平均或者合计的行或列,可根据模板调整行或列的下移后者右移,见ExcelWriter.setRowMarkersByCol;ExcelWriter.setRowMarkersByRow方法中fixme部分
         */
        // 按大类分组。key:大类名称,value:对应集合,size=5
        Map<String, List<Product>> categoryMap = productList.stream().collect(Collectors.groupingBy(Product::getCategory));
        for (int i = 1; i <= categoryMap.keySet().size(); i++) {
            String category = String.valueOf(categoryMap.keySet().toArray()[i-1]);
            baseInfoMap.put("category"+i, category);
            exr.replaceMarker(baseInfoMap);

            List<Product> products = categoryMap.get(category);
            // 按行替换marker
            exr.setMarkersByRow(title2, products, "category"+i);
        }
        // 设置sheet中所有合计值。
        List<Integer[]> contentAllLocation = exr.getContentAllLocation(title2, "合计");
        for (Integer[] location : contentAllLocation) {
            int rowIdx = location[0];
            int colIdx = location[3]+1;
            exr.computeFormula(title2, "SUM", false, rowIdx, colIdx,
                    rowIdx-5, rowIdx-1, colIdx, colIdx);
        }

        /*----------------sheet3 marker替换(按列追加数据)----------------*/
        /**
         * 申明:ExcelWriter.setMarkersByCol及其重载方法,不支持跨行合并单元格处理。见ExcelWriter.copyRowMerge方法fixme部分。
         * 所以,我们约定:
         * 1、约定每个category对应的品牌个数为5个,且模板中需要把5列创建好,可以多余5列,但是不能少于。
         * 2、每个category下对应的品牌个数不一致的话,公式计算时可能会有问题。
         * 3、合并单元格问题,可根据工具类ExcelWriter.copyRowMerge方法中fixme部分,再结合业务进行修改
         * 4、公式计算问题,求平均或者合计的行或列,可根据模板调整行或列的下移后者右移,见ExcelWriter.setRowMarkersByCol;ExcelWriter.setRowMarkersByRow方法中fixme部分
         */
        for (int i = 1; i <= categoryMap.keySet().size(); i++) {
            String category = String.valueOf(categoryMap.keySet().toArray()[i-1]);

            // 外层List代表有几列数据,内层List代表当前列数据有几行值
            List<List<Object>> brandWrapList = new ArrayList<>();
            List<List<Object>> descWrapList = new ArrayList<>();
            List<Product> products = categoryMap.get(category);
            for (Product product : products) {
                brandWrapList.add(Arrays.asList(product.getBrand(), product.getBrand()));// 添加2个重复元素,体现是按列添加数据
                descWrapList.add(Arrays.asList(product.getDesc(), product.getDesc()));// 添加2个重复元素,体现是按列添加数据
            }
            // 设置品牌
            exr.setMarkersByCol(title3, brandWrapList, "${category" + i + ".brand}");
            // 设置描述
            exr.setMarkersByCol(title3, descWrapList, "${category" + i + ".desc}");
            // todo:设置平均分和合计
        }


        byte[] bt = exr.getBytes();
        exr.coverWriteToLocal("C:\\Users\\Administrator\\Desktop", "记录表.xlsx", bt);
        exr.close();
    }

    public static List<Product> initData(){
        return Arrays.asList(
                new Product(1, 10, "20200101", "电子产品类", "OPPO", "说明:拍照5分钟,充电两小时", 2000.22d, LocalDateTime.now()),
                new Product(2, 20, "20200101", "电子产品类", "VIVO", "说明:x60", 1111.22d, LocalDateTime.now()),
                new Product(3, 30, "20200101", "电子产品类", "小米", "说明:为发烧而生", 3333.22d, LocalDateTime.now()),
                new Product(4, 40, "20200101", "电子产品类", "华为", "说明:鸿蒙操作系统", 4444.22d, LocalDateTime.now()),
                new Product(5, 50, "20200101", "电子产品类", "苹果", "说明:一起都是产品问题", 5555.22d, LocalDateTime.now()),

                new Product(6, 60, "20200101", "家居类", "桌子", "说明:这是桌子", 17.22d, LocalDateTime.now()),
                new Product(7, 70, "20200101", "家居类", "椅子", "说明:这是椅子", 21.22d, LocalDateTime.now()),
                new Product(8, 80, "20200101", "家居类", "油烟机", "说明:这是油烟机", 34.22d, LocalDateTime.now()),
                new Product(9, 90, "20200101", "家居类", "电风扇", "说明:这是电风扇", 56.22d, LocalDateTime.now()),
                new Product(10, 100, "20200101", "家居类", "液晶电视", "说明:这是液晶电视", 32.22d, LocalDateTime.now())
        );
    }

class Product {
    public int id;
    public int count;
    public String addOrder; // 进货编号
    public String category;
    public String brand;
    public String desc;
    public double price;
    public LocalDateTime createTime;
    // get set 构造方法省略
}
ExcelWriter.java 工具类

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 创建excel <br>
* 实现思路:java中调整excel样式非常麻烦。参考freemarker,提供一个excel模板,打上标记,代码替换标记内容即可 <br>
* 约定格式: <br>
* 对于 ${*} marker, 将被替换为java中数据指定的内容,单个单元格单个内容替换。允许一个单元格内容存在多个${*} marker<br>
* 对于 ${*.*} marker  我们要求一行中 ${row.a} ${row.b} ${row.c} 属于同一个数据结构的属性,不能有其他数据结构。下一组的${row.a} ${row.b} ${row.c} 将被写入excel下一行。一个单元格只允许存在一个${row.*} marker <br>
* @Date 2021/6/25 13:01
* @author andus.top
*/
public class ExcelWriter {

   // excel对象
   private Workbook workbook;
   // 暂存将会替换的 ${*}模板的数据
   private Map<String, Object> tmpMaker = new HashMap<>();

   /**
    * 传入模板
    * @param in  文件内容流
    * @param xls true:表示是旧的excel(*.xls) ,false:表示是新版excel(*.xlsx)
    * @author andus.top
    * @date 2021/6/25 13:01
    * @return null
   **/
   public ExcelWriter(InputStream in, boolean xls) throws IOException {
       if(xls){
           workbook = new HSSFWorkbook(in);
       }else{
           workbook = new XSSFWorkbook(in);
       }
   }

   /**
    * 传入模板
    * @param template excel模板
    * @author andus.top
    * @date 2021/6/25 13:07
    * @return null
   **/
   public ExcelWriter(File template) throws IOException {
       String fileName = template.getName();
       try(FileInputStream in = new FileInputStream(template)){
           if(fileName.endsWith(".xls")){
               workbook = new HSSFWorkbook(in);
           }else if(fileName.endsWith(".xlsx")){
               workbook = new XSSFWorkbook(in);
           }
       }catch (Exception e){
           e.printStackTrace();
       }finally {

       }
       if(workbook == null){
           throw new RuntimeException("excel 文件格式错误");
       }
   }

   /**
    * 传入模板
    * @param templateFilePath 文件路径,包含文件后缀
    * @author andus.top
    * @date 2021/6/25 12:52
    * @return null
   **/
   public ExcelWriter(String templateFilePath) throws IOException {
       this(new File(templateFilePath));
   }

   /**
    * 通过sheet名称获取sheet对象
    * @param sheetTitle map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 23:31
    * @return Sheet
    **/
   public Sheet getSheet(String sheetTitle){
       return workbook.getSheet(sheetTitle);
   }

   /**
    * 通过sheet下标获取sheet对象
    * @param sheetIdx sheet下标,从0开始
    * @author andus.top
    * @date 2021/6/25 23:32
    * @return Sheet
    **/
   public Sheet getSheet(int sheetIdx){
       return workbook.getSheetAt(sheetIdx);
   }

   /**
    * 替换excel所有sheet中${*}模板为info中的值,没有则置空
    * @param info map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 13:18
    * @return ExcelWriter
   **/
   public ExcelWriter replaceMarker(Map<String, Object> info) {
       replaceWorkbookMarker(info);
       if(info != null)info.clear();
       return this;
   }

   /**
    * 替换excel所有sheet中${key}模板为value值,没有则置空
    * @param key  模板中的key
    * @param value  模板被替换的值
    * @author andus.top
    * @date 2021/6/25 13:28
    * @return ExcelWriter
   **/
   public ExcelWriter replaceMarker(String key, Object value) {
       tmpMaker.put(key, value);
       replaceWorkbookMarker(tmpMaker);
       tmpMaker.clear();
       return this;
   }

   /**
    * 替换excel指定sheet中${*}模板为info中的值,没有则置空
    * @param sheetIndex sheet 下标序号,从0开始
    * @param info map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 13:30
    * @return ExcelWriter
   **/
   public ExcelWriter replaceMarker(int sheetIndex, Map<String, Object> info) {
       if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
           return this;
       }
       replaceSheetMarker(workbook.getSheetAt(sheetIndex), info);
       if(info != null)info.clear();
       return this;
   }

   /**
    * 替换excel指定sheet中${key}模板为value值,没有则置空
    * @param sheetIndex sheet 下标序号,从0开始
    * @param key  模板中的key
    * @param value  模板被替换的值
    * @author andus.top
    * @date 2021/6/25 13:30
    * @return ExcelWriter
    **/
   public ExcelWriter replaceMarker(int sheetIndex, String key, Object value) {
       if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
           return this;
       }
       // 暂存 key-value
       tmpMaker.put(key, value);
       replaceSheetMarker(workbook.getSheetAt(sheetIndex), tmpMaker);
       tmpMaker.clear();
       return this;
   }

   /**
    * 替换excel指定sheet中${*}模板为info中的值,没有则置空
    * @param sheetTitle sheet名称
    * @param info map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 13:34
    * @return ExcelWriter
    **/
   public ExcelWriter replaceMarker(String sheetTitle, Map<String, Object> info) {
       replaceSheetMarker(workbook.getSheet(sheetTitle), info);
       if(info != null)info.clear();
       return this;
   }

   /**
    * 替换excel指定sheet中${key}模板为value值,没有则置空
    * @param sheetTitle sheet名称
    * @param key  模板中的key
    * @param value  模板被替换的值
    * @author andus.top
    * @date 2021/6/25 13:30
    * @return ExcelWriter
    **/
   public ExcelWriter replaceMarker(String sheetTitle, String key, Object value) {
       tmpMaker.put(key, value);
       replaceSheetMarker(workbook.getSheet(sheetTitle), tmpMaker);
       tmpMaker.clear();
       return this;
   }

   /**
    * 批量设置模板中${rowKey.x}的值,按行追加数据
    * @param infos   将excel中的${rowKey.*} 替换为infos中bean对象的属性值。如List&lt;User&gt;集合,一个user对应excel一行
    * @param rowKey   设置rowkey。如user.id,此时rowkey=user,id为User的属性
    * @author andus.top
    * @date 2021/6/25 13:09
    * @return ExcelWriter
   **/
   public ExcelWriter setMarkersByRow(List<?> infos, String rowKey) throws IllegalAccessException {
       setWorkbookMarkers(infos, rowKey);
       return this;
   }

   /**
    * 批量设置指定sheet中${rowKey.x}模板的值,按行追加数据<br>不会覆盖首列为“合计”的行
    * @param sheetIndex sheet下标(从0开始)
    * @param infos   将excel中的${rowKey.*} 设置为infos中的值。如List&lt;User&gt;集合,一个user对应excel一行
    * @param rowKey   设置rowkey。如user.id,此时rowkey=user,id为User的属性
    * @author andus.top
    * @date 2021/6/25 13:12
    * @return ExcelWriter
   **/
   public ExcelWriter setMarkersByRow(int sheetIndex, List<?> infos, String rowKey) throws IllegalAccessException {
       if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
           return this;
       }
       setSheetMarkersByRow(workbook.getSheetAt(sheetIndex), infos, rowKey);
       return this;
   }

    /**
    * 批量设置指定sheet中${rowKey.x}模板的值,按行追加数据<br>不会覆盖首列为“合计”的行
    * @param sheetTitle sheet下标(从0开始)
    * @param infos   将excel中的${rowKey.*} 设置为infos中的值。如List&lt;User&gt;集合,一个user对应excel一行
    * @param rowKey   设置rowkey。如user.id,此时rowkey=user,id为User的属性
    * @author andus.top
    * @date 2021/6/25 13:16
    * @return ExcelWriter
   **/
   public ExcelWriter setMarkersByRow(String sheetTitle, List<?> infos, String rowKey) throws IllegalAccessException {
       setSheetMarkersByRow(workbook.getSheet(sheetTitle), infos, rowKey);
       return this;
   }

   /**
    * excel所有sheet中,从${}模板的位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param infos    bean对象的集合。外层List代表有几列数据,内层List代表当前列数据有几行值
    * @param marker  设置marker模板。假设${}模板在sheet中[row=0,col=1]的位置。将会从[0,1]单元格为起始位置,按列添加数据。
    * @author andus.top
    * @date 2021/6/25 13:55
    * @return ExcelWriter
   **/
   public ExcelWriter setMarkersByCol(List<List<Object>> infos, String marker) throws IllegalAccessException {
       for (int x = 0, sw = workbook.getNumberOfSheets(); x < sw; x++) {
           setSheetMarkersByCol(workbook.getSheetAt(x), infos, marker);
       }
       return this;
   }

   /**
    * excel所有sheet中,从startWriteLocation位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param infos   List对象。外层List代表有几列数据,内层List代表当前列数据有几行值
    * @param startWriteLocation 单元格位置。数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    * @author andus.top
    * @date 2021/6/25 14:12
    * @return ExcelWriter
    **/
   public ExcelWriter setMarkersByCol(List<List<Object>> infos, Integer[] startWriteLocation) throws IllegalAccessException {
       for (int x = 0, sw = workbook.getNumberOfSheets(); x < sw; x++) {
           setRowMarkersByCol(workbook.getSheetAt(x), infos, startWriteLocation);
       }
       return this;
   }

   /**
    * 指定sheet中,从${}模板的位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param sheetTitle  sheet名称
    * @param infos   List对象。外层List代表有几列数据,内层List代表当前列数据有几行值
    * @param marker 设置marker模板。假设${}模板在sheet中[row=0,col=1]的位置。将会从[0,1]单元格为起始位置,按列添加数据。
    * @author andus.top
    * @date 2021/6/25 14:20
    * @return ExcelWriter
    **/
   public ExcelWriter setMarkersByCol(String sheetTitle, List<List<Object>> infos, String marker) throws IllegalAccessException {
       if (StringUtils.isEmpty(sheetTitle)) {
           return this;
       }
       setSheetMarkersByCol(workbook.getSheet(sheetTitle), infos, marker);
       return this;
   }

   /**
    * 指定sheet中,从startWriteLocation位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param sheetTitle  sheet名称
    * @param infos   List对象。外层List代表有几列数据,内层List代表当前列数据有几行值
    * @param startWriteLocation 单元格位置。数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    * @author andus.top
    * @date 2021/6/25 14:23
    * @return ExcelWriter
    **/
   public ExcelWriter setMarkersByCol(String sheetTitle, List<List<Object>> infos, Integer[] startWriteLocation) throws IllegalAccessException {
       if (StringUtils.isEmpty(sheetTitle)) {
           return this;
       }
       setRowMarkersByCol(workbook.getSheet(sheetTitle), infos, startWriteLocation);
       return this;
   }

   /**
    * 指定sheet中,从${}模板的位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param sheetIndex  sheet下标,从0开始
    * @param infos   List对象。外层List代表有几列数据,内层List代表当前列数据有几行值
    * @param marker 设置marker模板。假设${}模板在sheet中[row=0,col=1]的位置。将会从[0,1]单元格为起始位置,按列添加数据。
    * @author andus.top
    * @date 2021/6/25 14:24
    * @return ExcelWriter
    **/
   public ExcelWriter setMarkersByCol(int sheetIndex, List<List<Object>> infos, String marker) throws IllegalAccessException {
       if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
           return this;
       }
       setSheetMarkersByCol(workbook.getSheetAt(sheetIndex), infos, marker);
       return this;
   }

   /**
    * 指定sheet中,从startWriteLocation位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param sheetIndex  sheet名称
    * @param infos   List对象。外层List代表有几列数据,内层List代表当前列数据有几行值
    * @param startWriteLocation 单元格位置。数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    * @author andus.top
    * @date 2021/6/25 14:23
    * @return ExcelWriter
    **/
   public ExcelWriter setMarkersByCol(int sheetIndex, List<List<Object>> infos, Integer[] startWriteLocation) throws IllegalAccessException {
       if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
           return this;
       }
       setRowMarkersByCol(workbook.getSheetAt(sheetIndex), infos, startWriteLocation);
       return this;
   }

   /**
    * 根据内容获取该内容所在的位置(返回第一内容所在的位置)<br>从第0行到0列到最后一行最后一列,依次遍历
    * @param sheetTitle sheet名称
    * @param tar 要查找的数据
    * @author andus.top
    * @date 2021/6/25 15:16
    * @return Integer[]<br> 1、返回null,表示该数据不存在
    *                  <br> 2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
   **/
   public Integer[] getContentLocation(String sheetTitle, String tar){
       return getContentLocation(workbook.getSheet(sheetTitle), tar);
   }

   /**
    * 根据内容获取该内容所在的位置(返回第一内容所在的位置)<br>从第0行到0列到最后一行最后一列,依次遍历
    * @param sheetIdx sheet下标从0开始
    * @param tar 要查找的数据
    * @author andus.top
    * @date 2021/6/25 15:17
    * @return Integer[]<br> 1、返回null,表示该数据不存在
    *                  <br> 2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
   **/
   public Integer[] getContentLocation(int sheetIdx, String tar){
       return getContentLocation(workbook.getSheetAt(sheetIdx), tar);
   }

   /**
    * 根据内容获取该内容 在该sheet中所有的位置<br>从第0行到0列到最后一行最后一列,依次遍历
    * @param sheetTitle sheet名称
    * @param tar 要查找的数据
    * @author andus.top
    * @date 2021/6/25 15:18
    * @return java.util.List&lt;java.lang.Integer[]&gt; <br> 1、返回null,表示该数据不存在
    *         <br>2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
   **/
   public List<Integer[]> getContentAllLocation(String sheetTitle, String tar){
       return getContentAllLocation(workbook.getSheet(sheetTitle), tar);
   }

   /**
    * 根据内容获取该内容 在该sheet中所有的位置<br>从第0行到0列到最后一行最后一列,依次遍历
    * @param sheetIdx sheet下标从0开始
    * @param tar 要查找的数据
    * @author andus.top
    * @date 2021/6/25 15:19
    * @return java.util.List&lt;java.lang.Integer[]&gt;<br> 1、返回null,表示该数据不存在
    *         <br> 2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
   **/
   public List<Integer[]> getContentAllLocation(int sheetIdx, String tar){
       return getContentAllLocation(workbook.getSheetAt(sheetIdx), tar);
   }

   /**
    * 单元格表达式,按列计算
    * @param sheetTitle sheet名称
    * @param formulaFlag 表达式。SUM/AVERAGE
    * @param isPoint true,结果保留两位小数,false,结果保留0为小数
    * @param startWriteLocation 存放结果单元格位置。数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    * @param rowStarIdx 数据开始行下标,从0开始
    * @param rowEndIdx 数据结束行下标,从0开始
    * @param colStartIdx 数据开始列下标,从0开始
    * @param colEndIdx 数据结束列下标,从0开始
    * @author andus.top
    * @date 2021/6/25 15:52
    * @return
    **/
   public void computeFormula(String sheetTitle, String formulaFlag, Boolean isPoint, Integer[] startWriteLocation, int rowStarIdx, int rowEndIdx, int colStartIdx, int colEndIdx){
       Sheet sheet = workbook.getSheet(sheetTitle);
       computeFormula(sheet, formulaFlag, isPoint, startWriteLocation, rowStarIdx, rowEndIdx, colStartIdx, colEndIdx);
   }

   /**
    * 单元格表达式,按列计算
    * @param sheet sheet
    * @param formulaFlag 表达式。SUM/AVERAGE
    * @param isPoint true,结果保留两位小数,false,结果保留0为小数
    * @param startWriteLocation 存放结果单元格位置。数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    * @param rowStarIdx 数据开始行下标,从0开始
    * @param rowEndIdx 数据结束行下标,从0开始
    * @param colStartIdx 数据开始列下标,从0开始
    * @param colEndIdx 数据结束列下标,从0开始
    * @author andus.top
    * @date 2021/6/25 16:00
    * @return
    **/
   public void computeFormula(Sheet sheet, String formulaFlag, Boolean isPoint, Integer[] startWriteLocation, int rowStarIdx, int rowEndIdx, int colStartIdx, int colEndIdx){
       int resRowIdx = startWriteLocation[0]; // 合并单元格取首行
       int resColIdx = startWriteLocation[2]; // 合并单元格取首列
       if(sheet != null) {
           Cell cell = sheet.getRow(resRowIdx).getCell(resColIdx);
           computeFormula(sheet, formulaFlag, isPoint, cell, rowStarIdx, rowEndIdx, colStartIdx, colEndIdx);
       }
   }

   /**
    * 单元格表达式,按列计算
    * @param sheetTitle sheet名称
    * @param formulaFlag 表达式。SUM/AVERAGE
    * @param isPoint true,结果保留两位小数,false,结果保留0为小数
    * @param resRowIdx 存放计算结果cell 行下标
    * @param resColIdx 存放计算结果cell 列下标
    * @param rowStarIdx 数据开始行下标,从0开始
    * @param rowEndIdx 数据结束行下标,从0开始
    * @param colStartIdx 数据开始列下标,从0开始
    * @param colEndIdx 数据结束列下标,从0开始
    * @author andus.top
    * @date 2021/6/25 15:52
    * @return
    **/
   public void computeFormula(String sheetTitle, String formulaFlag, Boolean isPoint, int resRowIdx, int resColIdx, int rowStarIdx, int rowEndIdx, int colStartIdx, int colEndIdx){
       Sheet sheet = workbook.getSheet(sheetTitle);
       if(sheet != null) {
           Cell cell = sheet.getRow(resRowIdx).getCell(resColIdx);
           computeFormula(sheet, formulaFlag, isPoint, cell, rowStarIdx, rowEndIdx, colStartIdx, colEndIdx);
       }
   }

   /**
    * 单元格表达式,按列计算
    * @param sheetTitle sheet名称
    * @param formulaFlag 表达式。SUM/AVERAGE
    * @param isPoint true,结果保留两位小数,false,结果保留0为小数
    * @param cell 存放计算结果的cell
    * @param rowStarIdx 数据开始行下标,从0开始
    * @param rowEndIdx 数据结束行下标,从0开始
    * @param colStartIdx 数据开始列下标,从0开始
    * @param colEndIdx 数据结束列下标,从0开始
    * @author andus.top
    * @date 2021/6/25 15:51
    * @return
    **/
   public void computeFormula(String sheetTitle, String formulaFlag, Boolean isPoint, Cell cell, int rowStarIdx, int rowEndIdx, int colStartIdx, int colEndIdx){
       Sheet sheet = workbook.getSheet(sheetTitle);
       computeFormula(sheet, formulaFlag, isPoint, cell, rowStarIdx, rowEndIdx, colStartIdx, colEndIdx);
   }

   /**
    * 单元格表达式,按列计算
    * @param sheet sheet
    * @param formulaFlag 表达式。SUM/AVERAGE
    * @param isPoint true,结果保留两位小数,false,结果保留0为小数
    * @param cell 存放计算结果的cell
    * @param rowStartIdx 数据开始行下标,从0开始
    * @param rowEndIdx 数据结束行下标,从0开始
    * @param colStartIdx 数据开始列下标,从0开始
    * @param colEndIdx 数据结束列下标,从0开始
    * @author andus.top
    * @date 2021/6/25 15:38
    * @return
   **/
   public void computeFormula(Sheet sheet, String formulaFlag, Boolean isPoint, Cell cell, int rowStartIdx, int rowEndIdx, int colStartIdx, int colEndIdx){
       if(sheet != null && cell != null) {
           if(isPoint){
               // 结果保留两位小数,并且内容居中显示
               setDoubleCell(cell);
           }else{
               // 结果保留0为小数,并且内容居中显示
               setNumberCell(cell);
           }
           String colTag = CellReference.convertNumToColString(colStartIdx);
           String colTag2 = CellReference.convertNumToColString(colEndIdx);
           //sum/avg函数
           String formula = formulaFlag + "(" + colTag + (rowStartIdx+1) + ":" + colTag2 + (rowEndIdx+1) + ")"; // SUM(C1:C9)   AVERAGE(C4:C8)
           cell.setCellFormula(formula);
           // 进行强制公式计算
           sheet.setForceFormulaRecalculation(true);// 重新计算公式的意思
       }
   }

   /**
    * 在指定的行插入一行,样式来源于sourceRow行
    * @param sheet 某个sheet页
    * @param sourceRow 具备样式的行
    * @param tarRowNum 拷贝到目标行的下标
    * @author andus.top
    * @date 2021/6/25 15:22
    * @return org.apache.poi.ss.usermodel.Row
    **/
   public static Row insertRow(Sheet sheet, Row sourceRow, int tarRowNum){
       int lastRow = sheet.getLastRowNum(); // n行返回n-1
       //  插入行
       if(tarRowNum <= lastRow){
           // 先把tarRowNum到lastRow向下移动n行,在移动后的tarRowNum处(移动后留的空白),新建一个新行,这样就实现了数据行的插入

           // 从下标为tarRowNum的行开始移动
           // 到下标为lastRow的行结束移动
           // 向下移动1行
           // true: 要复制  row height 样式
           // false: 是否将原始行的高度设置为默认值
           sheet.shiftRows(tarRowNum, lastRow, 1, true, false); // 向excel插入行,并复制原有样式
       }
       // 在tarRowNum 下标处创建行
       Row newRow = sheet.createRow(tarRowNum);
       copyRowStyle(sourceRow, newRow);
       return newRow;
   }

   /**
    * 复制整个sheet
    * @param sheetName source sheet 名称
    * @param targetName target sheet 名称
    * @author andus.top
    * @date 2021/6/25 16:28
    * @return ExcelWriter
   **/
   public ExcelWriter copySheet(String sheetName, String targetName) {
       int index = workbook.getSheetIndex(sheetName);
       if (index >= 0) {
           Sheet sh = workbook.cloneSheet(index);
           workbook.setSheetName(workbook.getSheetIndex(sh), targetName);
       }
       return this;
   }

   /**
    * 删除sheet
    * @param sheetName sheet 名称
    * @author andus.top
    * @date 2021/6/25 16:28
    * @return ExcelWriter
   **/
   public ExcelWriter removeSheet(String sheetName ) {
       workbook.removeSheetAt(workbook.getSheetIndex(sheetName));
       return this;
   }

   /**
    * 更换sheet名称
    * @param oldName 原sheet名称
    * @param newName 新sheet名称
    * @author andus.top
    * @date 2021/6/18 19:01
    * @return void
   **/
   public void renameSheetTitle(String oldName, String newName){
       workbook.setSheetName(workbook.getSheetIndex(oldName), newName);
   }

   /**
    * 获取workbook的字节数组,方便转为excel文件
    * @param
    * @author andus.top
    * @date 2021/6/25 16:29
    * @return byte[]
   **/
   public byte[] getBytes() {
       byte[] bs = null;
       try(ByteArrayOutputStream baos = new ByteArrayOutputStream();){
           workbook.write(baos);
           bs = baos.toByteArray();
           baos.flush();
       }catch (Exception e){
           throw new RuntimeException("getBytes occour error");
       }finally {
           try{
               workbook.close();
           }catch (Exception e){
               throw new RuntimeException(e);
           }
       }
       return bs;
   }

   /**
    * 根据byte数组,生成文件到本地
    * @param filePath  文件路径
    * @param fileName  文件名称(需要带后缀,如*.jpg、*.java、*.xml)
    * @param bs 字节数组
    * @author andus.top
    * @date 2021/6/25 16:30
    * @return void
   **/
   public void coverWriteToLocal(String filePath, String fileName, byte[] bs) {
       BufferedOutputStream bos = null;
       FileOutputStream fos = null;
       File file = null;
       try {
           File dir = new File(filePath);
           if(!dir.exists() && !dir.isDirectory()){//判断文件目录是否存在
               dir.mkdirs();
           }
           file = new File(filePath + File.separator + fileName);
           fos = new FileOutputStream(file);
           bos = new BufferedOutputStream(fos);
           bos.write(bs);
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           if (bos != null) {
               try {
                   bos.close();
               } catch (IOException e1) {
                   e1.printStackTrace();
               }
           }
           if (fos != null) {
               try {
                   fos.close();
               } catch (IOException e1) {
                   e1.printStackTrace();
               }
           }
       }
   }

   /**
    * 根据byte数组,网页下载文件
    * @param response  文件路径
    * @param fileName  文件名称(需要带后缀,如*.jpg、*.java、*.xml)
    * @param bytes  字节数组
    * @author andus.top
    * @date 2021/6/25 16:31
    * @return void
    **/
   public void downExcel(HttpServletResponse response, String fileName, byte[] bytes){
       ByteArrayInputStream in = new ByteArrayInputStream(bytes);
       try {
           response.setContentType("multipart/form-data");
           response.setHeader("content-disposition", "attachement;filename=" + new String(fileName.getBytes("utf-8"), "ISO-8859-1"));

           int b = 0;
           byte[] buffer = new byte[512];
           while (b != -1) {
               b = in.read(buffer);
               if (b != -1) {
                   response.getOutputStream().write(buffer, 0, b);
               }
           }
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           try {
               if (in != null) {
                   in.close();
               }
               response.getOutputStream().flush();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }

   /**
    * 关闭流
    * @param
    * @author andus.top
    * @date 2021/6/25 16:32
    * @return
   **/
   public void close(){
       try {
           if(workbook != null){
               workbook.close();
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
   }

   /**
    * 遍历每个sheet,替换excel所有sheet中${*}模板为info中的值,没有则置空
    * @param info map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 16:58
    * @return ExcelWriter
    **/
   private void replaceWorkbookMarker(Map<String, Object> info) {
       for (int x = 0, sw = workbook.getNumberOfSheets(); x < sw; x++) {
           replaceSheetMarker(workbook.getSheetAt(x), info);
       }
   }

   /**
    * 逐行遍历,替换excel所有sheet中${*}模板为info中的值,没有则置空
    * @param sheet
    * @param sheetInfo map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 16:59
    * @return ExcelWriter
    **/
   private void replaceSheetMarker(Sheet sheet, Map<String, Object> sheetInfo) {
       if (sheet == null) {
           return;
       }
       for (int x = sheet.getLastRowNum(); x >= 0; x--) { // sheet.getLastRowNum(); // n行返回n-1
           replaceMarkerByRow(sheet.getRow(x), sheetInfo);
       }
   }

   /**
    * 替换一行中${*}模板为info中的值,没有则置空
    * @param row
    * @param info map集合。<br>如key=unitName,value=CBA公司,所有sheet中${unitName}将会被替换为'CBA公司'
    * @author andus.top
    * @date 2021/6/25 17:06
    * @return ExcelWriter
    **/
   private void replaceMarkerByRow(Row row, Map<String, Object> info) {
       if (row == null) {
           return;
       }
       for (int x = 0, lc = row.getLastCellNum(); x < lc; x++) { // row.getLastCellNum()  n列,返回n
           Cell cell = row.getCell(x);
           if (cell != null) {
               // 某行某列的内容
               String content = String.valueOf(getCellContent(row, cell));
               // 核验是否是模板。我们约定格式: ${*} ,${*.*}是模板
               List<String[]> markerList = checkMarker(content);
               if (markerList != null && !markerList.isEmpty()) {
//                    if(markerList.size() == 1) {// 单元格只有一个marker,可直接覆盖
//                        String listKey = markerList.get(0)[0];
//                        String mapKey = markerList.get(0)[1];
//                        if (listKey == null && mapKey != null && info.get(mapKey) != null) {
//                            String value = String.valueOf(info.get(mapKey));
//                            cell.setCellValue(value);
//                        }
//                    }
                   // 单元格包含多个marker,使用String[]中第三个值采用替换方式
                   String cellContent = String.valueOf(getCellContent(row, cell));
                   for (String[] markerInfos : markerList) {
                       String listKey = markerInfos[0];
                       String mapKey = markerInfos[1];
                       String marker = markerInfos[2];
                       if (listKey == null && mapKey != null && info.get(mapKey) != null) {
                           String value = String.valueOf(info.get(mapKey));
                           cellContent = cellContent.replace(marker, value);
                       }
                   }
                   try {
                       Integer integer = Integer.valueOf(cellContent);
                       // 设置单元格为数字类型,并且内容居中显示
                       setNumberCell(cell);
                       cell.setCellValue(integer);
                   }catch (Exception ex){
                       cell.setCellValue(cellContent);
                   }
               }
           }
       }
   }

   /**
    * 遍历每个sheet,将${rowkey.x}替换为 infos中bean对象的属性值<br>不会覆盖首列为“合计”的行
    * @param infos bean对象的集合
    * @param rowKey 设置rowkey。如user.id,此时rowkey=user
    * @author andus.top
    * @date 2021/6/25 17:07
    * @return void
   **/
   private void setWorkbookMarkers(List<?> infos, String rowKey) throws IllegalAccessException {
       for (int x = 0, sw = workbook.getNumberOfSheets(); x < sw; x++) {
           setSheetMarkersByRow(workbook.getSheetAt(x), infos, rowKey);
       }
   }


   /**
    * 将sheet中${rowkey.x}替换为 infos中bean对象的属性值<br>不会覆盖首列为“合计”的行
    * @param sheet 某个sheet页
    * @param infos bean对象的集合
    * @param rowKey 设置rowkey。如user.id,此时rowkey=user
    * @author andus.top
    * @date 2021/6/25 17:08
    * @return
   **/
   private void setSheetMarkersByRow(Sheet sheet, List<?> infos, String rowKey) throws IllegalAccessException {
       if (sheet == null) {
           return;
       }
       for (int x = sheet.getLastRowNum(); x >= 0; x--) { // sheet.getLastRowNum() n行返回n-1
           setRowMarkersByRow(sheet.getRow(x), infos, rowKey);
       }
   }

   /**
    * 逐行,将${rowkey.x}替换为 infos中bean对象的属性值<br>不会覆盖首列为“合计”的行
    * @param infos bean对象的集合
    * @param rowKey 设置rowkey。如user.id,此时rowkey=user
    * @author andus.top
    * @date 2021/6/25 17:17
    * @return
   **/
   private void setRowMarkersByRow(Row row, List<?> infos, String rowKey) throws IllegalAccessException {
       if (row == null) {
           return;
       }
       boolean checked = false;
       // 暂存表头。key:列的下标,value:bean对象的属性名称
       Map<Integer, String> keyMap = new HashMap<>();
       for (int x = 0, lc = row.getLastCellNum(); x < lc; x++) {//  n列,返回n。
           Cell cell = row.getCell(x);
           if (cell != null) {
               // 某行某列的内容
               String content = String.valueOf(getCellContent(row, cell));
               // 核验是否是模板。我们约定格式: ${*} 和 ${*.*} 是模板
               List<String[]> markerList = checkMarker(content);
               if(markerList != null && !markerList.isEmpty()){

                   for (String[] markerInfos : markerList) {
                       String listKey = markerInfos[0];
                       String mapKey = markerInfos[1];
                       String marker = markerInfos[2];
                       if(marker.indexOf(".") > 0){
                           if (rowKey.equals(listKey)) { // 是同一个rowkey才替换
                               checked = true;
                               keyMap.put(x, mapKey);
                           } else {
                               if (checked) { // 该行,第一列rowkey是对的,此时checked = true; 后面的列有可能rowkey是错的
       //                            String sheetName = row.getSheet().getSheetName();
       //                            throw new RuntimeException("marker 标记错误:[sheet=" + sheetName + ",row=" + (row.getRowNum()+1)
       //                                    + ",column=" + x + "],marker=" + content);
                               } else {
                                   break;
                               }
                           }
                       }
                   }
               }
           }
       }

       if (checked) {
           if (infos == null || infos.isEmpty()) {// 如果为空,只需要把marker替换为空
               String tmp = null;
               for (Map.Entry<Integer, String> e : keyMap.entrySet()) {
                   // e.getKey() 列的下标;row.getCell()获取该行某列Cell
                   row.getCell(e.getKey()).setCellValue(tmp);
               }
           } else {
               // 将markers 替换为对应的值
               Map<String, Object> tmap = beanToMap(infos.get(0));// java Bean 转为Map。key:属性名,vlaue:属性值
               for (Map.Entry<Integer, String> e : keyMap.entrySet()) {
                   // e.getKey() 列的下标;row.getCell()获取该行某列Cell。
                   // e.getValue() 某个bean 的属性名
                   try {
                       Integer integer = Integer.valueOf(String.valueOf(tmap.get(e.getValue())));
                       Cell cell = row.getCell(e.getKey());
                       // 设置单元格为数字类型,并且内容居中显示
                       setNumberCell(cell);
                       cell.setCellValue(integer);
                   }catch (Exception ex){
                       row.getCell(e.getKey()).setCellValue(String.valueOf(tmap.get(e.getValue())));
                   }
               }

               // 复制行并设置对应的值
               int r = row.getRowNum(); // 行下标
               for (int x = 1, s = infos.size(); x < s; x++) {// 一个bean对应一行,x为相对${rowkey.x}的便宜量。
                   tmap = beanToMap(infos.get(x));
                   // 当前需要复制的行的下标。r为模板所在的行
                   int tarRowNum = r + x;
                   Row rowx = row.getSheet().getRow(tarRowNum);
                   // fixme:根据模板中“合计”或"平均"位置调整代码
                   if(rowx == null || "合计".equals(getCellContent(rowx,  rowx.getCell(0)))){// 模板已创建row
                       rowx = insertRow(row.getSheet(), row, tarRowNum);
                   }
                   for (Map.Entry<Integer, String> e : keyMap.entrySet()) {
                       // 列下标
                       int cellNum = e.getKey();
                       // 新创建行的某列单元格
                       Cell cell = rowx.getCell(cellNum);
                       if (cell == null) {
                           // 每行创建单元格
                           // cell = rowx.createCell(cellNum, CellType.NUMERIC);
                           cell = rowx.createCell(cellNum);
                       }
                       try {
                           Integer integer = Integer.valueOf(String.valueOf(tmap.get(e.getValue()))); // e.getValue() 某个bean 的属性名
                           // 设置单元格为数字类型,并且内容居中显示
                           setNumberCell(cell);
                           cell.setCellValue(integer);
                       }catch (Exception ex){
                           // 该单元格设置值
                           cell.setCellValue(String.valueOf(tmap.get(e.getValue())));// e.getValue() 某个bean 的属性名
                       }

                   }

               }
           }
       }
   }

   /**
    * 逐行遍历,从${}模板的位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param sheet 某个sheet页
    * @param infos bean对象的集合。外层list.size 表示新增列数,内层list.size 表示当前新增列新增的行数
    * @param marker 设置marker。假设${}模板在sheet中[row=0,col=1]的位置。将会从[0,1]单元格为起始位置,按列添加数据。
    * @author andus.top
    * @date 2021/6/25 17:19
    * @return
   **/
   private void setSheetMarkersByCol(Sheet sheet, List<List<Object>> infos, String marker) throws IllegalAccessException {
       if (sheet == null) {
           return;
       }
       for (int x = sheet.getLastRowNum(); x >= 0; x--) { // sheet.getLastRowNum() n行返回n-1
           setRowMarkersByCol(sheet, infos, marker);
       }
   }

   /**
    * 从${}模板的位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param infos  bean对象的集合。外层list.size 表示新增列数,内层list.size 表示当前新增列新增的行数
    * @param marker 设置marker。如${a.b}
    * @author andus.top
    * @date 2021/6/25 17:24
    * @return
   **/
   private void setRowMarkersByCol(Sheet sheet, List<List<Object>> infos, String marker) throws IllegalAccessException {
       if (StringUtils.isEmpty(marker)) {
           return;
       }
       /**
        * 1、返回null,表示该数据不存在
        * 2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
        */
       Integer[] contentLocation = getContentLocation(sheet, marker);
       setRowMarkersByCol(sheet, infos, contentLocation);

   }

   /**
    * 从${}模板的位置开始,按列追加数据。<br><b>不会覆盖首列单元格内容为“平均”的行&不会覆盖“合计”所在的列</b>
    * @param sheet 当前sheet
    * @param infos list对象的集合。外层list.size 表示新增列数,内层list.size 表示当前新增列新增的行数
    * @param startWriteLocation 单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    * @author andus.top
    * @date 2021/6/25 17:25
    * @return
   **/
   private void setRowMarkersByCol(Sheet sheet, List<List<Object>> infos, Integer[] startWriteLocation) throws IllegalAccessException {
       if (sheet == null || infos == null || infos.isEmpty() || startWriteLocation == null || startWriteLocation.length != 4) {
           return;
       }
       // 具备样式的单元格
       Row sourceRow = sheet.getRow(startWriteLocation[0]); // 始终以 marker 所在行为源样式行
       // 每次采用每行第一列为sourceCell。若为平均列,则使用上一行的第一列作为sourceCell。fixme:根据模板中“合计”或"平均"位置调整代码
       if("平均".equals(getCellContent(sourceRow,  sourceRow.getCell(0)))){
           sourceRow = sheet.getRow(startWriteLocation[0]-1);
       }
       if (startWriteLocation != null) {
           for (int c = 0; c < infos.size(); c++) { // c 表示相对 marker 所在列向右偏移的列数
               // 向右偏移量。考虑列为合并单元格。contentLocation[3]-contentLocation[2]+1 表示合并单元格列长度
               int shiftColIdx = c * (startWriteLocation[3]-startWriteLocation[2]+1);
               List<Object> colDataList = infos.get(c);// 每列值的集合
               if (colDataList != null && !colDataList.isEmpty()) {
                   Cell sourceCell = null;
                   if (c == 0 || c == 1) {
                       sourceCell = sourceRow.getCell(startWriteLocation[2]);
                   }
                   if (c > 1) {
                       sourceCell = sourceRow.getCell(startWriteLocation[2] + shiftColIdx - (startWriteLocation[3]-startWriteLocation[2]+1));
                   }
                   for (int r = 0; r < colDataList.size(); r++) {// r 表示相对 marker 所在行向下偏移的行数

                       // marker的标记位置,直接替换为数据
                       if (r == 0 && c == 0 && sourceRow.getRowNum() == startWriteLocation[0]) {
                           sourceCell.setCellValue(String.valueOf(colDataList.get(r)));
                           continue;
                       }

                       // 向下偏移量。考虑行为合并单元格。startWriteLocation[1]-startWriteLocation[0]+1 表示合并单元格行长度
                       int shiftRowIdx = r * (startWriteLocation[1]-startWriteLocation[0]+1);

                       Row tarRow = sheet.getRow(startWriteLocation[0] + shiftRowIdx);
                       // fixme:根据模板中“合计”或"平均"位置调整代码
                       if (tarRow == null || "平均".equals(getCellContent(tarRow,  tarRow.getCell(0)))) {
                           tarRow = insertRow(sheet, sourceRow, startWriteLocation[0] + shiftRowIdx);
                       }
                       Cell tarCell = tarRow.getCell(startWriteLocation[2] + shiftColIdx);
                       // 将合计列右移,保留合计列。fixme:根据模板中“合计”或"平均"位置调整代码
                       if("合计".equals(getCellContent(tarRow, tarRow.getCell(startWriteLocation[2] + shiftColIdx)))){
                           tarRow.shiftCellsRight(startWriteLocation[2] + shiftColIdx, startWriteLocation[2] + shiftColIdx,1);
                           tarCell = null;
                       }
                       if (tarCell == null) {
                           // 创建单元格
                           tarCell = tarRow.createCell(startWriteLocation[2] + shiftColIdx);
                       }
                       // 拷贝上一个单元格
                       copyCellStyle(sheet, sourceCell, tarCell, startWriteLocation[1]-startWriteLocation[0]+1, startWriteLocation[3]-startWriteLocation[2]+1);
                       // 该单元格设置值
                       try {
                           Integer integer = Integer.valueOf(String.valueOf(colDataList.get(r)));
                           // 设置单元格为数字类型,并且内容居中显示
                           setNumberCell(tarCell);
                           tarCell.setCellValue(integer);
                       }catch (Exception ex){
                           tarCell.setCellValue(String.valueOf(colDataList.get(r)));
                       }
                   }
               } else {
                   // 每列值的集合 为null或空
               }
           }
       }
   }

   /**
    * 根据内容获取该内容所在的位置(返回第一内容所在的位置)<br>从第0行到0列到最后一行最后一列,依次遍历
    * @param sheet 当前sheet
    * @param tar 要查找的数据
    * @author andus.top
    * @date 2021/6/25 17:36
    * @return java.lang.Integer[]<br> 1、返回null,表示该数据不存在
    *      <br> 2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。行或列数据不同,表明有行或列单元格合并。
    **/
   private Integer[] getContentLocation(Sheet sheet, String tar){
       if(sheet != null && tar != null && tar.length() > 0){

           for (int r = 0, endRow = sheet.getLastRowNum(); r <= endRow; r++) {// n行返回n-1
               Row row = sheet.getRow(r);
               if(row == null){
                   // 必须执行到最后一行,遍历完excel中的数据
                   continue;
               }
               // fixme  【已修复】开始行设置为0,保证每列数据不会错误。即,保证转换为key,value是 不会错位
               short startCol = 0; // row.getFirstCellNum():第0列无数据,第1列有数据,此时返回1
               short endCol = (short)(row.getLastCellNum()-(short) 1); //  n列,返回n。fixme: 【已修复】所以最大列数需减一

               // 遍历指定范围的列
               for (int x = startCol; x <= endCol; x++) {
                   Cell cell = row.getCell(x);

                   if(cell != null) {
                       boolean isMerge = isMergedRegion(sheet, row.getRowNum(), cell.getColumnIndex());
                       //判断是否具有合并单元格
                       if (isMerge) {// 合并单元格
                           Integer[] mergedRegionValueLocation = getMergedRegionValueLocation(row.getSheet(), row.getRowNum(), cell.getColumnIndex(), tar);
                           if(mergedRegionValueLocation != null){
                               return mergedRegionValueLocation;
                           }else{
                               continue;
                           }
                       } else {// 非合并单元格
                           String cellVal = String.valueOf(getCell(cell));
                           if(tar.equals(cellVal)){
                               Integer[] cellValueLocation = new Integer[]{row.getRowNum(), row.getRowNum(), cell.getColumnIndex(), cell.getColumnIndex()};
                               return cellValueLocation;
                           }else{
                               continue;
                           }
                       }
                   }
               }
           }

       }
       // 返回null,表示该数据不存在
       return null;
   }

   /**
    * 根据内容获取该内容在该sheet中所有的位置<br>从第0行到0列到最后一行最后一列,依次遍历
    * @param sheet 当前sheet
    * @param tar 要查找的数据
    * @author andus.top
    * @date 2021/6/25 17:38
    * @return java.util.List<java.lang.Integer[]><br> 1、返回null,表示该数据不存在
    *       <br> 2、数组数据依次为单元格的起始行, 结束行, 起始列, 结束列 的下标。数据相同时,表明有行或列单元格合并
    **/
   private List<Integer[]> getContentAllLocation(Sheet sheet, String tar){
       if(sheet != null && tar != null && tar.length() > 0){

           List<Integer[]> locationList = new ArrayList<>();
           for (int r = 0, endRow = sheet.getLastRowNum(); r <= endRow; r++) {// n行返回n-1
               Row row = sheet.getRow(r);
               if(row == null){
                   // 必须执行到最后一行,遍历完excel中的数据
                   continue;
               }
               // fixme  【已修复】开始行设置为0,保证每列数据不会错误。即,保证转换为key,value是 不会错位
               short startCol = 0; // row.getFirstCellNum():第0列无数据,第1列有数据,此时返回1
               short endCol = (short)(row.getLastCellNum()-(short) 1); //  n列,返回n。fixme: 【已修复】所以最大列数需减一

               // 遍历指定范围的列
               for (int x = startCol; x <= endCol; x++) {
                   Cell cell = row.getCell(x);

                   if(cell != null) {
                       boolean isMerge = isMergedRegion(sheet, row.getRowNum(), cell.getColumnIndex());
                       //判断是否具有合并单元格
                       if (isMerge) {// 合并单元格
                           Integer[] mergedRegionValueLocation = getMergedRegionValueLocation(row.getSheet(), row.getRowNum(), cell.getColumnIndex(), tar);
                           if(mergedRegionValueLocation != null){
                               locationList.add(mergedRegionValueLocation);
                           }else{
                               continue;
                           }
                       } else {// 非合并单元格
                           String cellVal = String.valueOf(getCell(cell));
                           if(tar.equals(cellVal)){
                               Integer[] cellValueLocation = new Integer[]{row.getRowNum(), row.getRowNum(), cell.getColumnIndex(), cell.getColumnIndex()};
                               locationList.add(cellValueLocation);
                           }else{
                               continue;
                           }
                       }
                   }
               }
           }
           return locationList;

       }
       // 返回null,表示该数据不存在
       return null;
   }

   /**
    * 获取合并单元格的值<br>
    * 即获取合并单元格第一个cell的值
    * @param sheet 当前sheet
    * @param row 当前行下标
    * @param column 当前列下标
    * @param tar 查询的值
    * @author andus.top
    * @date 2021/6/25 17:39
    * @return java.lang.Integer[]该合并单元格的位置。结构为:[int startRow, int endRow, int startCol, int endCol] <br>
    *      值不存在时返回null
    **/
   private Integer[] getMergedRegionValueLocation(Sheet sheet ,int row , int column, String tar){
       // 获得一个 sheet 中合并单元格的数量
       int sheetMergeCount = sheet.getNumMergedRegions();

       // 遍历合并单元格
       for(int i = 0 ; i < sheetMergeCount ; i++){

           // 得出具体的合并单元格
           CellRangeAddress ca = sheet.getMergedRegion(i);

           // 得到合并单元格的起始行, 结束行, 起始列, 结束列
           int firstColumn = ca.getFirstColumn();
           int lastColumn = ca.getLastColumn();
           int firstRow = ca.getFirstRow();
           int lastRow = ca.getLastRow();

           // 获取合并单元格第一个cell的值
           if(row >= firstRow && row <= lastRow){
               if(column >= firstColumn && column <= lastColumn){
                   Row fRow = sheet.getRow(firstRow);
                   Cell fCell = fRow.getCell(firstColumn);
                   String cellContent = String.valueOf(getCell(fCell));
                   if(tar.equals(cellContent)){
                       return new Integer[]{firstRow, lastRow, firstColumn, lastColumn};
                   }else{
                       continue;
                   }
               }
           }
       }

       return null ;
   }

   /**
    * 获取某单元格的值。若是合并单元格,取合并单元格的值
    * @param row 某行
    * @param cell 某单元格
    * @author andus.top
    * @date 2021/6/25 17:32
    * @return java.lang.Object
    **/
   private Object getCellContent(Row row, Cell cell){
       if(row == null || cell == null){
           return null;
       }
       boolean isMerge = isMergedRegion(row.getSheet(), row.getRowNum(), cell.getColumnIndex());
       //判断是否具有合并单元格
       if (isMerge) {
           Object cellValue = getMergedRegionValue(row.getSheet(), row.getRowNum(), cell.getColumnIndex());
           return cellValue;
       } else {
           Object cellValue = getCell(cell);
           return cellValue;
       }
   }

   /**
    * 获取非合并单元格的值
    * @param cell
    * @author andus.top
    * @date 2021/6/25 17:32
    * @return java.lang.Object
    **/
   private Object getCell(Cell cell){
       CellType cft = cell.getCellType();
       switch (cft){
           case BLANK:
               return "";
           case BOOLEAN:
               return cell.getBooleanCellValue();
           case ERROR:
               return cell.getErrorCellValue();
           case NUMERIC:
               if(DateUtil.isCellDateFormatted(cell)){
                   return cell.getLocalDateTimeCellValue();
               }else{
                   return cell.getNumericCellValue();
               }
           case FORMULA:
               return cell.getCellFormula();
           //return cell.getStringCellValue();
           default:
               return cell.getStringCellValue();
       }
   }

   /**
    * 获取合并单元格的值<br>
    * 即获取合并单元格第一个cell的值
    * @param sheet 当前sheet
    * @param row 当前行下标
    * @param column 当前列下标
    * @author andus.top
    * @date 2021/6/25 17:35
    * @return java.lang.Object 该合并单元格的值
    **/
   private Object getMergedRegionValue(Sheet sheet ,int row , int column){

       // 获得一个 sheet 中合并单元格的数量
       int sheetMergeCount = sheet.getNumMergedRegions();

       // 遍历合并单元格
       for(int i = 0 ; i < sheetMergeCount ; i++){

           // 得出具体的合并单元格
           CellRangeAddress ca = sheet.getMergedRegion(i);

           // 得到合并单元格的起始行, 结束行, 起始列, 结束列
           int firstColumn = ca.getFirstColumn();
           int lastColumn = ca.getLastColumn();
           int firstRow = ca.getFirstRow();
           int lastRow = ca.getLastRow();

           // 获取合并单元格第一个cell的值
           if(row >= firstRow && row <= lastRow){
               if(column >= firstColumn && column <= lastColumn){
                   Row fRow = sheet.getRow(firstRow);
                   Cell fCell = fRow.getCell(firstColumn);
                   return getCell(fCell) ;
               }
           }
       }

       return null ;
   }

   /**
    * 判断指定的单元格是否是合并单元格
    * @param sheet
    * @param row 行下标
    * @param column 列下标
    * @author andus.top
    * @date 2021/6/25 17:34
    * @return boolean true:是合并单元格
    **/
   private static boolean isMergedRegion(Sheet sheet, int row ,int column) {

       // 得到一个sheet中有多少个合并单元格
       int sheetMergeCount = sheet.getNumMergedRegions();
       for (int i = 0; i < sheetMergeCount; i++) {

           // 得出具体的合并单元格
           CellRangeAddress range = sheet.getMergedRegion(i);

           // 得到合并单元格的起始行, 结束行, 起始列, 结束列
           int firstColumn = range.getFirstColumn();
           int lastColumn = range.getLastColumn();
           int firstRow = range.getFirstRow();
           int lastRow = range.getLastRow();

           // 判断该单元格是否在合并单元格范围之内, 如果是, 则返回 true
           if(row >= firstRow && row <= lastRow){
               if(column >= firstColumn && column <= lastColumn){
                   return true;
               }
           }
       }
       return false;
   }

   /**
    * 当前行是否包含合并行
    * @param row
    * @author andus.top
    * @date 2021/6/25 17:41
    * @return int 返回合并行的长度<br>不包含,返回=1
    **/
   private static int isContainMergeRow(Row row){
       if(row != null) {
           Sheet sheet = row.getSheet();
           int rowNum = row.getRowNum();
           for (int x = 0, s = sheet.getNumMergedRegions(); x < s; x++) {
               CellRangeAddress mergedRegion = sheet.getMergedRegion(x); // 返回一共几个合并单元格
               int mFirstRowNum = mergedRegion.getFirstRow(); // 合并单元格起始行
               int mLastRowNum = mergedRegion.getLastRow();// 合并单元格结束行
               int mFirstColNum = mergedRegion.getFirstColumn();// 合并单元格起始列
               int mLastColNum = mergedRegion.getLastColumn();// 合并单元格结束列
               if(rowNum >= mFirstRowNum && rowNum <= mLastRowNum){
                   return mLastRowNum - mFirstRowNum + 1;
               }
           }
       }
       return -1;
   }

   /**
    * 将单元格设置为整数格式,并居中显示
    * @param cell
    * @author andus.top
    * @date 2021/6/25 15:39
    * @return
   **/
   private void setNumberCell(Cell cell){
       // CellStyle cellStyle = workbook.createCellStyle();
       CellStyle cellStyle = cell.getCellStyle();
       cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
       cellStyle.setAlignment(HorizontalAlignment.CENTER);

//        cellStyle.setBorderBottom(BorderStyle.THIN); // 底部边框
//        cellStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex()); // 底部边框颜色
//        cellStyle.setBorderLeft(BorderStyle.THIN);  // 左边边框
//        cellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex()); // 左边边框颜色
//        cellStyle.setBorderRight(BorderStyle.THIN); // 右边边框
//        cellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());  // 右边边框颜色
//        cellStyle.setBorderTop(BorderStyle.THIN); // 上边边框
//        cellStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());  // 上边边框颜色

       DataFormat dataFormat = workbook.createDataFormat();//创建格式化对象
       cellStyle.setDataFormat(dataFormat.getFormat("0"));

       cell.setCellStyle(cellStyle);// 单元格类型
   }

   /**
    * 将单元格设置为保留两位小数格式,并居中显示
    * @param cell
    * @author andus.top
    * @date 2021/6/25 15:40
    * @return
   **/
   private void setDoubleCell(Cell cell){
       // CellStyle cellStyle = workbook.createCellStyle();
       CellStyle cellStyle = cell.getCellStyle();
       cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
       cellStyle.setAlignment(HorizontalAlignment.CENTER);

//        cellStyle.setBorderBottom(BorderStyle.THIN); // 底部边框
//        cellStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex()); // 底部边框颜色
//        cellStyle.setBorderLeft(BorderStyle.THIN);  // 左边边框
//        cellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex()); // 左边边框颜色
//        cellStyle.setBorderRight(BorderStyle.THIN); // 右边边框
//        cellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());  // 右边边框颜色
//        cellStyle.setBorderTop(BorderStyle.THIN); // 上边边框
//        cellStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());  // 上边边框颜色

       DataFormat dataFormat = workbook.createDataFormat();//创建格式化对象
       cellStyle.setDataFormat(dataFormat.getFormat("0.00"));

       cell.setCellStyle(cellStyle);// 单元格类型
   }

   /**
    * 检验mark格式。不是marker返回null或者空集合
    * @param cellContent 该值可能包含多个${}形式marker,或者1个${a.b}形式marker
    * @author andus.top
    * @date 2021/6/25 17:28
    * @return java.util.List<java.lang.String[]>
    **/
   private List<String[]> checkMarker(String cellContent) {
       if (StringUtils.isNotEmpty(cellContent)) {
           List<String[]> list = new ArrayList<>();
           int start = cellContent.indexOf("${");
           int end = cellContent.indexOf("}");
           while (start != -1 && end != -1){
               String marker = cellContent.substring(start, end+1);
               if(end + 1 <= cellContent.length()) {
                   cellContent = cellContent.substring(end + 1);
               }
               int point = marker.indexOf(".");
               if(point != -1){// ${a.b}
                   String key = marker.substring(2, point);
                   String field = marker.substring(point+1, marker.length()-1);
                   if(StringUtils.isNotEmpty(key) && StringUtils.isNotEmpty(field)){
                       // key、value、全部的marker,方便直接替换内容
                       String[] strings = new String[]{key, field, marker};
                       list.add(strings);

                       // 检查marker
                       if(list.size() > 1){ // list.size  表示该cell 存在${rowkey.field}类型的marker 的数量
                           throw new RuntimeException("一个单元格只允许一个${rowkey.field}类型的marker:[ cellContent=" + cellContent +" ]");
                       }
                   }else{
                       throw new RuntimeException("marker标记错误:" + marker);
                   }
               }else{ // ${a}
                   String key = null;
                   String field = marker.substring(2, marker.length()-1);
                   if(StringUtils.isNotEmpty(field)){
                       // key、value、全部的marker,方便直接替换内容
                       String[] strings = new String[]{key, field, marker};
                       list.add(strings);
                   }else{
                       throw new RuntimeException("marker标记错误:" + marker);
                   }
               }
               start = cellContent.indexOf("${");
               end = cellContent.indexOf("}");
           }
           return list;
       }
       return null;
   }


   /**
    * 复制行样式
    * @param source 样式行
    * @param target 目标行
    * @param sourceRowLength source cell 跨行数
    * @param sourceColLength source cell 跨行数
    * @author andus.top
    * @date 2021/6/25 17:41
    * @return
   **/
   private void copyCellStyle(Sheet sheet, Cell source, Cell target, int sourceRowLength, int sourceColLength) {
       CellStyle sourceCellStyle = source.getCellStyle();
       if(sourceCellStyle != null){
           target.setCellStyle(sourceCellStyle);
       }
       // 行或列偏移量大于1,说明是合并单元格。此时,每个单元格样式都复制
       if(sourceRowLength > 1 || sourceColLength > 1){
           sourceCellStyle.setAlignment(HorizontalAlignment.CENTER);
           sourceCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
           target.setCellStyle(sourceCellStyle);
           try{
               // 存在就无需添加
               CellRangeAddress region = new CellRangeAddress(
                       target.getRowIndex(), target.getRowIndex()+sourceRowLength-1,
                       target.getColumnIndex(), target.getColumnIndex()+sourceColLength-1);
               sheet.addMergedRegion(region);
           }catch (Exception e){
               e.printStackTrace();
           }

       }
   }

   /**
    * 复制行样式
    * @param source 样式行
    * @param target 目标行
    * @author andus.top
    * @date 2021/6/25 17:42
    * @return
   **/
   private static void copyRowStyle(Row source, Row target) {
       CellStyle sourceRowStyle = source.getRowStyle();
       // 行样式
       if(sourceRowStyle != null){
           target.setRowStyle(sourceRowStyle);
       }
       // 每列样式
       for (int x = 0; x < source.getLastCellNum(); x++) { //  getLastCellNum() n列,返回n。
           Cell sourceCell = source.getCell(x);
           CellStyle sourceCellStyle = sourceCell.getCellStyle();
           Cell targetCell = target.getCell(x);
           if(targetCell == null){
               targetCell = target.createCell(x);
           }
           if(sourceCellStyle != null){
               targetCell.setCellStyle(sourceCellStyle);
           }
       }
       target.setHeight(source.getHeight());
       // 列合并单元格样式
       copyRowMerge(source, target);
   }

   /**
    * 复制列合并单元格。不支持跨行合并单元格样式拷贝
    * @param source 样式行
    * @param target 目标行
    * @author andus.top
    * @date 2021/6/25 15:24
    * @return
   **/
   private static void copyRowMerge(Row source, Row target) {
       Sheet sheet = source.getSheet();
       int targetRowNum = target.getRowNum();
       int sourceRowNum = source.getRowNum();
       for (int x = 0, s = sheet.getNumMergedRegions(); x < s; x++) {
           CellRangeAddress mergedRegion = sheet.getMergedRegion(x); // 返回一共几个合并单元格
           int mFirstRowNum = mergedRegion.getFirstRow(); // 合并单元格起始行
           int mLastRowNum = mergedRegion.getLastRow();// 合并单元格结束行
           if(targetRowNum >= mFirstRowNum && targetRowNum <= mLastRowNum && mFirstRowNum < mLastRowNum){
               // fixme:可根据业务情况单独处理合并单元格问题
               throw new RuntimeException("本方法不能有跨行的合并单元格[" + "firstRow: " + mFirstRowNum + ", lastRow: " + mLastRowNum + "]");
           }
           if(sourceRowNum >= mFirstRowNum && sourceRowNum <= mLastRowNum){
               if(mFirstRowNum < mLastRowNum){
                   // fixme:可根据业务情况单独处理合并单元格问题
                   throw new RuntimeException("本方法不能有跨行的合并单元格[" + "firstRow: " + mFirstRowNum + ", lastRow: " + mLastRowNum + "]");
               }
               int mFirstColNum = mergedRegion.getFirstColumn();// 合并单元格起始列
               int mLastColNum = mergedRegion.getLastColumn();// 合并单元格结束列
               // fixme:可根据业务情况单独处理合并单元格问题
               sheet.addMergedRegion(new CellRangeAddress(targetRowNum, targetRowNum, mFirstColNum, mLastColNum));
           }
       }
   }


   /**
    * java bean 转换为map
    * @param o java bean
    * @author andus.top
    * @date 2021/6/25 17:40
    * @return map对象。key:属性名称,value:属性的值
    **/
   private static Map<String, Object> beanToMap(Object o) throws IllegalAccessException {
       if(o == null){
           return new HashMap<>();
       }
       // Map 对象直接转换
       if(o instanceof Map){
           Map<String, Object> map = new HashMap<>();
           ((Map<?, ?>)o).forEach((k, v) -> map.put(String.valueOf(k), v));
           return map;
       }
       // bean 对象通过放射获取属性名称与值,封装为map
       HashMap<String, Object> map = new HashMap<>();
       Field[] fields = o.getClass().getDeclaredFields();
       for (Field field : fields) {
           field.setAccessible(true);
           map.put(field.getName(), field.get(o));
       }
       return map;
   }

}
Java将数据导出为Excel文件 对于大数据和大规模用户下载场景,Apache POI在内存消耗和效率上存在短板。这时,可以使用针对Java的高效Excel读写库EasyExcel。Apache POI是一个强大的Java库,用来处理Microsoft Office文件。对于Excel文件(.xls和.xlsx)处理,提供有。随着需求的复杂化,可能需要设置单元格样式、数据格式、合并单元格等。(.xlsx)等API。 阅读详情

相关推荐

【java】读取、创建和修改Excel文件

Apache POI库中的核心包,用于读取、创建和修改Excel文件。

AA2534193348的博客 3902

Java 解析 /生成 Excel文件

在程序的开发过程中,通常会使用Excel文件来进行数据的导入和导出,在使用Java实现此类需求时也要经理Excel文件的解析或生成在Java技术中,能实现此类需求的技术主要有ApachePOI,AlibabaEasyExcel和JXL。其中,JXL只支持Excel2003以下版本,所以现在不太常见,那简单学习一下另外两种技术吧。...

wan25110712的博客 1万+

C#操作Excel四大主流方案深度对比:OpenXml、NPOI、EPPlus与Spire.XLS

在.NET开发中,数据处理与报表生成是常见的业务需求,而Excel作为广泛使用的数据交换格式,其高效读写成为关键技术点。其原理在于通过解析或构建Office Open XML文件结构(.xlsx实为ZIP压缩的XML集合)来实现跨平台、无Office依赖的操作。这项技术的核心价值在于提升后端服务的自动化能力与数据处理效率,广泛应用于金融报表、业务数据导出、数据分析等场景。针对C#开发者,主流方案包括微软官方的底层OpenXml SDK、支持双格式的Apache NPOI、以API优雅著称的EPPlus,以及

weixin_30814329的博客 858

Java使用excelwiter导出excel的实现过程

实现excel台帐导出工具excelwriter

Loading... 3121

【FPGA】高效捕捉信号边沿的硬件实现技巧

本文深入探讨了FPGA设计中高效捕捉信号边沿的硬件实现技巧。核心在于通过“打两拍”同步器处理亚稳态风险,并详细解析了上升沿、下降沿及双边沿检测的基础原理与Verilog实现。文章进一步介绍了脉冲展宽、数字滤波等高级优化方案,并结合UART起始位检测与按键消抖等实战场景,提供了可靠的代码范例与调试心得,帮助工程师构建稳定可靠的边沿检测模块。

weixin_29165465的博客 225

海康威视HiTools实战:5分钟搞定工业镜头选型(附避坑指南)

本文详细解析了海康威视HiTools在工业镜头选型中的应用,通过五步操作流程帮助用户快速匹配适合的镜头型号。文章深入探讨了工业视觉系统的核心参数,包括焦距、视野、景深和畸变控制,并提供了光源搭配和系统集成的实战指南,助力提升机器视觉检测精度与效率。

u0v1w2x3的博客 1056

JAVA 读取excel(支持xlsx、xls格式, 支持合并单元格)

记录一下最近使用java操作excel的代码。 导包。注意两者版本需要一致 <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.9</version> </depe...

松林羊的博客 1368

ScanNet数据集详解

ScanNet 是一个 RGB-D 视频数据集,在超过 1500 次扫描中包含了 250 万个视图,并标注了 3D 相机位姿、表面重建和实例级的语义分割。为了收集这些数据,设计了一个易于使用且具有扩展性的 RGB-D 捕获系统,包括自动化的表面重建和众包的语义标注。如果您想下载 ScanNet 数据,请使用您的机构邮箱地址填写一份 ScanNet 使用条款协议,并发送至 scannet@googlegroups.com。压缩包是将3D的标注信息“拍扁”成2D图片的结果,对应原始视频的每一帧。

frostmelody 全网同名,大家多多关注呀~ 持续分享优质内容! 3917

java 读取excel(支持xlsx、xls格式, 支持合并单元格)

java读excel实现效果概念依赖代码实现测试代码ExcelReader.java 工具类 实现效果 将excel看成一个二维表,将数据存放在List<List>>中。[0,0]表示第一个单元格,[5,3]表示第6行,第4列数据,若空行或空列也会存在对应的下标位置。 合并单元的值取最左上角单元格的值。每个子单元格的值与合并单元格的值保持一致。如:[0行,1行,0列,1列]为合并单元格,值为“AAA”。那么[0,0]、[0,1]、[1,0]、[1,1]的值都为“AAA”。 可将某行为表头

松林羊的博客 3733

VFH避障/局部路径规划算法

VFH避障/局部路径规划算法 本文章是我看《The Vector Field Histogram-Fast Obstacle Avoidance for Mobile Robots》论文时候的笔记,感兴趣的欢迎去看原论文。 VFH中有一些概念是从其他算法中借鉴过来的,并且VFH算法本身就是对VFF(Virtural Force Field)的一种改进,其中有很多的相似之处。所以按照原论文的思路,先介绍由CMU提出的Certainty Grid算法中的一些概念,再介绍一下势场法中的一些概念,接着介绍一下VFF

qq_41845878的博客 8116

Serdes之什么是(CDR)?

光收发器和电收发器使用的是我们所说的串行数据通信。在这种通信方式中,数据位是按顺序一个接一个地传输的。光收发器和电收发器使用的是我们所说的串行数据通信。在这种通信方式中,数据位是按顺序一个接一个地传输的。目前市场上大多数收发器都没有专用的时钟输入,因此时钟恢复和数据重新定时等功能是在收发器外部(即在主板上)进行的。接收端则需要恢复时钟以采样串行线上的数据。为了实现这一点,接收端需要一个与输入数据频率大致相同的时钟。通过使参考时钟与输入数据信号的跳变相位对齐,可以生成一个恢复的时钟。这被称为时钟恢复。

叫好与叫座虽然不是对立面,但想在同一个作品中达到双重效果很难。 2388

java读取excel中的数据(包括.xlsx和.xls)

java读取excel中的数据(包括.xlsx和.xls)   java读取excel文件中的数据常用jxl和poi两种方式,其中最主要的区别在于jxl不支持.xlsx,而poi支持。小编平时用的比较多的是poi的方式,所以今天小编介绍的也是poi这种方式,另外一种方式小编就不再介绍了。其中poi提供了HSSFWorkbook和XSSFWorkbook两个实现类,区别在于HSSFWorkbook主要读取的是.xls格式的文件,XSSFWorkbook主要读取的是.xlsx格式的文件。 在使用poi的时候首先

kejizhentan的博客 4万+

【大模型】AI 辅助编程操作实战使用详解

AI 编程实战使用详解

congge 8459

JAVA用POI实现EXCEL导入数据库,支持.xls与.xlsx两种格式

poi操作Excel 主要通过HSSF,XSSF两种方式。 HSSF只能解析.xls格式的excel文件,XSSF支持.xls与.xlsx两种格式。 方法: 1.获取浏览器上传的excel文件 2.根据excel文件格式解析excel文件,读取excel每一行数据并存放到集合里 3.批量写入数据库 import java.io.IOException; i...

菜菜的博客 8168

【即插即用模块】Transformer篇 | AAAI 2026 | Circulant Attention:全新ViT自注意力范式,复杂度从O(N²) 降至O(NlogN)!

本文提出了一种基于频域优化的循环注意力模块(Circulant Attention),通过离散傅里叶变换(DFT)将传统自注意力中的矩阵乘法转化为频域元素乘法,显著降低了计算复杂度。该模块采用复数线性层适配频域特征,并引入门控调制机制增强表达能力,在保持全局依赖捕捉能力的同时,将复杂度从O(N²)降至O(NlogN)。实验表明,该模块在多种视觉任务中均能提升模型性能,且计算效率更高。适用于需要高效全局注意力的视觉Transformer架构,可作为传统自注意力的替代方案。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 150

银行监管报送系统系列介绍(二十一):银行监管报送系统总览

为保护法院案件当事人、利害关系人的合法权益,全面落实《最高人民法院、中国银行业监督管理委员会关于人民法院与银行业金融机构开展网络执行查控和联合信用惩戒工作的意见》(法【2014】266号),最高人民法院和银监会联合下发了《人民法院、银行业金融机构网络执行查控工作规范》(法【2015】321号),要求银行金融机构配合最高人民法院开展 “总对总”网络执行查控工作,并建立相应的业务系统,实现与法院的网络对接,协助法院完成对被执行人银行账户存款及其他金融资金的网络查询、冻结与扣划工作。

银行信息系统应用架构详解 2862

POI读取加密的EXCEL(兼容XLS,XLSX格式)

最近项目里需要读取加密的excel,有xls和xlsx两种类型,对于加解密apache官方网站有简要说明 需要的jar包poi-3.9,jar    poi-ooxml-3.9.jar  poi-ooxml-schemas-3.9.jar   xmlbeans-2.3.0.jar  dom4j-1.6.1.jar   注意poi的3个jar版本号要对应,不然会有一些问题。 上

瓶子里的小地盘 9524

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

工具方法:使用Hutool和poi处理导入excel时对合并单元格处理,java读取Excel合并单元格

最近处理导入Excel时,遇到了合并单元格处理问题,开始直接使用了hutool的 List read = excelReader.read();但是发现获取合并单元格值时,有拿不到的情况(可能是我用的hutool版本不支持)。所以结合poi进行了处理。废话不多说,直接上干货!

hua_cheng的博客 2104

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

国产化Excel处理控件Spire.XLS教程:使用Java将CSV转换为PDF(含格式设置)

如果能将 CSV 自动转换为 PDF ,就能快速生成清晰、美观的报表,既节省手动排版时间,又能保持数据的专业呈现。本文将介绍如何使用 Spire.XLS for Java 实现这一过程——从加载 CSV 到输出高质量 PDF,仅需数行代码即可完成。

开发者效率提升工具的博客 1137

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

java导出excel的xls、xlsx两种格式和HSSFworkbook,XSSFworkbook,SXSSFworkbook三种导出对象的区别

1.HSSFworkbook对象导出的excel为xls格式 2.XSSFworkbook对象导出的excel为xlsx格式 3.xls导出最大行数为65535,xlsx最大行数为104 4.xlsx和xls在数据量相同的情况下xlsx文件更小(数据量越大差距越大),xlsx是一种压缩的格式所以文件更小,(可以将xlsx文件后缀直接改为.zip打开) 5.使用XSSFworkbook导出xlsx格式时最好设置最大行数,不然容易发生内存溢出的情况。xls格式行数上限小不容易内存溢出。 6.SXSSFWorkb

qq_41844287的博客 5250

POI库读取xlsx和xls格式excel以及解决安卓上的适配

1.解决利用POI读取excel中的各种问题。 2.解决了安卓上无法正常使用POI库的问题。

分享+记录 6823

java poi设置单元格格式为数值_Java实现文件批量导入导出实践(兼容xls,xlsx)

来源:cnblogs.com/laoyeye/p/6938889.html1、介绍java实现文件的导入导出数据库,目前在大部分系统中是比较常见的功能了,今天写个小demo来理解其原理,没接触过的同学也可以看看参考下。目前我所接触过的导入导出技术主要有POI和iReport,poi主要作为一些数据批量导入数据库,iReport做报表导出。另外还有jxl类似poi的方式,不过貌似很久没跟新...

weixin_36287059的博客 599

Java实现文件批量导入导出实例(兼容xls,xlsx)

Java实现文件批量导入导出实例(兼容xls,xlsx) Java团长 2017-06-04 09:46 1、介绍 java实现文件的导入导出数据库,目前在大部分系统中是比较常见的功能了,今天写个小demo来理解其原理,没接触过的同学也可以看看参考下。 目前我所接触过的导入导出技术主要有POI和iReport,poi主要作为一些数据批量导入数据库,iReport做报表导

u013322876的博客 3万+

EasyExcel实现Excel文件的版本兼容处理:从XLS到CSV的无缝适配方案

你是否还在为Excel文件版本兼容性问题头疼?当业务系统同时接收XLS(Excel 97-2003)、XLSX(Excel 2007+)和CSV(逗号分隔值)格式的文件时,不同格式之间的解析差异、功能支持度不同以及潜在的内存溢出风险,常常成为开发过程中的"隐形陷阱"。EasyExcel作为阿里巴巴开源的高效Excel处理工具,凭借其低内存占用和强大的兼容性,为解决这一问题提供了优雅的解决方案。 ...

gitblog_00104的博客 927

JAVA操作Excel样式

框架我们选用框架是,阿里的easyExcel(是根据apche的easyPoi修改升级后得到的)首先创建一个excel表格【测试.xlsx】,在其中添加下列格式现在需要使用easyExcel框架,继承A1的样式,填充到B1。并且在B1写入【年龄】二字 1.在pom文件中导入easyExcel框架2.写入在easyExcel中,写入就是填充有两种方式,1种是创建一个对象,另一个是Map。我们通常使用Map方式,因为map更加灵活。他的写入方式是这样的 模板文件加一个{age} ,下面是执行代码 结果

前方的路在刚开始 2726

Java读取Excel数据并转换成Json格式

一、操作.xls后缀的Excel(使用jexcel和fastjson) 1、添加dependency <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.7</version>

远方不远 9724
上一篇: Java URLDecoder.decode 加号变成空格
下一篇: java 读取excel(支持xlsx、xls格式, 支持合并单元格)
松林羊
博客等级 码龄11年 43粉丝 58原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值