Subida de imágenes en Java JEE usando Servlets (Multipart)

En esta práctica aprenderás a:

  • Enviar un archivo desde un formulario HTML hacia un Servlet.
  • Procesar peticiones multipart/form-data.
  • Guardar una imagen en el servidor.
  • Obtener el nombre del archivo subido.
  • Persistir la referencia en base de datos.
  • Entender el flujo completo cliente → servidor → disco → BD → vista.

Arquitectura del proceso

El flujo que vamos a implementar es el siguiente:

[Formulario HTML]
        ↓
POST multipart/form-data
        ↓
[Servlet controlador]
        ↓
Procesa campos + fichero
        ↓
Guarda imagen en disco
        ↓
Inserta datos en BD (nombre del archivo)
        ↓
Redirección a galería
        ↓
[Vista HTML muestra la imagen]

Video

El Servlet

Anotaciones necesarias

@WebServlet("/BestiasFoto")
@MultipartConfig
public class BestiasFoto extends HttpServlet {

@WebServlet

Define la URL que ejecuta el servlet:

http://servidor/app/BestiasFoto

@MultipartConfig

Activa el soporte de subida de archivos.

Sin esta anotación:

  • request.getPart("foto") no funcionará
  • El servidor no podrá procesar multipart/form-data

Variables del Servlet

private String pathFiles = "C:\\entornos\\clasesMaster\\BestiarioJEE\\bestiario\\src\\main\\webapp\\img\\";
private File uploads = new File(pathFiles);
  • pathFiles → carpeta donde se guardarán las imágenes
  • uploads → objeto File que representa esa carpeta

En esta práctica guardamos las imágenes dentro del proyecto. En producción normalmente se usa una carpeta externa.


Método doPost()

protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    if(request.getParameter("opcion").equals("1")) {
        insertar_actualizarBestia(request,response);
    } else if(request.getParameter("opcion").equals("2")) {
        listarBestias(Integer.parseInt(request.getParameter("id")),request,response);
    }
}

Este método actúa como enrutador.

opcionAcción
1Inserta o actualiza una bestia + sube imagen
2Listado de bestias

Método insertar_actualizarBestia()

Este método:

  1. Recoge los datos del formulario
  2. Sube la imagen
  3. Inserta en BD
  4. Redirige a la galería
public void insertar_actualizarBestia(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

    int id = Integer.parseInt(request.getParameter("id"));
    String nombre = request.getParameter("nombre");
    int tam = Integer.parseInt(request.getParameter("tam"));
    int alineamiento = Integer.parseInt(request.getParameter("alineamiento"));
    String descripcion = request.getParameter("descripcion");

    String ruta = this.subirFoto(request, response);

    if(id != 0) {
        // actualizar
    } else {
        Bestia b = new Bestia(nombre, tam, alineamiento, descripcion, ruta);
        try {
            b.insertar();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        response.sendRedirect("galeria.html");
    }
}

Explicación

  • getParameter() → obtiene datos normales del formulario
  • subirFoto() → guarda la imagen y devuelve el nombre del archivo
  • sendRedirect() → evita reenviar el formulario al refrescar

Método subirFoto()

Este método recibe el archivo, lo guarda en el servidor y devuelve el nombre.

private String subirFoto(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

    Part part = request.getPart("foto");

    Path path = Paths.get(part.getSubmittedFileName());
    String fileName = path.getFileName().toString();

    InputStream input = part.getInputStream();

    String ruta ="";
    String nombreArchivo ="";

    if(input != null) {

        File file = new File(uploads, fileName);

        ruta = file.getAbsolutePath();
        nombreArchivo = file.getName();

        System.out.println("La ruta de la foto es: " + ruta);

        try {
            Files.copy(input, file.toPath());
        } catch (Exception e) {
            PrintWriter respuesta = response.getWriter();
            respuesta.print("Error al copiar la foto");
            System.out.println("Error al copiar la foto");
        }
    }

    return nombreArchivo;
}

Qué ocurre paso a paso

1. Obtener el archivo enviado

Part part = request.getPart("foto");

Part representa el archivo enviado desde el formulario.


2. Obtener el nombre del archivo

Path path = Paths.get(part.getSubmittedFileName());
String fileName = path.getFileName().toString();

Se usa Path para eliminar posibles rutas y quedarse solo con el nombre.


3. Obtener los bytes del archivo

InputStream input = part.getInputStream();

Un InputStream permite leer el contenido del archivo.


4. Crear el archivo destino

File file = new File(uploads, fileName);

Esto crea el archivo dentro de la carpeta img.


5. Copiar el archivo

Files.copy(input, file.toPath());

Copia los bytes desde el InputStream al fichero del servidor.


6. Devolver el nombre del archivo

returnnombreArchivo;

En la base de datos solo guardamos el nombre, no la ruta completa.


Formulario HTML necesario

<form action="BestiasFoto" method="post" enctype="multipart/form-data">

  <input type="hidden" name="opcion" value="1">
  <input type="hidden" name="id" value="0">

  <input type="text" name="nombre" placeholder="Nombre">
  <input type="number" name="tam" placeholder="Tamaño">
  <input type="number" name="alineamiento" placeholder="Alineamiento">
  <textarea name="descripcion"></textarea>

  <input type="file" name="foto" accept="image/*">

  <button type="submit">Guardar</button>

</form>

Mostrar la imagen guardada

Si en BD se guarda dragon.jpg:

<img src="img/dragon.jpg">

Métodos utilizados

HttpServletRequest

MétodoFunción
getParameter()Obtiene valores del formulario
getPart()Obtiene el archivo subido

Part

MétodoFunción
getSubmittedFileName()Nombre original del archivo
getInputStream()Bytes del archivo
getSize()Tamaño del archivo
getContentType()Tipo MIME

Files / Path

MétodoFunción
Files.copy()Copia el archivo al servidor
Paths.get()Crea un Path
getFileName()Obtiene el nombre del archivo

Buenas prácticas

  • Guardar solo el nombre del archivo en BD
  • Validar que el archivo no esté vacío
  • Validar tipo MIME (image/png, image/jpeg…)
  • Evitar nombres duplicados
  • Crear la carpeta si no existe

Tabla de métodos/clases usados

Servlet / Request / Response

ElementoMétodo¿Para qué sirve?
HttpServletRequestgetParameter("x")Lee un campo normal del formulario (texto, hidden, number…)
HttpServletRequestgetPart("foto")Recupera un archivo (o parte) del multipart con ese name
HttpServletResponsesendRedirect("url")Redirige al navegador a otra página tras el POST
HttpServletResponsegetWriter()Permite escribir una respuesta de texto al cliente

Multipart / Part

ElementoMétodo¿Para qué sirve?
PartgetSubmittedFileName()Devuelve el nombre original del fichero enviado
PartgetInputStream()Devuelve un stream con los bytes del archivo
PartgetSize()Tamaño del archivo en bytes (útil para validar)
PartgetContentType()Tipo MIME (ej. image/png)

Files / Path

ElementoMétodo¿Para qué sirve?
Pathsget(String)Crea un Path a partir de una ruta o nombre
PathgetFileName()Devuelve el último segmento (nombre)
Filescopy(InputStream, Path, ...)Copia datos de un stream a un fichero
FilegetAbsolutePath()Ruta absoluta en disco
FilegetName()Nombre del archivo sin ruta

Validaciones en la subida de imágenes (métodos independientes)

  • Validar el nombre del archivo
  • Evitar archivos duplicados
  • Validar tipo MIME
  • Validar extensión
  • Validar tamaño
  • Validar que el archivo sea realmente una imagen
  • Generar nombre seguro (UUID)

Cada método tiene una responsabilidad única.


1. Validar nombre de archivo

Evitar nombres peligrosos, rutas maliciosas o caracteres inválidos.

Método

private String validarNombreArchivo(String originalName) throws ServletException {

    if (originalName == null || originalName.isBlank()) {
        throw new ServletException("El archivo no tiene nombre válido");
    }

    // eliminar rutas potenciales
    String nombre = Paths.get(originalName).getFileName().toString();

    // permitir solo letras, números, punto, guion y guion bajo
    if (!nombre.matches("[a-zA-Z0-9._-]+")) {
        throw new ServletException("Nombre de archivo inválido");
    }

    return nombre;
}

Qué hace

  • Evita nombres vacíos
  • Elimina rutas (C:\algo\malicioso\file.jpg)
  • Bloquea caracteres peligrosos
  • Reduce riesgo de path traversal

2. Validar tipo MIME

Evitar que suban archivos ejecutables disfrazados de imagen.

Método

private void validarTipoMime(Part part) throws ServletException {

    String mime = part.getContentType();

    if (mime == null || !mime.startsWith("image/")) {
        throw new ServletException("Solo se permiten archivos de imagen");
    }
}

Qué hace

  • Comprueba que el archivo es image/*
  • Bloquea .exe, .jsp, .php, etc.

3. Validar extensión

Permitir solo formatos conocidos.

Método

private void validarExtension(String fileName) throws ServletException {

    String ext = "";

    int punto = fileName.lastIndexOf('.');
    if (punto >= 0) {
        ext = fileName.substring(punto + 1).toLowerCase();
    }

    switch (ext) {
        case "jpg":
        case "jpeg":
        case "png":
        case "gif":
        case "webp":
            return;
        default:
            throw new ServletException("Extensión no permitida");
    }
}

Qué hace

  • Extrae la extensión
  • Permite solo imágenes conocidas
  • Evita subir scripts renombrados

4. Validar tamaño del archivo

Evitar archivos demasiado grandes.

Método

private void validarTamano(Part part, long maxBytes) throws ServletException {

    if (part.getSize() == 0) {
        throw new ServletException("El archivo está vacío");
    }

    if (part.getSize() > maxBytes) {
        throw new ServletException("El archivo supera el tamaño permitido");
    }
}

Uso típico

validarTamano(part, 2*1024*1024); // 2 MB

5. Validar que realmente sea imagen

Evitar archivos falsos con extensión .jpg pero que no son imagen.

Método

private void validarContenidoImagen(InputStream input) throws ServletException, IOException {

    BufferedImage img = ImageIO.read(input);

    if (img == null) {
        throw new ServletException("El archivo no es una imagen válida");
    }
}

Qué hace

  • Intenta decodificar la imagen
  • Si falla → no es imagen real

6. Evitar archivos duplicados

No sobrescribir archivos existentes.

Método

private File evitarDuplicado(File carpeta, String fileName) {

    File file = new File(carpeta, fileName);

    if (!file.exists()) {
        return file;
    }

    String nombre = fileName;
    String base = nombre;
    String ext = "";

    int punto = nombre.lastIndexOf('.');
    if (punto >= 0) {
        base = nombre.substring(0, punto);
        ext = nombre.substring(punto);
    }

    int contador = 1;
    File nuevo;

    do {
        nuevo = new File(carpeta, base + "_" + contador + ext);
        contador++;
    } while (nuevo.exists());

    return nuevo;
}

Qué hace

Si ya existe:

dragon.jpg

Genera:

dragon_1.jpg
dragon_2.jpg
dragon_3.jpg

7. Generar nombre seguro (UUID)

Esto penalizaria el SEO si se trata de archivos que los buscadores pudiesen indexar

Evitar colisiones y nombres maliciosos.

Método

private String generarNombreSeguro(String original) {

    String ext = "";

    int punto = original.lastIndexOf('.');
    if (punto >= 0) {
        ext = original.substring(punto);
    }

    return java.util.UUID.randomUUID().toString() + ext;
}

Qué hace

Convierte:

dragon.png

en algo como:

f47ac10b-58cc-4372-a567-0e02b2c3d479.png

8. Crear carpeta si no existe

private void asegurarCarpeta(File carpeta) throws ServletException {

    if (!carpeta.exists()) {
        if (!carpeta.mkdirs()) {
            throw new ServletException("No se pudo crear la carpeta de subida");
        }
    }
}

9. Ejemplo integrando todas las validaciones

Este sería el flujo dentro de subirFoto():

Part part = request.getPart("foto");

validarTipoMime(part);
validarTamano(part, 2 * 1024 * 1024);

String nombreOriginal = validarNombreArchivo(part.getSubmittedFileName());
validarExtension(nombreOriginal);

InputStream input = part.getInputStream();
validarContenidoImagen(input);

// resetear stream tras validación
input = part.getInputStream();

asegurarCarpeta(uploads);

String nombreSeguro = generarNombreSeguro(nombreOriginal);

File destino = evitarDuplicado(uploads, nombreSeguro);

Files.copy(input, destino.toPath());

return destino.getName();


Subir un archivo parece trivial, pero en realidad es una intersección entre E/S, seguridad, validación, arquitectura y persistencia. Cuando el alumno domina esto, deja de “subir fotos” y empieza a construir backend serio.