В методе синхронного экспорта Excel SpringBoot служба будет заблокирована до тех пор, пока не будет создан файл Excel. Если экспортируется большой объем данных, эффективность будет низкой, а качество работы будет плохим. Эффективное решение — разделить экспортированные данные и использовать CompletableFuture для асинхронизации задачи экспорта, использовать easyExcel для параллельного экспорта нескольких файлов Excel и, наконец, сжать все файлы в формат ZIP для удобства загрузки.
На основе приведенного выше решения в среде Springboot следующий код может быть выполнен с высоким качеством для экспорта информации о заказе на продажу в файлы Excel, упаковки нескольких файлов Excel в ZIP-файл и, наконец, отправки его клиенту:
@RestController
public class SalesOrderController {
@Resource
private SalesOrderExportService salesOrderExportService;
@PostMapping(value = "/salesOrder/export")
public void salesOrderExport(@RequestBody @Validated RequestDto req, HttpServletResponse response) {
salesOrderExportService.salesOrderExport(req, response);
}
}
Отвечает за выполнение логики экспорта заказов на продажу:
@Slf4j
@Service
public class SalesOrderExportService {
@Autowired
@Qualifier("threadPoolTask")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Resource
private OrderManager OrderManager;
public void salesOrderExport(RequestDto req, HttpServletResponse response) {
// Чтобы получить данные экспорта, каждый экземпляр SalesOrder необходимо экспортировать в файл Excel.
List<SalesOrder> orderDataList = OrderManager.getOrder(req.getUserCode());
// Пропустить...проверить данные
InputStream zipFileInputStream = null;
Path tempZipFilePath = null;
Path tempDir = null;
// Получить шаблон экспорта
try (InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream("template/order_template.xlsx");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) {
if (Objects.isNull(templateInputStream)) {
throw new RuntimeException("Исключение при получении файла шаблона");
}
// Несколько потоков, принимающих файловый поток
IOUtils.copy(templateInputStream, outputStream);
// Создайте временный каталог экспорта файлов Excel, чтобы экспортировать в этот каталог несколько файлов Excel.
Path tmpDirRef = (tempDir = Files.createTempDirectory(req.userCode() + "dir_prefix"));
// Каждые 5 потоков продаж SalesOrder параллельно экспортируются в файлы Excel.
CompletableFuture[] salesOrderCf = Lists.partition(orderDataList, 5).stream()
.map(orderDataSubList -> CompletableFuture
.supplyAsync(() -> orderDataSubList.stream()
.map(orderData -> this.exportExcelToFile(tmpDirRef, outputStream, orderData))
.collect(Collectors.toList()), threadPoolTaskExecutor)
.exceptionally(e -> {throw new RuntimeException(e);}))
.toArray(CompletableFuture[]::new);
// Подождите, пока все файлы Excel будут экспортированы.
CompletableFuture.allOf(salesOrderCf).get(3, TimeUnit.MINUTES);
// Создать временный zip-файл
tempZipFilePath = Files.createTempFile(req.userCode() + TMP_ZIP_DIR_PRE, ".zip");
// Сожмите все файлы в каталоге Excel в zip-файл. Существует множество наборов инструментов zipUtil.
ZipUtil.zip(tempDir.toString(), tempZipFilePath.toString());
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(tempZipFilePath.toFile().getName(), "utf-8"));
// Записать поток zip-файла в ответ
zipFileInputStream = Files.newInputStream(tempZipFilePath);
IOUtils.copy(zipFileInputStream, response.getOutputStream());
} catch (Exception e) {
log.error("salesOrderExport,Exception:", e);
throw new RuntimeException("Исключение экспорта, повторите попытку позже");
} finally {
try {
// закрыть поток
if (Objects.nonNull(zipFileInputStream)) {
zipFileInputStream.close();
}
// Удалить временные файлы и каталоги
if (Objects.nonNull(tempDir)) {
Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.deleteIfExists(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.deleteIfExists(dir);
return FileVisitResult.CONTINUE;
}
});
}
if (Objects.nonNull(tempZipFilePath)) {
Files.deleteIfExists(tempZipFilePath);
}
} catch (Exception e) {
log.error("salesOrderExport, Не удалось закрыть файловый поток:", e);
}
}
Содержимое шаблона:
/**
* Экспортируйте один файл Excel, вызов многопоточного кода выше.
**/
private Path exportExcelToFile(Path temporaryDir, ByteArrayOutputStream templateOutputStream, SalesOrder data) {
Path temproaryFilePath = null;
try {
// Создание временных файлов
temproaryFilePath = Files.createTempFile(temporaryDir, data.getOrderNo(), ExcelTypeEnum.XLSX.getValue());
} catch (IOException e) {
throw new RuntimeException("exportExcelToFile, не удалось создать временный файл Excel:" + data.getOrderNo());
}
try (InputStream templateInputStream = new ByteArrayInputStream(templateOutputStream.toByteArray());
OutputStream temporaryFileOs = Files.newOutputStream(temproaryFilePath);
BufferedOutputStream tempOutStream = new BufferedOutputStream(temporaryFileOs)) {
// Используйте функцию шаблона easyExcel для экспорта данных заказа во временный файл.
ExcelWriter excelWriter = EasyExcel.write(tempOutStream, SalesOrder.class)
.withTemplate(templateInputStream).excelType(ExcelTypeEnum.XLSX).build();
// Заполнение данных шаблона
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(new FillWrapper("goods", data.getGoodsList()), fillConfig, writeSheet);
excelWriter.fill(data, writeSheet);
excelWriter.finish();
// temproaryFilePath.toFile().deleteOnExit();
return temproaryFilePath;
} catch (Exception e) {
throw new RuntimeException("exportExcelToFile, не удалось экспортировать файл Excel:" + data.getOrderNo(), e);
}
}
Файл экспорта выглядит следующим образом:
CompletableFuture
иThreadPoolTaskExecutor
,Распределите задачу экспорта заказов на продажу на несколько потоков для параллельного выполнения.,Значительно улучшена производительность при обработке больших заказов.Lists.partition
Метод разделения списка заказов на несколько подсписков,Каждый подсписок обрабатывается потоком,Здесь на каждые 5 заказов приходится одна тема.getResourceAsStream
загрузка метода,Простота обслуживания.try-with-resources
иtry-catch-finally
чтобы убедиться, что ресурс правильно закрытиубирать。