Всем привет! Меня зовут Алексей, и я работаю 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, ссылка на код
Farongy
Не надо так делать
И так тоже не надо.
На каждый входящий запрос у вас будет создаваться долгоживущий и нагруженный поток. Когда их станет 400+, вашему сервису станет плохо.
В целом всё это выглядит странно.
Если парсинг простой, можно делать на лету. Если сложный, то вот это:
выжрет память и будет ООМ.