Проблема с обработкой файла в многопоточном режиме
Проблема с обработкой файла в многопоточном режиме
Есть код контроллера и сервиса, который обрабатывает файл и выводит список email в консоль
import com.example.file_multithreading_problem.services.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping
public class FileController {
private final FileService fileService;
@Autowired
public FileController(FileService fileService) {
this.fileService = fileService;
}
@PostMapping(value = "/upload-from-files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> saveFromFiles(@RequestPart(value = "files", required = false) List<MultipartFile> files) {
fileService.parseValuesFromFileToDTO(files);
return new ResponseEntity<>(HttpStatus.OK);
}
}
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
@Service
public class FileService {
static final Logger log = LoggerFactory.getLogger(FileService.class);
public void parseValuesFromFileToDTO(List<MultipartFile> files) {
CompletableFuture<Void> filesDtoFromFile = CompletableFuture.runAsync(() ->
{
for(MultipartFile file : files) {
final Integer cellIndex = 0;
final Integer sheetIndex = 0;
final Integer limitValues = 100;
try {
Set<String> emailsFromFile = parseCellValueByCellIndexAndSheetIndexWithLimitValues(cellIndex,
sheetIndex,
limitValues,
file.getBytes());
for (String email:emailsFromFile) {
log.info(email);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}, Executors.newSingleThreadExecutor()).exceptionally((e) -> {
log.error("Ошибка парсинга значений из файла(ов)", e);
return null;
});
filesDtoFromFile.thenRun(() -> log.info("Чтение данных из файла(ов) завершено"));
}
/***
* Получение списка уникальных значений из файла из заданного листа и колонки по индексу с ограничением количества успешно считанных значений
*
* @param cellIndex - индекс колонки файла.
* @param sheetIndex - индекс листа файла.
* @param limit - ограничение по количеству значений итогового списка.
*
* @return Коллекция уникальных записей из файла.
*/
public static Set<String> parseCellValueByCellIndexAndSheetIndexWithLimitValues(Integer cellIndex, Integer sheetIndex,
Integer limit, byte[] fileBytes) throws IOException {
Set<String> values = new HashSet<>();
try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(fileBytes))) {
Sheet sheet = workbook.getSheetAt(sheetIndex);
for (Row row : sheet) {
Cell cell = row.getCell(cellIndex);
if (!ObjectUtils.isEmpty(cell) && Objects.equals(cell.getCellType(), CellType.STRING)) {
String value = cell.getStringCellValue();
if (!ObjectUtils.isEmpty(value)) {
values.add(value.toLowerCase());
}
if (Objects.nonNull(limit) && values.size() >= limit) {
break;
}
}
}
}
return values;
}
}
Если отправить один файл в запросе, то всё работает
2025-04-03T22:03:51.803+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : Чтение данных из файла(ов) завершено
Если отправить два файла, то в сервис файлы попадают, а дальше при обработки в многопоточном режиме падают с ошибкой
2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : [email protected]
2025-04-04T20:43:33.525+03:00 ERROR 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : Ошибка парсинга значений из файла(ов)
java.util.concurrent.CompletionException: java.lang.RuntimeException: java.nio.file.NoSuchFileException: C:\Users\Knd-a-aaosetskiy\AppData\Local\Temp\tomcat.8080.4482325861160796176\work\Tomcat\localhost\ROOT\upload_8740c826_186b_49e1_b8e7_a358cde48630_00000001.tmp
at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:323) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:359) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:364) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1851) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:1575) ~[na:na]
Caused by: java.lang.RuntimeException: java.nio.file.NoSuchFileException: C:\Users\Knd-a-aaosetskiy\AppData\Local\Temp\tomcat.8080.4482325861160796176\work\Tomcat\localhost\ROOT\upload_8740c826_186b_49e1_b8e7_a358cde48630_00000001.tmp
at com.example.file_multithreading_problem.services.FileService.lambda$parseValuesFromFileToDTO$0(FileService.java:43) ~[classes/:na]
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1848) ~[na:na]
... 3 common frames omitted
Caused by: java.nio.file.NoSuchFileException: C:\Users\Knd-a-aaosetskiy\AppData\Local\Temp\tomcat.8080.4482325861160796176\work\Tomcat\localhost\ROOT\upload_8740c826_186b_49e1_b8e7_a358cde48630_00000001.tmp
at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85) ~[na:na]
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103) ~[na:na]
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108) ~[na:na]
at java.base/sun.nio.fs.WindowsFileSystemProvider.newByteChannel(WindowsFileSystemProvider.java:234) ~[na:na]
at java.base/java.nio.file.Files.newByteChannel(Files.java:380) ~[na:na]
at java.base/java.nio.file.Files.newByteChannel(Files.java:432) ~[na:na]
at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:420) ~[na:na]
at java.base/java.nio.file.Files.newInputStream(Files.java:160) ~[na:na]
at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:196) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:97) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getBytes(StandardMultipartHttpServletRequest.java:259) ~[spring-web-6.2.2.jar:6.2.2]
at com.example.file_multithreading_problem.services.FileService.lambda$parseValuesFromFileToDTO$0(FileService.java:38) ~[classes/:na]
... 4 common frames omitted
2025-04-04T20:43:33.533+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : Чтение данных из файла(ов) завершено
Почему выпадает ошибка java.util.concurrent.CompletionException?
Полный код с запросами и прикрепленными файлами
Ответы (2 шт):
Если ты делаешь асинхронную обработку файла, то файл надо вычитать полностью до возврата из ResponseEntity<Void> saveFromFiles, потому что после возврата Томкат думает, что ты всё сделал, и удаляет временный файл.
Альтернативно, можно возвращать твой CompletableFuture Спрингу, и он тогда сам будет следить, когда асинхронная обработка закончилась.
Я сам Спринг уже плохо помню, что-то типа
public CompletableFuture<Void> parseValuesFromFileToDTO(List<MultipartFile> files) {
CompletableFuture<Void> filesDtoFromFile = CompletableFuture.runAsync(() ->
....
return filesDtoFromFile.thenRun(v -> log.info("Чтение данных из файла(ов) завершено"));
}
и
@PostMapping(value = "/upload-from-files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CompletableFuture<ResponseEntity<Void>> saveFromFiles(@RequestPart(value = "files", required = false) List<MultipartFile> files) {
return fileService.parseValuesFromFileToDTO(files)
.thenApply(()->new ResponseEntity<>(HttpStatus.OK));
}
Когда запрос с файлами поступает в контроллер, Spring создает для них временные ресурсы. Проблема возникает, если обработка этих файлов запускается в отдельном потоке (например, через CompletableFuture). Spring, не зная о фоновой задаче, считает обработку запроса завершенной и удаляет временные файлы. Это приводит к ошибке, когда фоновый поток пытается обратиться к уже удаленным данным.
Предложенные решения из моей статьи:
- Объявить запрос асинхронным.
Этот подход сообщает Spring, что обработка запроса еще продолжается в другом потоке, что предотвращает преждевременную очистку ресурсов.
Риск: При большом количестве одновременных запросов это может привести к повышенному потреблению памяти, так как связанные с ними ресурсы будут удерживаться дольше.
- Считать файлы в память до запуска фоновой задачи.
Данные файлов предварительно полностью загружаются в оперативную память (в виде массива байтов), и фоновый поток работает уже с этой копией, а не с временными файлами.
Риск: Если обрабатываются крупные файлы, этот метод может создать значительную нагрузку на оперативную память.
Вывод: Оба подхода решают исходную проблему, но требуют учета потенциальных рисков, связанных с использованием памяти.