Перейти к основному содержимому

Как загрузить файл в Spring Boot

Profile picture
Автор
Harpal Singh
Software Engineer
Profile picture
Автор
Namastecode
Оглавление

1. Введение

В этом кратком руководстве мы узнаем, как загрузить файл с помощью Spring Boot.

Давайте посмотрим, как это сделать через form submission.

2. Почему загрузка файлов важна?

Uploading files — очень распространённый сценарий использования в современных web applications, например, attaching documents в forms или uploading profile pictures. Несмотря на простоту понимания, корректная работа с файлами сопряжена с рядом сложностей. Например, открыв приложение без ограничений на file size, который пользователи могут загружать, мы рискуем перегрузить наше приложение или исчерпать доступное storage. Поэтому перед тем, как принять любой файл, нам нужно validate его type и content и handle failures gracefully.

Итак, в нашем приложении мы воспользуемся опциями, предлагаемыми Spring Boot, чтобы сделать его более надёжным, установив ограничение на file size и предоставляя обратную связь, чтобы пользователь понимал, что происходит.

3. Upload Flow

Ниже показан sequence diagram, который наглядно демонстрирует взаимодействия между User, Browser и нашим приложением Spring Boot:


sequenceDiagram
    participant User as User
    participant Browser as Browser
    participant Controller as UploadController
    participant Filesystem as File System

    User->>Browser: Selects file and submits form
    Browser->>Controller: POST /upload (multipart/form-data)
    Controller->>Controller: Check if file is empty
    alt File is valid
        Controller->>Filesystem: Save file bytes to disk
        Controller-->>Browser: Redirect to / with success message
    else File is invalid
        Controller-->>Browser: Redirect to / with error message
    end
    Browser->>User: Display upload status message


4. Конфигурация Maven

Давайте начнём с добавления наших dependencies. Убедимся, что используем последние версии spring-boot-starter-web и spring-boot-starter-thymeleaf.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.4.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
        <version>3.4.2</version>
    </dependency>
</dependencies>

Для нового проекта нам лучше начать с Spring Initializr и включить следующие зависимости:

  • Spring Web: MVC-фреймворк и встроенный Tomcat
  • Thymeleaf: Современный server-side Java template engine для web и standalone environments
Совет: Используйте Spring Boot DevTools для автоматических перезапусков во время разработки.

5. Реализация File Upload

Давайте приступим к реализации нашего приложения.

5.1 Upload Form в Thymeleaf

Сначала мы создадим Thymeleaf template, в котором будет базовая form с file input и button для submit the form на наш endpoint.

Нам нужно создать файл file-upload.html в папке templates внутри папки resources приложения:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css">
</head>
<body>
    <div class="container">
        <header>
            <h2>File Upload</h2>
        </header>
        <main>
            <form action="#" th:action="@{/upload}" method="post" enctype="multipart/form-data">
                <div class="input-group vertical">
                    <input type="file" name="file" required>
                    <button type="submit">Upload</button>
                </div>
            </form>
            <div th:if="${message}" class="alert"
                 th:classappend="${message.contains('Success') ? 'alert-success' : 'alert-error'}"
                 th:text="${message}"></div>
        </main>
    </div>
</body>
</html>
Мы часто забываем добавить атрибут enctype=multipart/form-data, и это очень распространённая ошибка. Без него browser не будет отправлять файл как multipart data, и upload не будет работать так, как ожидается.

После form мы добавили placeholder для отображения сообщений.

В нашем шаблоне мы используем mini.css, чтобы нам не приходилось заниматься стилизацией приложения, но при этом избежать стандартного внешнего вида браузера.

Вот и всё по стороне front-end.

5.2. Настройка хранилища

Далее нам нужно решить, где сохранять файлы, которые загружают наши пользователи. Давайте добавим свойство files.upload-dir в файл application.properties:

server.port=8090
files.upload-dir=./uploadedfiles

Это будет место, куда мы будем сохранять файлы.

5.3 Контроллер загрузки файлов

Теперь мы реализуем controller с именем FileUploadController, который будет предоставлять endpoint для отдачи созданного нами template в браузер:

@Controller
public class FileUploadController {

    @GetMapping("/")
    public String fileUpload() {
        return "file-upload";
    }

}

Нам просто нужно убедиться, что строка string, которую мы return, соответствует имени шаблона template, который мы создали ранее.

Далее, в том же контроллере мы создадим POST /upload endpoint для обработки запросов на загрузку файлов. Этот endpoint будет использовать MultipartFile в качестве request parameter, чтобы получить файл. MultipartFile — это интерфейс, который расширяет InputStreamSource:

@Value("${files.upload-dir}")
private String uploadDir;

@PostMapping("/upload")
public String handleUpload(
    @RequestParam("file") MultipartFile file,
    RedirectAttributes redirectAttributes
) {
    if (file.isEmpty()) {
        redirectAttributes.addFlashAttribute("message", "Please select a file");
        return "redirect:/";
    }
    try {
        String filename = StringUtils.cleanPath(file.getOriginalFilename());
        Path destination = Paths.get(uploadDir, filename);
        Files.createDirectories(destination.getParent());
        file.transferTo(destination);
        redirectAttributes.addFlashAttribute("message", "Success: " + filename + " (" + file.getSize() + " bytes)");
        return "redirect:/";
    } catch (IOException e) {
        redirectAttributes.addFlashAttribute("message", "Error: " + e.getMessage());
        return "redirect:/";
    }
}

Коротко: сначала мы получаем имя файла из входного объекта file. Затем мы создаём папки, необходимые для сохранения в destination, используя StringUtils.cleanPath, чтобы предотвратить simple path traversal attacks. Затем мы сохраняем входящие данные файла в destination и отправляем сообщение об успехе. В случае неудачи мы соответствующим образом уведомляем пользователя, чтобы он понимал, что происходит.

Также в контроллере мы внедрили директорию загрузки с помощью @Value и реализовали метод @PostMapping, который принимает MultipartFile вместе с объектом RedirectAttributes. Интерфейс RedirectAttributes в Spring MVC используется для передачи данных во время редиректа. В частности, мы использовали addFlashAttribute, чтобы временно сохранить сообщение в сессии, чтобы оно было доступно после редиректа при обработке шаблона, что позволяет отобразить корректную обратную связь.

На StringUtils.cleanPath не следует полагаться в security context. Мы рекомендуем использовать другие механизмы для предотвращения path-traversal issues.

Теперь, если мы запустим приложение с помощью mvn spring-boot:run и перейдём по адресу localhost:8090, мы должны увидеть нашу форму и сможем загружать небольшие файлы.

6. Обработка больших файлов

Теперь мы можем upload small files без каких-либо проблем. Но если file size превышает 1MB (the default limit), наше application выйдет из строя, и browser может отобразить error. Если мы хотим позволить users загружать более крупные files, мы можем настроить более высокий threshold.

6.1 Настройка Maximum File Size

Чтобы настроить максимально допустимый размер файла, мы изменяем свойства spring.servlet.multipart.max-file-size и spring.servlet.multipart.max-request-size в application.properties:

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=11MB
Распространённая ошибка: Request size должен превышать file size, чтобы учесть form data overhead.

Этого само по себе недостаточно. Нам также нужно настроить наш servlet container так, чтобы он не прерывал соединение, если входящий запрос превысит его default maximum size value до того, как сработает Spring’s multipart handling.

Наше приложение на Spring Boot по умолчанию использует embedded Tomcat. Мы можем настроить его на приём более крупных (или фактически неограниченных по размеру) POST requests, чтобы проверки размера выполнялись исключительно Spring’s multipart configuration:

server.tomcat.max-http-form-post-size=-1
server.tomcat.max-swallow-size=-1
Соображения по безопасности: Установка лимитов в -1 обычно не рекомендуется, поскольку это позволяет потенциально неограниченные по размеру запросы, что может привести к атакам Denial of Service (DoS). В идеале мы должны ограничивать размер запроса до разумного значения, соответствующего сценарию использования приложения.

Теперь, если мы попытаемся загрузить файлы размером более 10MB, Spring MVC предотвратит загрузку, но поведение по умолчанию всё ещё может привести к необработанной странице ошибки (например, Whitelabel Error Page с ошибкой 500, связанной с MaxUploadSizeExceededException). Если мы хотим настроить это поведение, чтобы взаимодействие в UI было более плавным, мы можем добавить собственную обработку ошибок.

6.2 Пользовательская обработка ошибок

За кулисами Spring MVC выбрасывает MaxUploadSizeExceededException, когда файл превышает настроенный лимит размера. Мы можем создать @ControllerAdvice, чтобы глобально перехватывать MultipartException и корректно обрабатывать эти ошибки. Мы перехватываем MultipartException, потому что MaxUploadSizeExceededException расширяет его, что позволяет нам также обрабатывать другие возможные ошибки, связанные с multipart:

@ControllerAdvice
public class FileUploadExceptionHandler {

    @ExceptionHandler(MultipartException.class)
    public String handleSizeExceeded(RedirectAttributes attrs) {
        attrs.addFlashAttribute("message",
            "File exceeds size limit!");
        return "redirect:/";
    }

}

Настройка @ControllerAdvice позволяет централизованно обрабатывать exceptions в нескольких controllers и возвращать user-friendly error messages или выполнять redirect с соответствующей обратной связью.

7. Советы для Production Environments

Хотя в этом руководстве мы объясняем простой file upload, существует несколько дополнительных соображений для production environments, которые здесь не рассматриваются.

Во-первых, мы не ограничиваем type загружаемых файлов. Мы могли бы решить это с помощью простой file extension check или реализовать более глубокую валидацию, такую как проверка MIME type или сканирование на viruses and malware.

В production environments, где мы ожидаем large files, стоит рассмотреть использование streaming approach, чтобы не загружать весь файл в memory на server. Альтернативно, стоит рассмотреть использование chunked uploads с client-side, когда файл разбивается на меньшие части, загружается по отдельности и затем собирается на server-side.

8. Заключение

В этой статье мы научились реализовывать file uploads в Spring Boot, используя простой HTML form, Thymeleaf и Spring MVC controller. Мы также узнали, как configure limits для large files и как customize the error handling process для size exceptions.

Мы разместили полный код в репозитории на GitHub.

Related

Http Requests с Rest Client в Spring Framework 6
В этом руководстве объясняется, как использовать RestClient API в Spring 6.1 для выполнения HTTP-запросов. Мы рассматриваем настройку и кастомизацию RestClient, обработку запросов (GET, POST, PUT, DELETE) и приводим пример его использования с OpenAI API. Также мы обсуждаем обработку ошибок. К концу руководства мы будем знать, как эффективно использовать RestClient в наших проектах и понимать его преимущества по сравнению с RestTemplate.
Понимание синтаксиса Cron для @Scheduled в Spring
Давайте узнаем, как использовать аннотацию @Scheduled в Spring с cron expressions.
Testcontainers в Spring Boot Integration Tests
Узнаем, как использовать testcontainers в приложении Spring boot при создании integration tests.