Saltar al contenido

Servlets y persistencia

Cómo se atiende una petición dentro de un servidor Jakarta EE: el servlet y su ciclo de vida, la sesión de usuario, los filtros, cómo se empaqueta y se despliega la aplicación, y cómo llegan sus datos a la base de datos con JDBC y con JPA.

El servlet y su ciclo de vida

El servlet es la pieza central del contenedor web. Es una clase Java que escribe el programador y que el servidor asocia a una URL: cuando llega una petición a esa dirección, el contenedor llama al servlet, y este encamina el trabajo hacia la capa de negocio y devuelve una respuesta. Dicho de otro modo, es la pasarela entre el mundo HTTP y el código de la aplicación.

Su ciclo de vida lo gobierna el contenedor por completo, y son tres métodos que hay que saberse en orden.

  1. init(config): se llama una sola vez, cuando el contenedor crea la instancia del servlet. Es donde se prepara lo que va a durar toda la vida del componente.
  2. service(request, response): se llama en cada petición. Mira el verbo HTTP y deriva la llamada al método correspondiente: doGet para GET, doPost para POST, y así con el resto.
  3. destroy(): se llama una sola vez, cuando la aplicación o el servidor se detienen, para liberar lo que se hubiera reservado.

De ahí sale una consecuencia que se pregunta mucho: hay una única instancia de cada servlet atendiendo a todos los usuarios a la vez, cada uno en su hilo. Por eso un atributo de instancia en un servlet es una fuente de errores, mientras que las variables locales del método, que son privadas de cada hilo, son seguras.

java
@WebServlet("/simulacro")
public class SimulacroServlet extends HttpServlet {

    // NO: un atributo así lo comparten todas las peticiones
    // private Intento intentoEnCurso;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        String slugTema = req.getParameter("tema");   // viene de la URL
        Intento intento = servicio.nuevoIntento(slugTema);
        req.getSession().setAttribute("intento", intento);
        res.sendRedirect("/simulacro/pregunta/1");
    }
}

Un servlet no es el patrón Front Controller. Es un componente; Front Controller es un patrón de diseño que consiste en canalizar todas las peticiones por un único punto de entrada, y suele implementarse con un servlet, pero ni todo servlet lo aplica ni el patrón obliga a usar servlets.

Para el examen

  • init(): una vez, al crearlo

  • service(): en cada petición; deriva a doGet o doPost

  • destroy(): al parar

  • Cuántas instancias hay: UNA sola, atendiendo a todos en hilos distintos

  • Consecuencia: los atributos de instancia no son seguros

La sesión de usuario y el flujo de una aplicación web

HTTP no tiene estado: cada petición llega como si fuera la primera y el servidor no recuerda nada del que la envía. Para poder hablar de «el usuario que está haciendo el test» hace falta añadir ese recuerdo por encima del protocolo, y la forma estándar de hacerlo es la sesión.

La primera vez que alguien entra, el servidor crea una sesión, le asigna un identificador y se lo devuelve al navegador en una cookie que en el mundo Java se llama JSESSIONID. A partir de ahí el navegador la envía en cada petición y el servidor sabe a qué sesión corresponde. La cookie no lleva datos del usuario: solo lleva la llave con la que el servidor encuentra sus datos.

Entender qué objeto se crea cuándo es lo que evita los errores de concurrencia y de fugas de memoria. Son cuatro alcances distintos y se preguntan comparados.

ObjetoCuándo se creaAlcance
Petición y respuestaEn cada petición, junto con el hilo que la atiendeSe descartan al terminar de responder
Sesión (HttpSession)La primera vez que se conecta un usuarioVive mientras dure la sesión de ese usuario y solo la ve él
Servlets y filtrosAl arrancar la aplicación, un objeto por claseCompartidos por todas las peticiones a la vez
Contexto de aplicación (ServletContext)Al arrancar la aplicaciónUno solo, global, compartido por todos los componentes

Todo lo que se guarda en sesión ocupa memoria del servidor mientras el usuario siga ahí, y multiplicado por cada usuario conectado. Por eso en sesión va lo mínimo, y el resto se recupera de la base de datos cuando hace falta.

Para el examen

  • Problema de fondo: HTTP no tiene estado

  • Cómo se sostiene la sesión: con la cookie JSESSIONID, que solo lleva la llave

  • Los cuatro alcances: petición, sesión, servlet y contexto de aplicación

Filtros, petición y respuesta en detalle

Un filtro es un componente que se interpone entre la petición y el servlet. Los filtros se encadenan uno detrás de otro y cada uno se ocupa de una sola cosa: comprobar la autenticación, fijar la codificación de caracteres, comprimir la respuesta, registrar el tiempo de proceso. Cada filtro decide si deja pasar la petición al siguiente eslabón, si la modifica o si la corta ahí mismo. Ese diseño es el patrón de cadena de responsabilidad, y su ventaja es que se añade o se quita un filtro sin tocar los servlets.

La API de servlets separa cada concepto en dos niveles: una interfaz general y otra específica de HTTP. Se hizo así porque en su día se pensó que podría haber servlets sobre otros protocolos. Nunca ocurrió, pero la separación se quedó, y en un test la pareja general y específica es exactamente lo que se pregunta.

ConceptoInterfaz generalVersión HTTP
ComponenteServlet, GenericServletHttpServlet
PeticiónServletRequestHttpServletRequest
RespuestaServletResponseHttpServletResponse

En la práctica no se implementa la interfaz Servlet: se hereda de HttpServlet, que es una clase de utilidad que ya reparte el trabajo por verbo y deja solo el hueco de doGet, doPost y compañía. De la petición se leen los parámetros de la URL o del formulario y las cabeceras, por ejemplo la de autenticación. En la respuesta se añaden cookies, se envían códigos de error de la familia 400 y 500, o se manda al navegador a otra dirección con una redirección, que viaja con un código de la familia 300.

La asociación entre una clase y la URL a la que responde se puede declarar de dos formas: en el descriptor de despliegue, el fichero web.xml que vive bajo WEB-INF, o directamente sobre la clase con una anotación. La anotación es hoy lo habitual y evita mantener un fichero aparte; el descriptor sigue siendo útil cuando se quiere cambiar el mapeo sin recompilar.

xml
<!-- src/main/webapp/WEB-INF/web.xml: la forma clásica -->
<servlet>
  <servlet-name>SimulacroServlet</servlet-name>
  <servlet-class>com.llegandoalcorte.web.SimulacroServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>SimulacroServlet</servlet-name>
  <url-pattern>/simulacro</url-pattern>
</servlet-mapping>

<!-- equivale a anotar la clase con @WebServlet("/simulacro") -->

Para el examen

  • Dónde van los filtros: encadenados delante de los servlets

  • Qué patrón siguen: cadena de responsabilidad

  • Parejas general y HTTP: Servlet y HttpServlet, ServletRequest y HttpServletRequest

  • Dónde se declara el mapeo: en web.xml o por anotación

Empaquetado y despliegue

Una aplicación Jakarta EE no se copia como un montón de ficheros sueltos: se entrega empaquetada en un único archivo comprimido, con una estructura fijada por la especificación, y se despliega en el servidor. Hay tres formatos y se distinguen por lo que contienen.

FormatoQué empaqueta
.jarClases Java. Es el formato de una biblioteca y también el de un módulo de componentes de negocio
.warUn módulo web: páginas, recursos estáticos, clases y bibliotecas propias, más el descriptor bajo WEB-INF
.earLa aplicación empresarial completa: reúne los módulos web y los de negocio en un solo artefacto

La suma que hay que recordar: un .ear es la unión de sus .war y sus .jar. Los componentes de negocio viajan en .jar, la parte web en .war, y el .ear los agrupa.

Al desplegar, el nombre del archivo determina por defecto la primera parte de la URL de la aplicación. Como ese nombre no siempre es el que interesa publicar, la ruta se puede fijar aparte en la configuración del despliegue, y así un módulo llamado admin.war puede atender en una dirección distinta de «admin».

La configuración de los componentes de negocio también tuvo su descriptor XML en su momento, pero está en desuso: hoy un bean de sesión se declara anotando la clase, y el fichero solo aparece en aplicaciones antiguas.

Para el examen

  • .jar: clases y componentes de negocio

  • .war: el módulo web, con WEB-INF

  • .ear: la aplicación completa: la unión de sus .war y sus .jar

Persistencia: de JDBC al ORM con JPA

Guardar los datos de una aplicación orientada a objetos en una base de datos relacional obliga a traducir entre dos mundos que no encajan: en uno hay objetos con referencias entre sí, en el otro hay tablas con filas y claves ajenas. Hay dos maneras de resolverlo y las dos entran en el temario.

JDBCJPA (ORM)
NivelBajo: se trabaja con SQL y con tablasAlto: se trabaja con objetos
Quién escribe el SQLEl programador, sentencia a sentenciaLa biblioteca, a partir de las clases y sus anotaciones
Qué se manejaConexiones, sentencias y un cursor de resultadosEntidades: un objeto se corresponde con una fila
Dónde viveEn Java SE, en el paquete java.sqlEn Jakarta EE, sobre una implementación como Hibernate

ORM significa correspondencia entre objetos y relaciones (Object Relational Mapping). La biblioteca inspecciona las clases en tiempo de ejecución y, siguiendo un convenio de nombres entre clases y tablas y entre atributos y columnas, construye ella misma las sentencias. La aplicación le entrega objetos de negocio y ella se encarga del resto.

Aquí hay una distinción que cae con frecuencia: JPA es la especificación, es decir, la API y sus reglas; quien la implementa son productos concretos, y los tres nombres que hay que reconocer son Hibernate, EclipseLink (heredero de TopLink) y OpenJPA. Se programa contra la especificación, y el producto se elige al desplegar.

Para consultar, JPA no usa SQL sino JPQL, un lenguaje con la misma forma pero que nombra clases y atributos en lugar de tablas y columnas. Cuando hace falta una sentencia que JPQL no cubre, se puede lanzar una consulta nativa, que es SQL puro y por tanto atada al gestor concreto.

sql
-- SQL: habla de tablas y columnas
SELECT i.* FROM intento i
  JOIN alumno a ON a.id = i.alumno_id
 WHERE a.modalidad = 'EXPRESS';

-- JPQL: habla de clases y atributos
SELECT i FROM Intento i
 WHERE i.alumno.modalidad = :modalidad

El último concepto imprescindible es cuándo se traen los datos relacionados. Con carga temprana (EAGER), al recuperar un objeto se trae también lo que cuelga de él; es cómodo, pero una sola consulta puede acabar arrastrando media base de datos. Con carga perezosa (LAZY) solo se trae lo pedido, y lo relacionado se busca si se llega a usar. Las reglas por defecto de la especificación son fáciles de recordar si se piensa en el volumen: lo que apunta a un único objeto (@ManyToOne y @OneToOne) es EAGER, y lo que apunta a una colección (@OneToMany y @ManyToMany) es LAZY.

Para el examen

  • JDBC: bajo nivel: SQL a mano, en Java SE

  • JPA: el ORM de alto nivel; es una especificación

  • Implementaciones de JPA: Hibernate, EclipseLink y OpenJPA

  • JPQL: consulta nombrando clases y atributos, no tablas

  • Carga por defecto: EAGER hacia un único objeto y LAZY hacia una colección

JPA en detalle: configuración, EntityManager y anotaciones

La API de persistencia vive en el paquete jakarta.persistence y se configura en un fichero propio, el persistence.xml, donde se declara la unidad de persistencia y el origen de datos que va a usar. El origen de datos no se define en la aplicación: lo crea y lo administra el servidor, y la aplicación se limita a nombrarlo. Así, cambiar de base de datos es cuestión de configuración del servidor y no de recompilar.

El objeto con el que se trabaja es el gestor de entidades (EntityManager). Sus métodos principales conviene tenerlos con una frase cada uno, porque el examen pregunta exactamente eso.

MétodoQué hace
persistDa de alta una entidad nueva
findRecupera una entidad por su clave primaria
mergeVuelve a poner bajo control del gestor un objeto que estaba fuera
removeDa de baja la entidad
containsIndica si esa entidad está gestionada en este momento
flushFuerza a que los cambios pendientes se lancen ya contra la base de datos

La correspondencia entre la clase y la tabla se declara con anotaciones sobre la propia clase: cuáles marcan la entidad y su tabla, cuál es la clave primaria, a qué columna va cada atributo, qué atributos no se guardan, cómo se relacionan unas entidades con otras, qué consultas quedan definidas con nombre, y qué métodos se ejecutan justo antes de guardar o de actualizar.

java
@Entity
@Table(name = "intento")
public class Intento {

    @Id
    private Long id;

    @Column(name = "slug_subtema")
    private String slugSubtema;

    // una colección: por defecto se carga de forma perezosa
    @OneToMany(mappedBy = "intento")
    private List<Respuesta> respuestas;

    // calculado al vuelo: no se guarda en ninguna columna
    @Transient
    private int porcentajeAciertos;

    @PrePersist
    void antesDeGuardar() { this.fecha = LocalDateTime.now(); }
}

Por debajo de todo esto sigue estando JDBC. La cadena completa es: el servidor publica un origen de datos, el origen entrega una conexión, sobre la conexión se prepara una sentencia y el resultado se recorre con un cursor. Cada fabricante de base de datos aporta su controlador, que es simplemente un .jar más en el servidor.

Para el examen

  • persist y find: dar de alta y recuperar por clave

  • merge y remove: reincorporar y borrar

  • flush: fuerza el volcado a la base de datos

  • Dónde va la configuración: en persistence.xml; el origen de datos lo crea el servidor