Subir ficheros con Angular y Spring Boot

COMPARTIR EN REDES SOCIALES

En esta entrada vamos a revisar como subir ficheros con Angular y Spring Boot. Angular será nuestra parte de Frontend donde implementaremos el formulario para subir imágenes o cualquier tipo de fichero y en Spring Boot tendremos la parte de Backend, es decir, la parte que recibirá nuestro fichero y lo guardará en un directorio en concreto.

Realizaremos un ejemplo con la creación de un usuario, donde subiremos los datos del usuario y también una imagen del usuario.

La parte de Angular

Crearemos un proyecto sencillo en Angular con un formulario para subir cualquier tipo de fichero, utilizaremos ReactiveForms:

Veamos primero el código de nuestro «app.module.ts»

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Aquí importamos el módulo de formularios reactivos: «ReactiveFormsModule» y el módulo para realizar peticiones http: «HttpClientModule»

Nuestro «app.component.html»:

Se trata de un formulario muy sencillo, pero que sirve para el ejemplo que estamos tratando:

Nombre: <input type="text" [formControl]="this.nombre">
Apellidos: <input type="text" [formControl]="this.apellidos">
Email: <input type="text" [formControl]="this.email">
Imagen: <input type="file" class="file-input" (change)="onFileSelected($event)" #fileUpload>
<button (click)="enviarFormulario()">Enviar</button>

Como podéis apreciar tenemos cuatro campos:

  1. Campo para insertar el nombre del usuario.
  2. Campo para insertar los apellidos del usuario.
  3. Campo para insertar el email del usuario.
  4. Campo para seleccionar el avatar del usuario.

Se vería de la siguiente manera:

Formulario para enviar datos de usuario y un fichero.
Formulario para enviar datos de usuario y un fichero.

Veamos ahora nuestro componente

Veamos primero el código y luego expliquémoslo:

import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {
  nombre = new FormControl('');
  apellidos = new FormControl('');
  email = new FormControl('');
  formData = new FormData();
  
  constructor(private httpClient: HttpClient){}

  enviarFormulario(){
    //Creamos el objeto usuario, con las propiedades de nuestro formulario
    let usuario = { nombre: this.nombre.value, apellidos: this.apellidos.value, email:this.email.value }
    
    //Añadimos a nuestro objeto formData nuestro objeto convertido a String
    this.formData.append("usuario", JSON.stringify(usuario));

    //Realizamos la petición a SpringBoot

    this.httpClient.post<any>('http://localhost:4200/users/save', this.formData).subscribe(data => {
        //En este punto nuestra petición ha funcionado correctamente
        alert("Usuario creado correctamente");
    });

  }

  onFileSelected(event:any){
    const file:File = event.target.files[0];

    this.formData.append("fichero", file);
  }

}

Aquí tenemos 4 atributos de clase importantes:

  1. «nombre», de tipo «FormControl», nos servirá para almacenar el nombre del usuario.
  2. «apellidos», de tipo «FormControl», nos servirá para almacenar los apellidos del usuario.
  3. «email», de tipo «FormControl», nos servirá para almacenar el email del usuario.
  4. «formData» de tipo «FormData», nos servirá para almacenar en él tanto los datos del usuario como la imagen que ha seleccionado.

Con respecto a las funciones tenemos 2 funciones importantes:

  1. La función «enviarFormulario», será la encargada de enviar los datos a nuestro servidor con SpringBoot.
  2. La función «onFileSelected», será la encargada de añadir a nuestro «formData» el fichero seleccionado por el usuario.

El funcionamiento es muy sencillo y es el siguiente:

  1. Creamos el objeto «usuario», con las propiedades de nuestro formulario.
  2. Añadimos a nuestro objeto «formData» nuestro objeto convertido a String.
  3. Realizamos la petición a Spring Boot.

Es importante destacar que la imagen ya se hallará en nuestro «formData» dado que la función «onFileSelected» se ejecutará nada más el usuario seleccione un fichero en el formulario.

Por último, hemos creado un proxy para evitar problemas de CORS en nuestro ejemplo, ya sabéis que al tener corriendo angular en el puerto 4200 y SpringBoot en el puerto 8080 esto se considera una petición CORS y de no añadir un proxy no podemos realizar la petición desde Angular:

{
    "/users/save": {
      "target": "http://localhost:8080",
      "secure": false
    },
    "/photos": {
        "target": "http://localhost:8080",
        "secure": false
    }
}

Ya tenemos preparado nuestro componente para realizar las peticiones a Spring Boot!!!!

La parte de Spring Boot

En esta parte crearemos un controlador que nos servirá para:

  1. Obtener los datos del usuario junto a su imagen y guardar dicha imagen en un directorio de nuestro servidor.
  2. Poder obtener las fotos de los usuarios.

Veamos primero nuestra clase Usuario:

package es;

public class User {

	private String nombre;
	private String apellidos;
	private String email;
	private String imagen;

	public User(String nombre, String apellidos, String email) {
		super();
		this.nombre = nombre;
		this.apellidos = apellidos;
		this.email = email;
	}

	public String getNombre() {
		return nombre;
	}

	public void setNombre(String nombre) {
		this.nombre = nombre;
	}

	public String getApellidos() {
		return apellidos;
	}

	public void setApellidos(String apellidos) {
		this.apellidos = apellidos;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getImagen() {
		return imagen;
	}

	public void setImagen(String imagen) {
		this.imagen = imagen;
	}
}

Se trata de un simple POJO con getter y setters para cada propiedad del formulario.

Veamos ahora el controlador:

package es;
import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.google.gson.Gson;

@RestController
public class MyController {

	@GetMapping("/photos/{filename}")
    public ResponseEntity<byte[]> getImage(@PathVariable("filename") String filename) {
        byte[] image = new byte[0];
        try {
            image = FileUtils.readFileToByteArray(new File("photos/" + filename));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).body(image);
    }
	
	@PostMapping("/users/save")
    public void saveUser(@RequestParam("usuario") String strUsuario, @RequestParam("fichero") MultipartFile multipartFile) throws IOException {
        //Obtenemos el nombre del fichero
        String fileName = StringUtils.cleanPath(multipartFile.getOriginalFilename());
        //Establecemos el directorio donde se subiran nuestros ficheros  
        String uploadDir = "photos";
        
        Gson gson = new Gson();
        User usuario = gson.fromJson(strUsuario, User.class);
        //Obtenemos la propiedades del usuario
        System.out.println(usuario.getNombre());
        System.out.println(usuario.getApellidos());
        System.out.println(usuario.getEmail());
        
        //Establacecemos la imagen
        usuario.setImagen(fileName);
        System.out.println(usuario.getImagen());
        
        //Guardamos la imagen
        FileUploadUtil.saveFile(uploadDir, fileName, multipartFile);
    }
}

Nuestro controlador es muy sencillo, primero tenemos un método GET que nos permite obtener cualquier imagen dado su nombre y después tenemos el método más interesante del controlador, en este caso, el método «saveUser» que responde al mapeo POST /users/save.

En este método es importante comprender los parámetros de entrada:

  1. @RequestParam(«usuario») String strUsuario, será nuestro JSON del usuario en formato String. Tendremos que convertirlo posteriormente a Objeto Java.
  2. @RequestParam(«fichero») MultipartFile multipartFile, será nuestro fichero, por ello es de tipo «MultipartFile».

El método realiza las siguientes acciones:

  1. Obtiene el nombre del fichero a partir del parámetro de entrada «multipartFile».
  2. Establece el directorio donde se guardará nuestra imagen.
  3. Convierte el String JSON que representa al usuario en un Objeto de Java utilizando para ello la librería GSON.
  4. Imprimimos las propiedades del usuario para comprobar que funciona correctamente.
  5. Establecemos al usuario el nombre de la imagen.
  6. Mostramos esta propiedad también.
  7. Por último, creamos el fichero llamando al método «saveFile» de la clase «FileUploadUtil»

Y ahora nuestra clase «FileUploadUtil»:

package es;
import java.io.*;
import java.nio.file.*;
 
import org.springframework.web.multipart.MultipartFile;
 
public class FileUploadUtil {
     
    public static void saveFile(String uploadDir, String fileName,
            MultipartFile multipartFile) throws IOException {
        Path uploadPath = Paths.get(uploadDir);
         
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }
         
        try (InputStream inputStream = multipartFile.getInputStream()) {
            Path filePath = uploadPath.resolve(fileName);
            Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException ioe) {        
            throw new IOException("Could not save image file: " + fileName, ioe);
        }      
    }
}

Como podéis apreciar en esta clase disponemos de un método que dada la ruta de un directorio, el nombre del fichero que queremos almacenar en él y el fichero en sí, dicho método nos almacena el fichero en el directorio seleccionado.

Nuestro pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>SpringBootUploadImage</groupId>
	<artifactId>SpringBootUploadImage</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<build>
		<sourceDirectory>src</sourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<release>17</release>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>2.7.0</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<version>2.7.0</version>
		</dependency>

		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.9.0</version>
		</dependency>

		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.11.0</version>
		</dependency>

	</dependencies>

</project>

Las pruebas de nuestra aplicación en Angular + Spring Boot

Cuando enviamos nuestro formulario en Angular la salida en nuestro servidor es la siguiente:


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.0)

2022-06-13 14:02:16.599  INFO 2560 --- [           main] es.Application                           : Starting Application using Java 17.0.2 on DESKTOP-501JLRB with PID 2560 (C:\Users\ivanr\eclipse-workspace\SpringBootUploadImage\target\classes started by ivanr in C:\Users\ivanr\eclipse-workspace\SpringBootUploadImage)
2022-06-13 14:02:16.603  INFO 2560 --- [           main] es.Application                           : No active profile set, falling back to 1 default profile: "default"
2022-06-13 14:02:17.274  INFO 2560 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-06-13 14:02:17.283  INFO 2560 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-06-13 14:02:17.284  INFO 2560 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.63]
2022-06-13 14:02:17.355  INFO 2560 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-06-13 14:02:17.355  INFO 2560 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 720 ms
2022-06-13 14:02:17.650  INFO 2560 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-06-13 14:02:17.657  INFO 2560 --- [           main] es.Application                           : Started Application in 1.359 seconds (JVM running for 1.637)
2022-06-13 14:02:31.599  INFO 2560 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-06-13 14:02:31.599  INFO 2560 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-06-13 14:02:31.599  INFO 2560 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
Ivan
Ramon Moran
aaa@gmail.com
imagen.png

Podemos comprobar que se ha subido nuestro fichero correctamente si navegamos en nuestro caso a http://localhost:4200/photos/imagen.png:

Foto subida al servidor y siendo servida por Spring Boot a través de un proxy de Angular
Foto subida al servidor y siendo servida por Spring Boot a través de un proxy de Angular

Es importante destacar que este mismo mecanismo nos serviría para subir cualquier tipo de fichero, no solo imágenes, podríamos subir pdf’s, documentos de word, etc…

Y esto ha sido todo, como podéis apreciar no es complicado subir ficheros con Angular y Spring Boot si se tiene claro como hacerlo!

Si os ha gustado el artículo dejad un comentario y compartirlo en Redes Sociales.

Un saludo programadores.


COMPARTIR EN REDES SOCIALES

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *