Subir archivos con Spring Boot

Subir archivos a un sitio web no es una tarea poco común, pero tampoco es muy sencillo de lograr. Algunos casos de uso de por qué querrías subir un archivo a un...

Introducción

Subir archivos a un sitio web no es una tarea poco común, pero tampoco es muy fácil de lograr. Algunos casos de uso de por qué desea cargar un archivo en un sitio web incluyen servicios que ofrecen conversiones de archivos en línea y sitios web para compartir fotos. En determinadas aplicaciones puede que incluso queramos enviar un archivo a otro usuario, etc.

Spring proporciona una interfaz MultipartFile para manejar solicitudes HTTP de varias partes para cargar archivos. Las solicitudes de archivos de varias partes dividen los archivos grandes en fragmentos más pequeños, lo que lo hace eficiente para cargar archivos. Se puede encontrar más información al respecto aquí.

Configuración del proyecto

Para demostrar la carga de archivos, crearemos una aplicación Spring MVC típica que consta de un Controlador, un Servicio para el procesamiento de backend y hoja de tomillo para ver la representación.

La forma más sencilla de comenzar con un proyecto básico de Spring Boot, como siempre, es usar Spring Initializr. Seleccione su versión preferida de Spring Boot y agregue las dependencias Web y Thymeleaf:

spring_file_upload_create_project

Después de esto, genere como un proyecto Maven y ¡ya está todo listo!

Construyendo la aplicación

Clase de servicio

Comencemos por construir primero la capa Servicio. Lo llamaremos FileService.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Service
public class FileService {

    @Value("${app.upload.dir:${user.home}}")
    public String uploadDir;

    public void uploadFile(MultipartFile file) {

        try {
            Path copyLocation = Paths
                .get(uploadDir + File.separator + StringUtils.cleanPath(file.getOriginalFilename()));
            Files.copy(file.getInputStream(), copyLocation, StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception e) {
            e.printStackTrace();
            throw new FileStorageException("Could not store file " + file.getOriginalFilename()
                + ". Please try again!");
        }
    }
}

Vamos a desglosarlo línea por línea:

  • @Service es una especialización de la anotación @Component. Le dice a Spring que esta es una clase de servicio. Por lo general, toda la lógica empresarial se escribe en esta capa.
  • Luego tenemos una variable uploadDir, que usaremos para almacenar la ruta del directorio en el que queremos que se cargue nuestro archivo. Está anotado con @Value, lo que significa que su valor puede ser establecido por el archivo application.properties mediante la clave app.upload.dir. En caso de que esta clave no esté definida, el valor predeterminado es user.home, que se encuentra en una variable de entorno de cada sistema operativo.
  • Entonces tenemos un método público ‘uploadFile’ que toma un ‘MultipartFile’ como argumento.
  • Luego creamos la ‘Ruta’ completa del archivo usando la clase ‘Rutas’ proporcionada por Java. StringUtils.cleanPath se usa para limpiar la ruta y simplemente le agregamos uploadDir usando un File.separator. Siempre use métodos de utilidad para manejar rutas en el código porque manejará automáticamente diferentes implementaciones del sistema operativo. Por ejemplo, en Windows, el separador de archivos es \ mientras que en Linux es /.
  • Luego copiamos el archivo a la ubicación usando Files.copy. La opción de copia REPLACE_EXISTING anulará cualquier archivo con el mismo nombre allí.
  • Si hay una Excepción en todo este proceso, la capturamos y lanzamos una excepción FileStorageException personalizada.

Excepción personalizada

Escribimos una FileStorageException personalizada para cualquier excepción durante el proceso de carga de archivos. Es una clase simple que extiende RuntimeException:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class FileStorageException extends RuntimeException {

    private static final long serialVersionUID = 1L;
    private String msg;

    public FileStorageException(String msg) {
        this.msg = msg;
    }

    public String getMsg() {
        return msg;
    }
}

Para poder usar la excepción de la forma en que lo hicimos, Spring necesita saber cómo lidiar con ella si se encuentra. Para eso, hemos creado un AppExceptionHandler que está anotado con @ControllerAdvice y tiene un @ExceptionHandler definido para FileStorageException:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@ControllerAdvice
public class AppExceptionHandler {

    @ExceptionHandler(FileStorageException.class)
    public ModelAndView handleException(FileStorageException exception, RedirectAttributes redirectAttributes) {

        ModelAndView mav = new ModelAndView();
        mav.addObject("message", exception.getMsg());
        mav.setViewName("error");
        return mav;
    }
}

En el método handleException simplemente devolvimos el objeto ModelAndView que devolverá el mensaje de error establecido en una vista de error, que es solo una plantilla de Thymeleaf llamada error.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>ERROR</title>
</head>
<body>
  <h1>Error!!!</h1>
  <div th:if="${message}">
    <h2 th:text="${message}"/>
  </div>
</body>
</html>

Si desea leer más acerca de las excepciones en Java y Spring, lo cubrimos en detalle en los siguientes artículos:

Controlador y interfaz

Ahora vamos a crear una clase FileController simple que usará el FileService para manejar la carga de archivos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
public class FileController {

    @Autowired
    FileService fileService;

    @GetMapping("/")
    public String index() {
        return "upload";
    }

    @PostMapping("/uploadFile")
    public String uploadFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {

        fileService.uploadFile(file);

        redirectAttributes.addFlashAttribute("message",
            "You successfully uploaded " + file.getOriginalFilename() + "!");

        return "redirect:/";
    }
}

Vamos a desglosarlo línea por línea:

  • La anotación @Controller también es una especialización de la anotación @Component. Hace que una clase acepte la solicitud HTTP y responda en consecuencia. También se ocupa de las diversas conversiones de la carga útil de la solicitud a una estructura de datos interna.
  • A continuación, @Autowired el bean FileService para que podamos usar su método uploadFile.
  • Entonces tenemos un GetMapping simple en / que simplemente devolverá String upload. Al ser una clase de controlador, Spring buscará upload.html y lo entregará al navegador.
  • A continuación, tenemos un PostMapping de /uploadFile, que tiene un RequestParam de MultipartFile que es un objeto que tiene nuestro archivo y sus detalles de metadatos.
  • Luego usamos el método FileService uploadFile para cargar el archivo. RedirectAttributes es una especialización de la interfaz Spring Model que se usa para seleccionar atributos para un escenario de redireccionamiento.
  • Si la operación anterior es exitosa, configuramos el mensaje de éxito en redirectAttributes y redirigimos a la misma página.

Ahora hagamos otra plantilla de Thymeleaf, upload.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <h1>Spring Boot File Upload Example</h1>
  <hr/>
  <h4>Upload Single File:</h4>
  <form method="POST" th:action="@{/uploadFile}" enctype="multipart/form-data">
    <input type="file" name="file"/> <br/><br/>
    <button type="submit">Submit</button>
  </form>
  <hr/>
  <div th:if="${message}">
    <h2 th:text="${message}"/>
  </div>
</body>
</html>

Arriba, tenemos un formulario simple que se asigna a la URL /uploadFile. Tenga en cuenta que enctype es multipart/form-data y el tipo input es file. En la parte inferior tiene un mensaje div para mostrar el mensaje de éxito.

Nuestra clase principal es una clase principal típica de Spring Boot:

1
2
3
4
5
6
7
@SpringBootApplication
public class FileIoApplication {

    public static void main(String[] args) {
        SpringApplication.run(FileIoApplication.class, args);
    }
}

Ejecutemos nuestra aplicación y naveguemos a http://localhost:8080:

spring_file_upload_upload_single_file_preview

Elija un archivo y cárguelo, debería ver algo como:

spring_file_upload_upload_single_file_result

Subir varios archivos

Del mismo modo, podemos escribir código para cargar varios archivos. Agregue la siguiente asignación en FileController.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@PostMapping("/uploadFiles")
public String uploadFiles(@RequestParam("files") MultipartFile[] files, RedirectAttributes redirectAttributes) {

    Arrays.asList(files)
        .stream()
        .forEach(file -> fileService.uploadFile(file));

    redirectAttributes.addFlashAttribute("message",
        "You successfully uploaded all files!");

    return "redirect:/";
}

Como puede ver, el mapeo /uploadFiles es similar al anterior, excepto que tiene MultipartFile[] como argumento. Usamos la API de transmisión de Java 8 para cargar cada archivo en la matriz.

Al igual que antes, si la operación anterior es exitosa, configuramos el mensaje de éxito en redirectAttributes y redirigimos a la misma página.

Ahora, solo necesitamos actualizar el código en la plantilla upload.html para manejar esto:

1
2
3
4
5
6
7
<h4>Upload Multiple Files:</h4>
<form method="POST" th:action="@{/uploadFiles}" enctype="multipart/form-data">
  <input type="file" name="files" multiple/> <br/><br/>
  <button type="submit">Submit</button>
</form>

<hr/>

Lo único diferente del HTML anterior es que la asignación es al punto final /uploadFiles y input tiene un atributo múltiple, lo que le permite seleccionar más de un archivo. Además, dado que @RequestParam es archivos, tenemos que usar el mismo nombre en HTML.

Ejecutemos nuestra aplicación nuevamente y naveguemos a http://localhost:8080:

spring_file_upload_upload_multiple_file_preview

Elegir la segunda opción ahora nos permite seleccionar más de un archivo de nuestro sistema de archivos y cargarlos todos.

spring_file_upload_upload_multiple_file_result

Limitación del tamaño de archivo

Puede ajustar los límites de carga de archivos usando spring.servlet.multipart.max-file-size y spring.servlet.multipart.max-request-size en application.properties:

1
2
spring.servlet.multipart.max-file-size = 5MB
spring.servlet.multipart.max-request-size = 5MB

Puede establecer los límites en KB, MB, GB, etc.

El valor predeterminado para spring.servlet.multipart.max-file-size es 1 MB y el valor predeterminado para spring.servlet.multipart.max-request-size es 10 MB. Aumentar el límite para max-file-size es probablemente una buena idea ya que el valor predeterminado es muy bajo, pero tenga cuidado de no establecerlo demasiado alto, lo que podría sobrecargar su servidor.

Conclusión

En este artículo, hemos cubierto cómo cargar un solo archivo y varios archivos en una aplicación Spring Boot. Utilizamos la interfaz MultipartFile de Spring para capturar el archivo en la solicitud HTTP y las plantillas de Thymeleaf como nuestro motor de renderizado.

Como siempre, el código de los ejemplos utilizados en este artículo se puede encontrar en GitHub.