Ir al contenido

Cómo subir un archivo en Spring Boot

Spring Spring-Boot File-Upload Thymeleaf
Autor
Harpal Singh
Software Engineer
Traducido por
Namastecode
Tabla de contenido

1. Introducción

En este breve tutorial, aprenderemos cómo subir un archivo utilizando Spring Boot.

Veamos cómo hacerlo a través de un form submission.

2. ¿Por qué son importantes las cargas de archivos?

Subir archivos es un caso de uso muy común en las aplicaciones web modernas, por ejemplo, adjuntando documentos en formularios o subiendo fotos de perfil. Aunque es sencillo de entender, manejar archivos correctamente presenta algunos desafíos. Por ejemplo, exponer una aplicación sin restricciones sobre el tamaño de archivo que los usuarios pueden subir puede abrumar nuestra aplicación o consumir todo el almacenamiento disponible. Por lo tanto, antes de aceptar cualquier archivo, necesitamos validar su tipo y contenido y manejar los errores de manera adecuada.

Entonces, en nuestra aplicación, utilizaremos las opciones ofrecidas por Spring Boot para hacerla robusta, imponiendo un límite en el tamaño del archivo y proporcionando retroalimentación para que el usuario entienda lo que está sucediendo.

3. Flujo de Carga

El siguiente diagrama de secuencia muestra visualmente las interacciones entre el Usuario, el Navegador y nuestra aplicación de 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

Lo siento, pero parece que no has incluido contenido para traducir. Si deseas que traduzca algo específico, por favor proporciónamelo y estaré encantado de ayudarte.

4. Configuración de Maven

Comencemos incluyendo nuestras dependencias. Asegurémonos de obtener las versiones más recientes de spring-boot-starter-web y 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>

Para un nuevo proyecto, es mejor empezar desde el Spring Initializr e incluir estas dependencias:

  • Spring Web: marco MVC y Tomcat embebido
  • Thymeleaf: Un moderno motor de plantillas Java del lado del servidor para entornos web y autónomos.
Consejo: Utiliza Spring Boot DevTools para reinicios automáticos durante el desarrollo.

5. Implementando la Carga de Archivos

Comencemos a implementar nuestra aplicación.

5.1 Formulario de Carga en Thymeleaf

Primero, crearemos una plantilla de Thymeleaf que incluya un formulario básico con un campo de entrada de archivo y un botón para enviar el formulario a nuestro endpoint.

Necesitamos crear un archivo file-upload.html en una carpeta llamada templates dentro de la carpeta de recursos de la aplicación:

<!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>
Un error muy común es olvidar añadir el atributo enctype=multipart/form-data. Sin él, el navegador no enviará el archivo como datos multipart, y la carga no funcionará como se espera.

Después del form, agregamos un placeholder para mostrar mensajes.

En nuestra plantilla, estamos utilizando mini.css para que no tengamos que preocuparnos por el estilo de la aplicación, pero aún así evitar la apariencia predeterminada del navegador.

Eso es todo para el lado del front-end.

5.2. Configuración de Almacenamiento

A continuación, necesitamos decidir dónde guardar los archivos que nuestros usuarios suben. Añadamos una propiedad files.upload-dir en el archivo application.properties:

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

Este será el lugar donde guardaremos los archivos.

5.3 Controlador de Carga de Archivos

Ahora, implementaremos un controlador llamado FileUploadController que expone un endpoint para servir la plantilla que creamos al navegador:

@Controller
public class FileUploadController {

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

}

Solo necesitamos asegurarnos de que la cadena que devolvemos coincida con el nombre de la plantilla que creamos anteriormente.

A continuación, en el mismo controlador, crearemos un endpoint POST /upload para manejar las solicitudes de carga de archivos. Este endpoint utilizará un MultipartFile como parámetro de solicitud para obtener el archivo. MultipartFile es una interfaz que extiende 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:/";
    }
}

En resumen, primero obtenemos el nombre del archivo del objeto de entrada file. Luego, creamos las carpetas necesarias para guardar destination usando StringUtils.cleanPath para prevenir ataques de traversal de ruta simple. Después, guardamos los datos del archivo entrante en destination y enviamos un mensaje de éxito. En caso de falla, notificamos al usuario en consecuencia para que sepa lo que está sucediendo.

También, en el controlador, inyectamos el directorio de carga usando @Value e implementamos un método @PostMapping que recibe el MultipartFile junto con un objeto RedirectAttributes. La interfaz RedirectAttributes en Spring MVC se utiliza para pasar datos durante una redirección. Específicamente, usamos addFlashAttribute para guardar el mensaje en la sesión temporalmente, de modo que esté disponible después de la redirección cuando se procesa la plantilla, permitiendo así mostrar la retroalimentación correcta.

StringUtils.cleanPath no debe ser utilizado como referencia en un contexto de seguridad. Se deben emplear otros mecanismos para evitar problemas de recorrido de ruta.

Ahora, si ejecutamos la aplicación usando mvn spring-boot:run y vamos a localhost:8090, deberíamos ver nuestro form y poder subir archivos pequeños.

6. Manejo de Archivos Grandes

Ahora podemos subir archivos pequeños sin ningún problema. Pero si el tamaño del archivo es mayor a 1MB (el límite predeterminado), nuestra aplicación fallará, y el navegador podría mostrar un error. Si queremos permitir a los usuarios subir archivos más grandes, podemos configurar un umbral más alto.

6.1 Configurar el Tamaño Máximo de Archivo

Para configurar el tamaño máximo de archivo permitido, cambiamos las propiedades spring.servlet.multipart.max-file-size y spring.servlet.multipart.max-request-size en application.properties:

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=11MB
Error Común: El tamaño de la solicitud debe ser mayor que el tamaño del archivo para tener en cuenta la sobrecarga de los datos del formulario.

Esto por sí solo no es suficiente. También necesitamos configurar nuestro contenedor de servlets para que no aborte una conexión si la solicitud entrante excede su valor máximo de tamaño predeterminado antes de que se active el manejo de multipart de Spring. Nuestra aplicación de Spring Boot utiliza Tomcat embebido por defecto. Podemos configurarlo para aceptar solicitudes POST más grandes (o de tamaño efectivamente ilimitado) de modo que las verificaciones de tamaño sean manejadas únicamente por la configuración de multipart de Spring:

server.tomcat.max-http-form-post-size=-1
server.tomcat.max-swallow-size=-1
Preocupaciones de seguridad: Establecer límites a -1 generalmente no se recomienda, ya que permite solicitudes de tamaño potencialmente ilimitado, lo que puede llevar a ataques de Denegación de Servicio (DoS). Idealmente, debemos limitar el tamaño de la solicitud a un valor razonable apropiado para el caso de uso de la aplicación.

Ahora, si intentamos subir archivos que superen los 10MB, Spring MVC impedirá la carga, pero el comportamiento por defecto todavía podría resultar en una página de error no manejada (como una página de error Whitelabel mostrando un error 500 relacionado con MaxUploadSizeExceededException). Si queremos personalizar este comportamiento para que la interacción en la interfaz de usuario sea más fluida, podemos agregar manejo de errores personalizado.

6.2 Manejo de Errores Personalizado

Detrás de escena, Spring MVC lanza una MaxUploadSizeExceededException cuando un archivo excede el límite de tamaño configurado. Podemos crear un @ControllerAdvice para capturar MultipartException globalmente y manejar estos errores de manera elegante. Capturamos MultipartException porque MaxUploadSizeExceededException la extiende, lo que nos permite manejar otros errores potenciales relacionados con multipart también:

@ControllerAdvice
public class FileUploadExceptionHandler {

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

}

Configurar un @ControllerAdvice proporciona una manera centralizada de manejar excepciones a través de múltiples controladores y devolver mensajes de error amigables para el usuario o redirigir con la retroalimentación adecuada.

7. Consejos para Entornos de Producción

Mientras que este tutorial explica una carga de archivos simple, hay varias consideraciones adicionales para entornos de producción que no se abordan aquí.

Primero, no estamos limitando el tipo de archivos permitidos para subir. Podríamos resolver esto con una simple verificación de la extensión del archivo o implementar una validación más profunda, como verificar el tipo MIME o escanear en busca de virus y malware.

En entornos de producción donde se esperan archivos grandes, consideremos utilizar un enfoque de streaming para evitar cargar el archivo completo en la memoria del servidor. Alternativamente, consideremos utilizar cargas en bloques desde el lado del cliente, donde el archivo se divide en partes más pequeñas, se sube por separado y luego se vuelve a ensamblar en el lado del servidor.

8. Conclusión

En este artículo, aprendimos cómo implementar la carga de archivos en Spring Boot utilizando un formulario HTML simple, Thymeleaf y un controlador Spring MVC. También aprendimos cómo configurar límites para archivos grandes y personalizar el proceso de manejo de errores para las excepciones de tamaño.

Puedes encontrar el código completo en GitHub.

Relacionados

Solicitudes HTTP con Rest Client en Spring Framework 6
Spring Restclient HTTP Rest Openai
Comprendiendo la Sintaxis Cron de @Scheduled en Spring
Spring Cron Scheduling
Testcontainers en Pruebas de Integración de Spring Boot
Spring Testcontainers Testing