Как элегантно экспортировать Excel

Java
Как элегантно экспортировать Excel

предисловие

У проекта компании недавно возникла потребность: экспорт отчетов. После выхода из строя всей системы необходимо экспортировать как минимум более 100 отчетов. В настоящее время очень важно, как элегантно реализовать экспорт отчетов и повысить производительность выпуска. Ниже мы в основном поделимся с вами методами использования и идеями реализации этого класса инструментов.

Реализованные функциональные точки

Для той же операции для каждого отчета мы его естественно извлечем, это очень просто. И самое главное: как инкапсулировать операции, разные для каждого отчета, и максимально улучшить возможность повторного использования; по вышеуказанным принципам мы в основном реализуем следующие ключевые функции:

  • Экспорт любых типов данных
  • Свободно установить заголовок
  • Свободно задайте формат экспорта полей

Пример использования

Как упоминалось выше, этот класс инструментов реализует три функциональные точки, естественно, вы можете установить эти три точки при его использовании:

  • установить список данных
  • установить заголовок
  • поле формата

следующееexportФункция может напрямую возвращать данные Excel клиенту, гдеproductInfoPosсписок данных для экспорта,ExcelHeaderInfoОн используется для сохранения информации заголовка, включая имя заголовка, первый столбец заголовка, последний столбец, первую строку и последнюю строку. Так как все форматы экспортируемых данных по умолчанию являются строками, параметр Map также требуется для указания типа формата поля (например, числовой тип, десятичный тип, тип даты). Здесь каждый знает, как им пользоваться, и эти параметры будут подробно объяснены ниже.

@Override
    public void export(HttpServletResponse response, String fileName) {
        // 待导出数据
        List<TtlProductInfoPo> productInfoPos = this.multiThreadListProduct();
        ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo());
        excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook());
    }

    // 获取表头信息
    private List<ExcelHeaderInfo> getHeaderInfo() {
        return Arrays.asList(
                new ExcelHeaderInfo(1, 1, 0, 0, "id"),
                new ExcelHeaderInfo(1, 1, 1, 1, "商品名称"),

                new ExcelHeaderInfo(0, 0, 2, 3, "分类"),
                new ExcelHeaderInfo(1, 1, 2, 2, "类型ID"),
                new ExcelHeaderInfo(1, 1, 3, 3, "分类名称"),

                new ExcelHeaderInfo(0, 0, 4, 5, "品牌"),
                new ExcelHeaderInfo(1, 1, 4, 4, "品牌ID"),
                new ExcelHeaderInfo(1, 1, 5, 5, "品牌名称"),

                new ExcelHeaderInfo(0, 0, 6, 7, "商店"),
                new ExcelHeaderInfo(1, 1, 6, 6, "商店ID"),
                new ExcelHeaderInfo(1, 1, 7, 7, "商店名称"),

                new ExcelHeaderInfo(1, 1, 8, 8, "价格"),
                new ExcelHeaderInfo(1, 1, 9, 9, "库存"),
                new ExcelHeaderInfo(1, 1, 10, 10, "销量"),
                new ExcelHeaderInfo(1, 1, 11, 11, "插入时间"),
                new ExcelHeaderInfo(1, 1, 12, 12, "更新时间"),
                new ExcelHeaderInfo(1, 1, 13, 13, "记录是否已经删除")
        );
    }

    // 获取格式化信息
    private Map<String, ExcelFormat> getFormatInfo() {
        Map<String, ExcelFormat> format = new HashMap<>();
        format.put("id", ExcelFormat.FORMAT_INTEGER);
        format.put("categoryId", ExcelFormat.FORMAT_INTEGER);
        format.put("branchId", ExcelFormat.FORMAT_INTEGER);
        format.put("shopId", ExcelFormat.FORMAT_INTEGER);
        format.put("price", ExcelFormat.FORMAT_DOUBLE);
        format.put("stock", ExcelFormat.FORMAT_INTEGER);
        format.put("salesNum", ExcelFormat.FORMAT_INTEGER);
        format.put("isDel", ExcelFormat.FORMAT_INTEGER);
        return format;
    }

добиться эффекта

Анализ исходного кода

Ха-ха, немного интересно самому анализировать собственный код. Поскольку размещать слишком много кода неудобно, можно сначала клонировать исходный код на github, а потом вернуться читать статью.✨Исходный адрес✨используется ЛЗpoi 4.0.1Эта версия этого инструмента, естественно, используется, если вы хотите практически экспортировать большие объемы данных.SXSSFWorkbookэтот компонент. Я не буду здесь много говорить о конкретном использовании poi, здесь я в основном объясню, как инкапсулировать и использовать poi.

Переменные-члены

мы фокусируемся наExcelUtilsЭтот класс, этот класс является ядром реализации экспорта, давайте взглянем на три переменных-члена.

    private List list;
    private List<ExcelHeaderInfo> excelHeaderInfos;
    private Map<String, ExcelFormat> formatInfo;
list

Эта переменная-член используется для сохранения данных для экспорта.

ExcelHeaderInfo

Эта переменная-член в основном используется для сохранения информации заголовка.Поскольку нам нужно определить несколько данных заголовка, нам нужно использовать список для их сохранения.ExcelHeaderInfoКонструктор выглядит следующим образомExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)

  • firstRow: первая строка позиции, занимаемой заголовком
  • lastRow: Последняя строка позиции, занимаемой заголовком
  • firstCol: первый столбец позиции, занимаемой заголовком
  • lastCol: Последняя строка позиции, занимаемой заголовком
  • title: имя заголовка
ExcelFormat

Этот параметр в основном используется для форматирования поля, нам нужно предварительно согласовать его преобразование в формат, который не может быть определен пользователем. Итак, мы определяем переменную типа перечисления, Класс перечисления имеет только одну переменную-член строкового типа, которая используется для сохранения формата, который мы хотим преобразовать, напримерFORMAT_INTEGERпреобразуется в целое число. Поскольку нам нужно принять формат преобразования нескольких полей, для получения определен тип карты, и этот параметр можно опустить (формат по умолчанию — строка).

public enum ExcelFormat {

    FORMAT_INTEGER("INTEGER"),
    FORMAT_DOUBLE("DOUBLE"),
    FORMAT_PERCENT("PERCENT"),
    FORMAT_DATE("DATE");

    private String value;

    ExcelFormat(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

основной метод

1. Создайте заголовок

Этот метод используется для инициализации заголовка, а ключом к созданию заголовка является класс Sheet в poi.addMergedRegion(CellRangeAddress var1)метод, который используется для单元格融合. Мы пройдемся по списку ExcelHeaderInfo, выполним слияние ячеек в соответствии с информацией о координатах каждой ExcelHeaderInfo, а затем объединим каждую ячейку после слияния.首行и首列Создайте ячейку в месте , а затем присвойте ячейке значение. Выполняя описанные выше шаги, вы можете настроить любой тип заголовка.

    // 创建表头
    private void createHeader(Sheet sheet, CellStyle style) {
        for (ExcelHeaderInfo excelHeaderInfo : excelHeaderInfos) {
            Integer lastRow = excelHeaderInfo.getLastRow();
            Integer firstRow = excelHeaderInfo.getFirstRow();
            Integer lastCol = excelHeaderInfo.getLastCol();
            Integer firstCol = excelHeaderInfo.getFirstCol();

            // 行距或者列距大于0才进行单元格融合
            if ((lastRow - firstRow) != 0 || (lastCol - firstCol) != 0) {
                sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
            }
            // 获取当前表头的首行位置
            Row row = sheet.getRow(firstRow);
            // 在表头的首行与首列位置创建一个新的单元格
            Cell cell = row.createCell(firstCol);
            // 赋值单元格
            cell.setCellValue(excelHeaderInfo.getTitle());
            cell.setCellStyle(style);
            sheet.setColumnWidth(firstCol, sheet.getColumnWidth(firstCol) * 17 / 12);
        }
    }
2. Преобразуйте данные

Прежде чем присвоить текст, нам нужно преобразовать исходный список данных в двумерный массив строк, причина преобразования его в строковый формат заключается в том, что он может единообразно обрабатывать различные типы, а затем мы можем преобразовать его обратно, если это необходимо.

    // 将原始数据转成二维数组
    private String[][] transformData() {
        int dataSize = this.list.size();
        String[][] datas = new String[dataSize][];
        // 获取报表的列数
        Field[] fields = list.get(0).getClass().getDeclaredFields();
        // 获取实体类的字段名称数组
        List<String> columnNames = this.getBeanProperty(fields);
        for (int i = 0; i < dataSize; i++) {
            datas[i] = new String[fields.length];
            for (int j = 0; j < fields.length; j++) {
                try {
                    // 赋值
                    datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j));
                } catch (Exception e) {
                    LOGGER.error("获取对象属性值失败");
                    e.printStackTrace();
                }
            }
        }
        return datas;
    }

В этом методе мы ловко реализуем любой тип экспорта данных, используя технологию отражения (любой тип здесь относится к любому типу отчета, разные отчеты, экспортируемые данные должны быть разными, тогда в реализации Java класс сущности определенно не тот) . Чтобы преобразовать список в соответствующий двумерный массив, нам нужно знать следующую информацию:

  • Количество столбцов двумерного массива
  • Количество строк в двумерном массиве
  • Значение каждого элемента двумерного массива

Что, если будут получены три вышеупомянутые информации?

  • по отражениюField[] getDeclaredFields()Этот метод получает все поля класса сущностей, таким образом, косвенно зная количество столбцов.
  • Разве размер List не равен количеству строк в двумерном массиве?
  • Хотя имена полей каждого класса сущностей разные, можем ли мы действительно не получить значение поля класса сущностей? Нет, знаешь, у тебя есть反射, вы равносильны обладанию всем миром, так что вы ничего не можете сделать. Здесь мы не используем отражение напрямую, а используем метод, называемыйBeanUtilsИнструмент может легко помочь нам назначить поля и получить значения полей для класса сущностей. очень просто, поBeanUtils.getProperty(list.get(i), columnNames.get(j))С помощью этой строки кода мы получаем объектlist.get(i)второе имяcolumnNames.get(j)Значение этого поля.list.get(i)Конечно, это класс сущностей, в котором мы просматриваем исходные данные, иcolumnNamesСписок представляет собой массив всех имен полей класса сущности, который также получается путем отражения.Конкретную реализацию см. в исходном коде LZ.
3. Текст задания

Текст здесь обозначен как официальное содержимое таблицы данных.На самом деле странных навыков не так уж и много.Основные функции реализованы выше.Здесь в основном выполняется присвоение ячеек и обработка формата экспорта(в основном для экспорта excel. удобное управление позже).

    // 创建正文
    private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) {
        List<String> columnNames = getBeanProperty(fields);
        for (int j = 0; j < columnNames.size(); j++) {
            if (formatInfo == null) {
                row.createCell(j).setCellValue(content[i][j]);
                continue;
            }
            if (formatInfo.containsKey(columnNames.get(j))) {
                switch (formatInfo.get(columnNames.get(j)).getValue()) {
                    case "DOUBLE":
                        row.createCell(j).setCellValue(Double.parseDouble(content[i][j]));
                        break;
                    case "INTEGER":
                        row.createCell(j).setCellValue(Integer.parseInt(content[i][j]));
                        break;
                    case "PERCENT":
                        style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%"));
                        Cell cell = row.createCell(j);
                        cell.setCellStyle(style);
                        cell.setCellValue(Double.parseDouble(content[i][j]));
                        break;
                    case "DATE":
                        row.createCell(j).setCellValue(this.parseDate(content[i][j]));
                }
            } else {
                row.createCell(j).setCellValue(content[i][j]);
            }
        }
    }

Основной метод экспорта класса инструмента почти готов, теперь поговорим о проблеме многопоточного запроса.

Еще два очка

1. Данные многопоточного запроса

В идеале очень полная, а в реальности все же немного худенькая. Хотя LZ создает 20 потоков для запроса данных 50w, общая эффективность не 50w/20, а всего на несколько секунд быстрее.Те, кто знает причину, могут оставить мне сообщение для обсуждения.

Давайте сначала поговорим о конкретных идеях: поскольку несколько потоков выполняются одновременно, вы не можете гарантировать, какой поток завершит выполнение первым, но мы должны обеспечить согласованность порядка данных. Здесь мы использовалиCallableинтерфейс, реализуяCallableПоток интерфейса может иметь возвращаемое значение, мы можем получить результаты запроса всех подпотоков, а затем объединить их в набор результатов. Так как же обеспечить порядок слияния?Сначала мы создалиFutureTaskСписок типов,FutureTaskТип — возвращаемый набор результатов.

List<FutureTask<List<TtlProductInfoPo>>> tasks = new ArrayList<>();

Когда мы запускаем поток каждый раз, потокFutureTaskдобавить вtaskslist, чтобы порядок элементов в списке задач соответствовал порядку, в котором мы запускаем потоки.

           FutureTask<List<TtlProductInfoPo>> task = new FutureTask<>(new listThread(map));
            log.info("开始查询第{}条开始的{}条记录", i * THREAD_MAX_ROW, THREAD_MAX_ROW);
            new Thread(task).start();
            // 将任务添加到tasks列表中
            tasks.add(task);

Далее это последовательное значение штекера, мы последовательно начинаем сtasksвычеркнуть из спискаFutureTask, затем выполнитеFutureTaskизget()метод, который блокирует вызвавший его поток до тех пор, пока не получит возвращаемый результат. После такого цикла последовательное сохранение всех данных завершается.

       for (FutureTask<List<TtlProductInfoPo>> task : tasks) {
            try {
                productInfoPos.addAll(task.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

2. Как решить таймаут интерфейса

Если вам нужно экспортировать массивные данные, может возникнуть проблема:接口超时, основная причина в том, что весь процесс экспорта занимает слишком много времени. На самом деле это тоже очень легко решить.Время отклика интерфейса слишком велико, поэтому мы можем сократить время отклика. Мы используем异步编程Решение, есть много способов реализовать асинхронное программирование, здесь мы используем самый простой в springAsyncAnnotation, метод с этой аннотацией может сразу вернуть результат ответа. Что касается использования аннотаций, вы можете проверить это сами.Вот ключевые шаги реализации:

  1. Напишите асинхронный интерфейс, который отвечает за получение запроса на экспорт от клиента, а затем начинает выполнять экспорт (примечание: экспорт здесь не возвращается напрямую клиенту, а загружается на сервер локально), до тех пор, пока выдается команда экспорта, ее можно немедленно отправить клиенту. Возвращает уникальный флаг файла excel (используется для последующего поиска файла), и интерфейс завершает работу.
  2. Напишите интерфейс статуса excel.После того, как клиент получит уникальный признак файла excel, он начинает опрашивать и вызывать этот интерфейс каждую секунду, чтобы проверить статус экспорта файла excel.
  3. Напишите интерфейс, который возвращает файл excel локально с сервера, если клиент проверяет, что excel был успешно загружен на到服务器本地, в это время вы можете запросить интерфейс для прямой загрузки файла.

Таким образом, проблема тайм-аута интерфейса может быть решена.

Адрес источника

GitHub.com/дорогой кун для/ой…

Исходный код принимает позу

  1. Создайте таблицу (вставьте данные самостоятельно)
CREATE TABLE `ttl_product_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '记录唯一标识',
  `product_name` varchar(50) NOT NULL COMMENT '商品名称',
  `category_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '类型ID',
  `category_name` varchar(50) NOT NULL COMMENT '冗余分类名称-避免跨表join',
  `branch_id` bigint(20) NOT NULL COMMENT '品牌ID',
  `branch_name` varchar(50) NOT NULL COMMENT '冗余品牌名称-避免跨表join',
  `shop_id` bigint(20) NOT NULL COMMENT '商品ID',
  `shop_name` varchar(50) NOT NULL COMMENT '冗余商店名称-避免跨表join',
  `price` decimal(10,2) NOT NULL COMMENT '商品当前价格-属于热点数据,而且价格变化需要记录,需要价格详情表',
  `stock` int(11) NOT NULL COMMENT '库存-热点数据',
  `sales_num` int(11) NOT NULL COMMENT '销量',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_del` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '记录是否已经删除',
  PRIMARY KEY (`id`),
  KEY `idx_shop_category_salesnum` (`shop_id`,`category_id`,`sales_num`),
  KEY `idx_category_branch_price` (`category_id`,`branch_id`,`price`),
  KEY `idx_productname` (`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=15000001 DEFAULT CHARSET=utf8 COMMENT='商品信息表';
  1. запустить программу
  2. В адресной строке браузера введите:http://localhost:8080/api/excelUtils/exportчтобы завершить загрузку

агитационная сессия

Эта статья написана здесь Друзья, кому она понравилась, могут ставить лайки, комментировать и подписываться!