Introducción a las Aplicaciones Web en Java

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 18 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🌐 ¿Qué son las aplicaciones web en Java?

Una aplicación web en Java es un programa que se ejecuta en un servidor y genera contenido dinámico que se entrega al navegador del usuario a través del protocolo HTTP. A diferencia de las aplicaciones de escritorio (como las construidas con Swing o JavaFX), las aplicaciones web no requieren instalación en el equipo del cliente: basta con un navegador para acceder a ellas.

El ecosistema Java ofrece una de las plataformas más robustas y maduras para el desarrollo web empresarial. Desde los años noventa, tecnologías como los Servlets y las páginas JSP sentaron las bases de lo que hoy conocemos como Jakarta EE (anteriormente J2EE y Java EE), una especificación que define estándares para construir aplicaciones web escalables, seguras y portables.

La idea central es sencilla: el navegador del usuario envía una petición HTTP al servidor; un componente Java (Servlet o JSP) procesa esa petición, consulta bases de datos o servicios externos si es necesario, y devuelve una respuesta — normalmente una página HTML, datos en formato JSON o un documento XML. Todo este proceso se ejecuta dentro de un contenedor web que gestiona el ciclo de vida de los componentes y proporciona servicios esenciales como la seguridad, la concurrencia y la gestión de sesiones.

💡 Dato clave: Según la encuesta de Jakarta EE 2025, el 58% de los desarrolladores empresariales usa Jakarta EE como base para sus aplicaciones web, superando por primera vez a otros frameworks como plataforma principal declarada.

🏗️ Arquitectura de una aplicación web Java

Las aplicaciones web Java siguen típicamente una arquitectura de tres capas que separa responsabilidades de forma clara y facilita el mantenimiento, las pruebas y la escalabilidad del sistema.

📋 Capa de presentación (Vista)

Es la capa que el usuario ve e interactúa. En Java, la capa de presentación se implementa tradicionalmente con páginas JSP, plantillas Thymeleaf, o en aplicaciones modernas, mediante APIs REST que alimentan un frontend JavaScript (React, Angular, Vue). Esta capa se encarga de formatear los datos para su visualización y capturar la entrada del usuario.

⚙️ Capa de lógica de negocio (Controlador / Modelo)

Contiene las reglas del negocio y la lógica de procesamiento. Los Servlets actúan como controladores que reciben las peticiones, invocan la lógica de negocio y deciden qué vista devolver al usuario. En aplicaciones más complejas, esta capa incluye componentes como EJB (Enterprise JavaBeans) o servicios gestionados por CDI (Contexts and Dependency Injection).

🗄️ Capa de acceso a datos (Persistencia)

Gestiona la comunicación con bases de datos y servicios externos. Java ofrece JDBC como API de acceso directo a bases de datos, y JPA (Jakarta Persistence, antes Java Persistence API) como capa de abstracción ORM (Object-Relational Mapping) que permite trabajar con objetos Java en lugar de escribir SQL directamente.

Flujo de una petición web en Java
  [Navegador]
       |
       | (1) Petición HTTP GET /productos
       v
  [Servidor Web / Contenedor]
       |
       | (2) Despacha la petición al Servlet correspondiente
       v
  [ProductoServlet.java]  ← Capa de Control
       |
       | (3) Invoca la lógica de negocio
       v
  [ProductoService.java]  ← Capa de Negocio
       |
       | (4) Consulta la base de datos vía JPA
       v
  [ProductoRepository.java → Base de Datos]  ← Capa de Persistencia
       |
       | (5) Devuelve lista de productos
       v
  [ProductoServlet.java]
       |
       | (6) Reenvía datos a la vista JSP
       v
  [productos.jsp]  ← Capa de Presentación
       |
       | (7) Genera HTML con los datos
       v
  [Navegador muestra la página]

🧩 Componentes web: Servlets y JSP

Los componentes web son los bloques fundamentales de cualquier aplicación web Java. La especificación Jakarta EE define dos tipos principales: los Servlets y las páginas JSP (JavaServer Pages). Aunque ambos pueden generar contenido dinámico, cada uno tiene fortalezas distintas que los hacen más adecuados para diferentes tareas.

🔧 Servlets: el motor de procesamiento

Un Servlet es una clase Java que extiende HttpServlet y procesa peticiones HTTP. Es el componente ideal para la lógica de control: recibe parámetros, valida datos, invoca servicios de negocio y decide qué respuesta enviar. Los Servlets son clases Java puras, lo que facilita las pruebas unitarias y la depuración.

HolaMundoServlet.java — Servlet básico con Jakarta EE
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hola")       // URL que activa este Servlet
public class HolaMundoServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {

        // Configurar la respuesta
        response.setContentType("text/html;charset=UTF-8");

        // Obtener parámetro del usuario (si existe)
        String nombre = request.getParameter("nombre");
        if (nombre == null || nombre.isBlank()) {
            nombre = "Mundo";
        }

        // Generar la respuesta HTML
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html><head><title>Hola</title></head>");
        out.println("<body>");
        out.println("<h1>¡Hola, " + nombre + "!</h1>");
        out.println("<p>Servlet ejecutado a las: " + new java.util.Date() + "</p>");
        out.println("</body></html>");
    }
}
✅ Nota: Observa que en Jakarta EE el paquete es jakarta.servlet, no javax.servlet como en las versiones anteriores (Java EE). Este cambio de namespace se produjo en Jakarta EE 9 y es obligatorio en todas las versiones posteriores.

📄 JSP (JavaServer Pages): la vista dinámica

Una página JSP es un documento basado en texto (normalmente HTML) que permite incrustar código Java y expresiones dinámicas. Su ventaja principal es que resulta mucho más natural para construir interfaces visuales: el diseñador puede trabajar con HTML estándar mientras el programador añade la lógica dinámica con etiquetas especiales.

saludo.jsp — Página JSP con Expression Language (EL)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Saludo</title>
</head>
<body>
    <h1>Bienvenido, ${nombre}</h1>

    <!-- Condicional con JSTL -->
    <c:if test="${not empty productos}">
        <h2>Productos disponibles:</h2>
        <ul>
            <c:forEach var="producto" items="${productos}">
                <li>${producto.nombre} — ${producto.precio} €</li>
            </c:forEach>
        </ul>
    </c:if>

    <c:if test="${empty productos}">
        <p>No hay productos disponibles en este momento.</p>
    </c:if>
</body>
</html>

🔄 Servlets vs. JSP: ¿cuándo usar cada uno?

Criterio Servlet JSP
Mejor para Lógica de control, APIs REST, procesamiento de datos Generar vistas HTML, plantillas con datos dinámicos
Tipo de contenido Cualquiera (HTML, JSON, XML, binario) Principalmente HTML y XML
Facilidad para HTML Incómodo (HTML como strings en Java) Natural (HTML con Java incrustado)
Testabilidad Alta (clase Java estándar) Media (requiere contenedor para compilar)
Patrón MVC Controlador (C) Vista (V)
⚠️ Importante: En proyectos profesionales, la práctica recomendada es separar responsabilidades usando el patrón MVC: los Servlets actúan como controladores (reciben peticiones y coordinan la lógica), las JSP como vistas (presentan datos al usuario), y los POJOs/EJBs como modelo (contienen la lógica de negocio y acceso a datos).

📦 El contenedor web y su función

Los componentes web Java no se ejecutan de forma aislada: necesitan un entorno de ejecución especializado llamado contenedor web (o contenedor de Servlets). El contenedor es el intermediario entre el servidor HTTP y los componentes Java, proporcionando una serie de servicios fundamentales que simplifican enormemente el desarrollo.

🔑 Servicios que proporciona el contenedor

El contenedor web gestiona automáticamente aspectos que serían extremadamente complejos de implementar manualmente. Entre los servicios más importantes se encuentran: el despacho de peticiones, que enruta cada petición HTTP al Servlet correcto según la URL; la gestión del ciclo de vida, que se encarga de crear, inicializar y destruir los Servlets; la gestión de sesiones, que mantiene el estado del usuario entre peticiones HTTP (que son stateless por naturaleza); la seguridad, que implementa autenticación y autorización; la concurrencia, que maneja múltiples peticiones simultáneas mediante hilos (threads); y el acceso a recursos como bases de datos, colas de mensajes y servicios de nombres (JNDI).

Flujo interno del contenedor web
  Petición HTTP del navegador
          │
          ▼
  ┌─────────────────────────────┐
  │     CONTENEDOR WEB          │
  │  (Tomcat, Jetty, etc.)      │
  │                             │
  │  1. Recibe petición HTTP    │
  │  2. Crea objetos Request    │
  │     y Response              │
  │  3. Identifica el Servlet   │
  │     según URL mapping       │
  │  4. Crea/reutiliza hilo     │
  │  5. Invoca doGet()/doPost() │
  │  6. Gestiona sesión HTTP    │
  │  7. Envía respuesta HTTP    │
  │  8. Registra en el log      │
  └─────────────────────────────┘
          │
          ▼
  Respuesta HTML al navegador

🧵 Concurrencia y Virtual Threads

Tradicionalmente, los contenedores web asignan un hilo del sistema operativo por cada petición concurrente. Con miles de usuarios simultáneos, esto puede agotar los recursos del servidor. Jakarta EE 11 introduce soporte oficial para Virtual Threads (hilos virtuales de Java 21), que permiten manejar cientos de miles de peticiones concurrentes con un consumo de memoria mínimo. Esto supone un avance significativo en la escalabilidad de las aplicaciones web Java.

📂 Estructura de un archivo WAR

Para desplegar una aplicación web en un servidor Java, es necesario empaquetarla en un formato estándar llamado WAR (Web Application Archive). Un archivo WAR es esencialmente un archivo ZIP con una estructura de directorios predefinida que el contenedor web sabe interpretar.

Estructura estándar de un archivo WAR
mi-aplicacion.war
│
├── index.html                    ← Página de inicio (recurso estático)
├── css/
│   └── estilos.css               ← Hojas de estilo
├── js/
│   └── app.js                    ← Scripts del cliente
├── img/
│   └── logo.png                  ← Imágenes
│
├── productos.jsp                 ← Página JSP (vista dinámica)
├── login.jsp
│
└── WEB-INF/                      ← Directorio protegido (NO accesible desde el navegador)
    │
    ├── web.xml                   ← Descriptor de despliegue (configuración)
    │
    ├── classes/                   ← Clases Java compiladas (.class)
    │   └── com/
    │       └── miempresa/
    │           ├── servlet/
    │           │   └── ProductoServlet.class
    │           ├── service/
    │           │   └── ProductoService.class
    │           └── model/
    │               └── Producto.class
    │
    └── lib/                      ← Librerías JAR de terceros
        ├── mysql-connector-j-9.0.jar
        └── jstl-3.0.jar

Los elementos clave de esta estructura son los siguientes. El directorio raíz contiene los recursos accesibles desde el navegador: páginas HTML, CSS, JavaScript, imágenes y páginas JSP. El directorio WEB-INF es especial porque el contenedor web impide el acceso directo desde el navegador a su contenido, protegiendo así las clases compiladas, la configuración y las librerías. Dentro de WEB-INF/classes se colocan las clases Java compiladas (Servlets, servicios, modelos) organizadas según su paquete. El directorio WEB-INF/lib contiene las librerías JAR de terceros que la aplicación necesita.

✅ Buena práctica: El directorio WEB-INF es el lugar seguro para almacenar plantillas JSP que solo deben ser accesibles a través de un Servlet controlador (forward), nunca directamente por URL. Esto refuerza el patrón MVC y evita que el usuario evite la validación de seguridad accediendo directamente a una vista.

📦 Creación de un archivo WAR

Existen varias formas de crear un archivo WAR. En proyectos profesionales, lo habitual es usar herramientas de construcción automatizadas:

Tres formas de crear un WAR
// 1. Con Maven (el método más habitual en proyectos profesionales):
//    En pom.xml: <packaging>war</packaging>
$ mvn clean package
//    → Genera: target/mi-aplicacion.war

// 2. Con Gradle:
//    En build.gradle: plugins { id 'war' }
$ gradle war
//    → Genera: build/libs/mi-aplicacion.war

// 3. Manualmente con el comando jar (útil para aprender):
$ cd directorio-raiz-aplicacion
$ jar cvf mi-aplicacion.war .
//    → Empaqueta toda la estructura del directorio actual

📝 El descriptor de despliegue web.xml

El archivo web.xml es el descriptor de despliegue de la aplicación web: un documento XML que configura el comportamiento de los componentes web. Aunque en Jakarta EE moderno muchas configuraciones se realizan mediante anotaciones Java (como @WebServlet), el web.xml sigue siendo importante para configuraciones globales, filtros de seguridad y parámetros de contexto.

web.xml — Descriptor de despliegue de ejemplo
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <!-- Nombre de la aplicación -->
    <display-name>Mi Tienda Online</display-name>

    <!-- Parámetros de contexto (accesibles desde toda la aplicación) -->
    <context-param>
        <param-name>adminEmail</param-name>
        <param-value>admin@mitienda.com</param-value>
    </context-param>

    <!-- Declaración de un Servlet (alternativa a @WebServlet) -->
    <servlet>
        <servlet-name>ProductoServlet</servlet-name>
        <servlet-class>com.miempresa.servlet.ProductoServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Mapeo de URL al Servlet -->
    <servlet-mapping>
        <servlet-name>ProductoServlet</servlet-name>
        <url-pattern>/productos</url-pattern>
    </servlet-mapping>

    <!-- Filtro de codificación UTF-8 -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>com.miempresa.filter.EncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <!-- Página de inicio por defecto -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- Páginas de error personalizadas -->
    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/error/404.jsp</location>
    </error-page>

    <!-- Tiempo de sesión en minutos -->
    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
</web-app>
💡 Anotaciones vs. web.xml: La anotación @WebServlet("/productos") es equivalente al bloque <servlet> + <servlet-mapping> del web.xml. En proyectos modernos se prefieren las anotaciones por su simplicidad, reservando el web.xml para configuraciones que afectan a toda la aplicación (filtros globales, páginas de error, timeouts de sesión).

🔄 Ciclo de vida de una aplicación web

Toda aplicación web Java atraviesa un ciclo de vida definido que va desde su desarrollo hasta su eliminación del servidor. Comprender este ciclo es esencial para escribir código robusto y evitar fugas de recursos.

🛠️ Fase 1: Desarrollo

Se escriben los componentes web (Servlets, JSP), las clases de negocio, las vistas y se configura el descriptor de despliegue. Se organizan según la estructura WAR estándar.

📦 Fase 2: Empaquetado

El código fuente se compila y se empaqueta junto con las librerías y recursos estáticos en un archivo WAR mediante Maven, Gradle o manualmente.

🚀 Fase 3: Despliegue

El archivo WAR se coloca en el directorio de despliegue del servidor (por ejemplo, webapps/ en Tomcat). El contenedor lo detecta, lo descomprime y registra los Servlets y filtros definidos.

⚡ Fase 4: Ejecución (ciclo del Servlet)

Cuando llega la primera petición para un Servlet, el contenedor ejecuta su método init() una sola vez. Para cada petición posterior, invoca service() que delega en doGet(), doPost(), doPut() o doDelete() según el método HTTP. Cuando el servidor se apaga o la aplicación se elimina, se invoca destroy() para liberar recursos.

Ciclo de vida de un Servlet
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/ciclo-vida")
public class CicloVidaServlet extends HttpServlet {

    // (1) INICIALIZACIÓN — Se ejecuta UNA sola vez al cargar el Servlet
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        System.out.println("✅ Servlet inicializado. "
            + "Aquí se abren conexiones a BD, se cargan configuraciones...");
    }

    // (2) SERVICIO — Se ejecuta en CADA petición (un hilo por petición)
    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().println("<h1>Petición procesada</h1>");
    }

    // (3) DESTRUCCIÓN — Se ejecuta UNA sola vez al apagar el servidor
    @Override
    public void destroy() {
        System.out.println("❌ Servlet destruido. "
            + "Aquí se cierran conexiones, se liberan recursos...");
    }
}

🗑️ Fase 5: Eliminación

Cuando el administrador retira la aplicación del servidor (undeploy) o apaga el servidor, el contenedor invoca destroy() en todos los Servlets activos y libera los recursos asociados.

🖥️ Servidores de aplicaciones Java

Para ejecutar aplicaciones web Java necesitamos un servidor que incluya un contenedor web. Existen dos categorías principales: los contenedores de Servlets (ligeros, solo implementan la parte web de la especificación) y los servidores de aplicaciones completos (implementan toda la especificación Jakarta EE, incluyendo EJB, JMS, JTA, etc.).

Servidor Tipo Licencia Uso recomendado
Apache Tomcat Contenedor de Servlets Apache 2.0 (libre) Aprendizaje, proyectos web, microservicios
Eclipse Jetty Contenedor de Servlets Apache 2.0 / EPL (libre) Aplicaciones embebidas, alta concurrencia
WildFly Servidor completo Jakarta EE LGPL (libre) Aplicaciones empresariales completas
Eclipse GlassFish Servidor de referencia Jakarta EE EPL (libre) Implementación de referencia, pruebas de especificación
Payara Server Servidor completo Jakarta EE CDDL / Comercial Producción empresarial con soporte
Open Liberty (IBM) Servidor completo Jakarta EE EPL (libre) Cloud-native, microservicios, configuración modular
✅ Recomendación para empezar: Si estás comenzando con aplicaciones web Java, Apache Tomcat es la elección ideal. Es ligero, fácil de instalar, tiene excelente documentación y es suficiente para la mayoría de proyectos web. Solo necesitarás un servidor completo como WildFly o GlassFish si tu aplicación requiere EJB, JMS u otras especificaciones empresariales avanzadas.

📜 De J2EE a Jakarta EE: la evolución

La plataforma empresarial Java ha experimentado una transformación profunda a lo largo de más de dos décadas. Entender esta evolución es importante porque en la documentación, tutoriales y código existente encontrarás referencias a los tres nombres que ha tenido la plataforma.

📅 Línea temporal de la plataforma

Período Nombre Propietario Hito destacado
1999–2006 J2EE (Java 2 Enterprise Edition) Sun Microsystems Nace la plataforma: Servlets, JSP, EJB, JDBC
2006–2017 Java EE (Java Platform, Enterprise Edition) Oracle (adquirió Sun en 2010) CDI, JAX-RS, WebSocket, JSON-P
2018–presente Jakarta EE Eclipse Foundation (open source) Namespace jakarta.*, Jakarta EE 11, Virtual Threads

🔄 El cambio clave: de javax a jakarta

El cambio más visible para los desarrolladores fue la migración del namespace de paquetes. Todo el código que antes usaba javax.servlet, javax.persistence, etc., ahora utiliza jakarta.servlet, jakarta.persistence, etc. Este cambio se introdujo en Jakarta EE 9 (2020) y es obligatorio desde entonces.

Cambio de namespace: javax → jakarta
// ❌ Código antiguo (Java EE / J2EE)
import javax.servlet.http.HttpServlet;
import javax.persistence.Entity;
import javax.inject.Inject;

// ✅ Código actual (Jakarta EE 9+)
import jakarta.servlet.http.HttpServlet;
import jakarta.persistence.Entity;
import jakarta.inject.Inject;

🚀 Jakarta EE 11: el estado actual (2025)

La versión más reciente de la plataforma es Jakarta EE 11, lanzada en junio de 2025. Entre sus novedades más relevantes se encuentran: soporte para Java 17 y 21 como versiones mínima y recomendada respectivamente; la nueva especificación Jakarta Data para simplificar el acceso a datos; soporte nativo para Virtual Threads en Jakarta Concurrency; mejoras en CDI como modelo de programación central; y la eliminación de especificaciones obsoletas como Managed Beans. Jakarta EE 12 está ya en desarrollo con fecha prevista para finales de 2026.

🎯 Ejemplo integrador: aplicación web completa

Vamos a construir paso a paso una pequeña aplicación web de gestión de tareas que ilustra todos los conceptos estudiados. La aplicación sigue el patrón MVC con un Servlet como controlador, una JSP como vista y una clase Java como modelo.

📋 Paso 1: El modelo (Tarea.java)

Tarea.java — Clase modelo (POJO)
package com.miempresa.model;

import java.time.LocalDate;

/**
 * Representa una tarea en el gestor de tareas.
 * Ejemplo de POJO (Plain Old Java Object) como modelo de datos.
 */
public class Tarea {

    private long id;
    private String titulo;
    private String descripcion;
    private boolean completada;
    private LocalDate fechaCreacion;

    // Constructor
    public Tarea(long id, String titulo, String descripcion) {
        this.id = id;
        this.titulo = titulo;
        this.descripcion = descripcion;
        this.completada = false;
        this.fechaCreacion = LocalDate.now();
    }

    // Getters y setters
    public long getId()                    { return id; }
    public String getTitulo()              { return titulo; }
    public String getDescripcion()         { return descripcion; }
    public boolean isCompletada()          { return completada; }
    public LocalDate getFechaCreacion()    { return fechaCreacion; }
    public void setCompletada(boolean c)   { this.completada = c; }

    @Override
    public String toString() {
        return String.format("Tarea[id=%d, titulo='%s', completada=%b]",
            id, titulo, completada);
    }
}

⚙️ Paso 2: El controlador (TareaServlet.java)

TareaServlet.java — Controlador MVC con Servlet
package com.miempresa.servlet;

import com.miempresa.model.Tarea;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Servlet controlador para la gestión de tareas.
 * Maneja peticiones GET (listar) y POST (crear nueva tarea).
 */
@WebServlet("/tareas")
public class TareaServlet extends HttpServlet {

    // Almacén temporal en memoria (en producción sería una BD)
    private final List<Tarea> tareas = new ArrayList<>();
    private long siguienteId = 1;

    @Override
    public void init() throws ServletException {
        // Datos de ejemplo al iniciar
        tareas.add(new Tarea(siguienteId++, "Estudiar Servlets",
            "Leer la documentación oficial de Jakarta Servlet"));
        tareas.add(new Tarea(siguienteId++, "Configurar Tomcat",
            "Instalar Apache Tomcat 11 y desplegar el primer WAR"));
        tareas.add(new Tarea(siguienteId++, "Crear proyecto Maven",
            "Configurar pom.xml con dependencias de Jakarta EE 11"));
    }

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
                         throws ServletException, IOException {
        // Pasar la lista de tareas a la vista JSP
        request.setAttribute("tareas", tareas);
        request.setAttribute("totalTareas", tareas.size());
        request.setAttribute("completadas",
            tareas.stream().filter(Tarea::isCompletada).count());

        // Forward a la vista (la JSP no es accesible directamente)
        request.getRequestDispatcher("/WEB-INF/vistas/tareas.jsp")
               .forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
                          throws IOException {
        request.setCharacterEncoding("UTF-8");

        String titulo = request.getParameter("titulo");
        String descripcion = request.getParameter("descripcion");

        if (titulo != null && !titulo.isBlank()) {
            tareas.add(new Tarea(siguienteId++,
                titulo.strip(), descripcion != null ? descripcion.strip() : ""));
        }

        // Patrón POST-Redirect-GET para evitar reenvío del formulario
        response.sendRedirect(request.getContextPath() + "/tareas");
    }
}

🖼️ Paso 3: La vista (tareas.jsp)

WEB-INF/vistas/tareas.jsp — Vista con JSTL y EL
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Gestor de Tareas</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; max-width: 700px; margin: 40px auto; }
        .stats { background: #e8f4fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
        table { width: 100%; border-collapse: collapse; }
        th { background: #0b459f; color: white; padding: 10px; text-align: left; }
        td { padding: 8px; border-bottom: 1px solid #ddd; }
        .completada { text-decoration: line-through; color: #888; }
        form { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-top: 20px; }
        input, textarea { width: 100%; padding: 8px; margin: 5px 0 15px 0; box-sizing: border-box; }
        button { background: #0b459f; color: white; padding: 10px 20px; border: none;
                 border-radius: 4px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>📋 Gestor de Tareas</h1>

    <div class="stats">
        <strong>Total:</strong> ${totalTareas} tareas |
        <strong>Completadas:</strong> ${completadas}
    </div>

    <table>
        <thead>
            <tr><th>ID</th><th>Título</th><th>Descripción</th><th>Estado</th></tr>
        </thead>
        <tbody>
            <c:forEach var="tarea" items="${tareas}">
                <tr class="${tarea.completada ? 'completada' : ''}">
                    <td>${tarea.id}</td>
                    <td>${tarea.titulo}</td>
                    <td>${tarea.descripcion}</td>
                    <td>${tarea.completada ? '✅ Hecha' : '⏳ Pendiente'}</td>
                </tr>
            </c:forEach>
        </tbody>
    </table>

    <form method="post" action="tareas">
        <h3>Añadir nueva tarea</h3>
        <label>Título:</label>
        <input type="text" name="titulo" required>
        <label>Descripción:</label>
        <textarea name="descripcion" rows="3"></textarea>
        <button type="submit">Crear tarea</button>
    </form>
</body>
</html>
💡 Patrón PRG (Post-Redirect-Get): Observa cómo el método doPost() termina con sendRedirect() en lugar de hacer forward directo a la JSP. Esto evita que al refrescar la página el navegador reenvíe el formulario y cree tareas duplicadas. Es una buena práctica fundamental en desarrollo web.

⚠️ Errores comunes y buenas prácticas

❌ Errores frecuentes en principiantes

1. Poner lógica de negocio en JSP: Las páginas JSP deben contener solo código de presentación. Toda la lógica (validaciones, cálculos, acceso a datos) debe estar en Servlets o clases de servicio. Mezclar HTML con lógica Java produce código imposible de mantener.

2. No usar el patrón PRG (Post-Redirect-Get): Después de procesar un formulario con doPost(), siempre redirigir con sendRedirect(). Si se hace un forward directo, al refrescar la página el navegador reenviará el formulario.

3. Almacenar estado en variables de instancia del Servlet: Los Servlets son compartidos entre todos los usuarios (solo existe una instancia). Guardar datos de usuario en campos de instancia causa errores de concurrencia graves. Usa HttpSession para datos por usuario o ServletContext para datos globales.

4. No configurar la codificación UTF-8: Si no se establece la codificación tanto en la petición (request.setCharacterEncoding("UTF-8")) como en la respuesta (response.setContentType("text/html;charset=UTF-8")), los caracteres especiales como acentos y eñes se mostrarán corruptos.

5. Acceder a JSP directamente por URL: Las JSP que forman parte del patrón MVC deben colocarse dentro de WEB-INF/ para que solo sean accesibles mediante forward desde un Servlet. Ponerlas en la raíz del WAR permite saltarse la validación del controlador.

✅ Buenas prácticas profesionales

Separar responsabilidades (MVC): Mantén los Servlets como controladores ligeros que coordinan el flujo, las JSP como vistas sin lógica, y los POJOs/servicios como lógica de negocio. Esta separación facilita las pruebas, el mantenimiento y la escalabilidad.

Usar filtros para tareas transversales: La codificación de caracteres, la autenticación, el logging y la compresión se implementan mejor como Filter en lugar de repetir código en cada Servlet.

Externalizar la configuración: Los parámetros que pueden cambiar entre entornos (URLs de base de datos, credenciales, modos de operación) deben ir en el web.xml como context-param o en archivos de propiedades externos, nunca hardcodeados en el código.

Gestionar recursos con try-with-resources: Las conexiones a base de datos, flujos de entrada/salida y otros recursos deben cerrarse siempre. Usa la sintaxis try-with-resources de Java para garantizar que se liberan incluso si ocurre una excepción.

✏️ Ejercicios prácticos resueltos

Ejercicio 1: Servlet de calculadora

Crea un Servlet que reciba dos números y una operación (suma, resta, multiplicación, división) como parámetros de la URL y devuelva el resultado en formato HTML. Debe validar que los parámetros existan y que no se divida entre cero.

Ver solución
CalculadoraServlet.java
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/calculadora")
public class CalculadoraServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp) throws IOException {

        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        try {
            double a = Double.parseDouble(req.getParameter("a"));
            double b = Double.parseDouble(req.getParameter("b"));
            String op = req.getParameter("op");

            double resultado = switch (op) {
                case "suma"   -> a + b;
                case "resta"  -> a - b;
                case "mult"   -> a * b;
                case "div"    -> {
                    if (b == 0) throw new ArithmeticException("División entre cero");
                    yield a / b;
                }
                default -> throw new IllegalArgumentException("Operación no válida: " + op);
            };

            out.printf("<h1>Resultado: %.2f</h1>", resultado);
            out.printf("<p>%.2f %s %.2f = %.2f</p>", a, op, b, resultado);

        } catch (NumberFormatException e) {
            resp.setStatus(400);
            out.println("<h1>Error: parámetros numéricos inválidos</h1>");
        } catch (ArithmeticException | IllegalArgumentException e) {
            resp.setStatus(400);
            out.println("<h1>Error: " + e.getMessage() + "</h1>");
        }
    }
}
// Uso: /calculadora?a=10&b=3&op=suma → Resultado: 13.00

Ejercicio 2: Filtro de logging

Implementa un Filter de Jakarta EE que registre en el log del servidor la URL, el método HTTP y el tiempo de respuesta (en milisegundos) de cada petición que pase por la aplicación.

Ver solución
LoggingFilter.java
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.logging.Logger;

@WebFilter("/*")   // Se aplica a TODAS las URLs de la aplicación
public class LoggingFilter implements Filter {

    private static final Logger log = Logger.getLogger(LoggingFilter.class.getName());

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest) request;
        long inicio = System.currentTimeMillis();

        // Dejar pasar la petición al siguiente filtro o al Servlet
        chain.doFilter(request, response);

        // Medir el tiempo DESPUÉS de procesar
        long duracion = System.currentTimeMillis() - inicio;

        log.info(String.format("[%s] %s %s — %d ms",
            httpReq.getMethod(),
            httpReq.getRequestURI(),
            httpReq.getQueryString() != null ? "?" + httpReq.getQueryString() : "",
            duracion));
    }
}
// Salida en log: [GET] /tareas ?filtro=pendientes — 23 ms

Ejercicio 3: Contador de visitas con sesión

Crea un Servlet que cuente el número de visitas del usuario utilizando HttpSession. Muestra el contador y la fecha de la primera visita. Incluye un botón para reiniciar el contador (invalidar la sesión).

Ver solución
ContadorVisitasServlet.java
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@WebServlet("/visitas")
public class ContadorVisitasServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp) throws IOException {

        // Verificar si se pidió reiniciar
        if ("reset".equals(req.getParameter("accion"))) {
            req.getSession().invalidate();
            resp.sendRedirect(req.getContextPath() + "/visitas");
            return;
        }

        HttpSession sesion = req.getSession(true);

        // Incrementar contador (o inicializar si es nueva sesión)
        Integer visitas = (Integer) sesion.getAttribute("contador");
        if (visitas == null) {
            visitas = 0;
            sesion.setAttribute("primeraVisita",
                LocalDateTime.now().format(
                    DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")));
        }
        visitas++;
        sesion.setAttribute("contador", visitas);

        // Generar respuesta
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<h1>Contador de visitas</h1>");
        out.printf("<p>Has visitado esta página <strong>%d</strong> veces.</p>%n", visitas);
        out.printf("<p>Primera visita: %s</p>%n", sesion.getAttribute("primeraVisita"));
        out.printf("<p>ID de sesión: %s</p>%n", sesion.getId());
        out.println("<a href='visitas?accion=reset'>Reiniciar contador</a>");
    }
}

Ejercicio 4: Estructura WAR de un proyecto

Dado un proyecto de tienda online con: un Servlet para productos, otro para el carrito de compras, dos JSP para las vistas, un filtro de autenticación, una librería de conexión a MySQL y una hoja de estilos CSS, dibuja la estructura completa del archivo WAR siguiendo las convenciones estándar.

Ver solución
Estructura WAR de la tienda online
tienda-online.war
│
├── index.html                        ← Página de inicio estática
├── css/
│   └── tienda.css                    ← Hoja de estilos
├── img/
│   └── logo.png                      ← Recursos estáticos
│
└── WEB-INF/                          ← Protegido del acceso directo
    │
    ├── web.xml                       ← Descriptor de despliegue
    │                                    (filtro de autenticación configurado aquí)
    │
    ├── vistas/                       ← JSP dentro de WEB-INF (solo accesibles via forward)
    │   ├── productos.jsp             ← Vista de catálogo de productos
    │   └── carrito.jsp               ← Vista del carrito de compras
    │
    ├── classes/                       ← Clases compiladas
    │   └── com/
    │       └── tienda/
    │           ├── servlet/
    │           │   ├── ProductoServlet.class
    │           │   └── CarritoServlet.class
    │           ├── filter/
    │           │   └── AuthFilter.class        ← Filtro de autenticación
    │           ├── service/
    │           │   ├── ProductoService.class
    │           │   └── CarritoService.class
    │           └── model/
    │               ├── Producto.class
    │               └── ItemCarrito.class
    │
    └── lib/                          ← Librerías JAR externas
        └── mysql-connector-j-9.0.jar ← Driver MySQL

Razones de diseño:
• Las JSP están dentro de WEB-INF/vistas/ → solo accesibles por forward desde Servlets
• El filtro AuthFilter intercepta las URL protegidas (configurado en web.xml)
• Las clases siguen una estructura de paquetes clara: servlet, filter, service, model
• Solo index.html, CSS e imágenes son directamente accesibles desde el navegador

❓ Preguntas frecuentes sobre Introducción a las Aplicaciones Web en Java

Las dudas más comunes respondidas de forma clara y directa.

Una aplicación web en Java es un programa que se ejecuta en un servidor web y genera contenido dinámico (HTML, JSON, XML) que se envía al navegador del usuario. Utiliza tecnologías como Servlets, JSP y frameworks basados en Jakarta EE para procesar peticiones HTTP, acceder a bases de datos y construir interfaces interactivas.
Un Servlet es una clase Java pura que procesa peticiones HTTP mediante código Java, ideal para lógica de control y procesamiento de datos. Una página JSP (JavaServer Pages) es un documento basado en texto (similar a HTML) que permite incrustar código Java, más adecuado para generar vistas con contenido estático mezclado con datos dinámicos. Internamente, las JSP se compilan a Servlets.
Un contenedor web (también llamado contenedor de Servlets) es el entorno de ejecución que gestiona el ciclo de vida de los componentes web. Proporciona servicios como despacho de peticiones, gestión de sesiones, seguridad, concurrencia y acceso a recursos del servidor. Apache Tomcat es el contenedor web de referencia más utilizado.
Un archivo WAR (Web Application Archive) es un formato de empaquetado estándar para aplicaciones web Java. Es esencialmente un archivo ZIP con una estructura de directorios específica que contiene Servlets compilados, páginas JSP, librerías JAR, archivos estáticos (HTML, CSS, imágenes) y el descriptor de despliegue web.xml. Se despliega directamente en un servidor de aplicaciones.
Son tres nombres para la misma plataforma en diferentes etapas de su evolución. J2EE (Java 2 Enterprise Edition) fue el nombre original de 1999 a 2006. Java EE (Java Platform, Enterprise Edition) fue la denominación de 2006 a 2017. Jakarta EE es el nombre actual desde 2018, cuando Oracle transfirió el proyecto a la Eclipse Foundation. La versión más reciente es Jakarta EE 11 (2025).
Sí, es posible crear aplicaciones web usando solo la API de Servlets y JSP que proporciona Jakarta EE. De hecho, entender estas tecnologías base es fundamental antes de usar frameworks como Spring Boot, Quarkus o Jakarta MVC. Los frameworks simplifican el desarrollo pero se construyen sobre estos mismos componentes web estándar.
Apache Tomcat es la opción más recomendada para principiantes por su ligereza, amplia documentación y facilidad de configuración. Soporta Servlets, JSP y WebSockets. Para aplicaciones que requieran la especificación Jakarta EE completa (EJB, JMS, JTA), servidores como WildFly, GlassFish o Payara son las opciones habituales.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Introducción a las Aplicaciones Web en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!