Всем привет! Меня зовут Алексей, и я работаю Java‑разработчиком с 2018 года. В статье расскажу, как столкнулся с проблемой обработки MultipartFile в многопоточном режиме. Почему эта проблема возникает и какие решения существуют.
Изначально стояла задача организовать фоновую обработку Excel-файлов: принимать файл, мгновенно возвращать клиенту HTTP-200 (без данных), а обработку содержимого выполнять асинхронно в отдельном потоке.
Вроде задачка тривиальная. Делаем контроллер:
@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); } }
Делаем сервис для обработки с использованием CompletableFuture, чтобы руками не создавать потоки.
@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 : test1_1@gmail.com 2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_5@gmail.com 2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_2@gmail.com 2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_4@gmail.com 2025-04-03T22:03:51.804+03:00 INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_3@gmail.com 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 : test1_1@gmail.com 2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_5@gmail.com 2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_2@gmail.com 2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_4@gmail.com 2025-04-04T20:43:33.524+03:00 INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService : test1_3@gmail.com 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 : Чтение данных из файла(ов) завершено
Первое, что делаем — проверяем, а точно ли два файла пришли в сервис?
Из скрина видно, что точно файлы пришли. Тогда нужно понять, почему файл удаляется. Давайте посмотрим документацию на MultipartFile.
Перевод описания Mutipart файла из документации:
Представление загруженного файла, полученное в результате запроса, состоящего из нескольких частей.
Содержимое файла сохраняется либо в памяти, либо временно на диске. В любом случае пользователь несет ответственность за копирование содержимого файла в хранилище на уровне сеанса или постоянное хранилище по желанию. Временное хранилище будет очищено в конце обработки запроса.
Из документации становится понятно, что MutipartFile — это объект Spring, который привязан к запросу. А откуда у нас начинается обработка http запроса?
Правильно, из DispatcherServlet.
Давайте построим цепочку вызовов и посмотрим, где файл создается:
1. DispatcherServlet.doDispatch()
2. StandardServletMultipartResolver.resolveMultipart()
3. HttpServletRequest.getParts() (Servlet API)
4. org.apache.catalina.connector.Request.parseParts() (Tomcat)
Ок, мы поняли, что файл создаётся Томкатом как временный в момент создания запроса, но кто и когда его удаляет?
Ответ кроется в последних строчках DispatcherServlet.doDispatch().
Из кода видно, что если конкуретная обработка не начата, то при завершении обработки удаляем файл.

Под конкурентной обработкой понимается запрос не пустой и асинхронный.
Получается, если Spring не знает о том, что запрос асинхронный, то просто удаляет MultipartFile.
Решение номер 1: сообщить Spring, что запрос асинхронный
Отсюда есть следующий вариант решения: сообщить Spring, что запрос асинхронный:
Для этого выносим CompletableFuture в контроллер.
Ставим аннотацию Async над методом, который вызываем, чтобы Spring понял, что этот метод асинхронный.
@Async, которая указывает Spring на необходимость асинхронной обработки файлов. И добавляем аннотацию @EnableAsyncк классу с аннотацией @SpringBootApplication.
После этого ошибка ушла.
Решение номер 2: выгрузить файлы в массив байт до обработки в CompletableFuture

Когда второй вариант может быть полезен?
Если обработку файлов нужно встроить в сервис, который уже обрабатывает json в form-data, а теперь ещё часть данных из excel файлов берёт, и вы уверены, что не будет очень больших файлов и вы не получите OutOfMemoryError.
Итог:
-
Когда делаете обработку файла в многопоточном режиме, проверьте работу программы с несколькими файлами.
-
Если работаете с MultipartFile, то учитывайте что срок времени жизни данного объекта привязан к времени жизни запроса.
-
Если обрабатываете MultipartFile в многопоточном режиме, то либо сообщите Spring об этом или выгрузите файл в память до обработки файлов в многопоточном режиме.
Ссылки: документация для MultiPart, ссылка на код
ссылка на оригинал статьи https://habr.com/ru/articles/928016/
Добавить комментарий