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.
- 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.
- 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.
- 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.
@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.
| Objeto | Cuándo se crea | Alcance |
|---|---|---|
| Petición y respuesta | En cada petición, junto con el hilo que la atiende | Se descartan al terminar de responder |
| Sesión (HttpSession) | La primera vez que se conecta un usuario | Vive mientras dure la sesión de ese usuario y solo la ve él |
| Servlets y filtros | Al arrancar la aplicación, un objeto por clase | Compartidos por todas las peticiones a la vez |
| Contexto de aplicación (ServletContext) | Al arrancar la aplicación | Uno 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.
| Concepto | Interfaz general | Versión HTTP |
|---|---|---|
| Componente | Servlet, GenericServlet | HttpServlet |
| Petición | ServletRequest | HttpServletRequest |
| Respuesta | ServletResponse | HttpServletResponse |
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.
<!-- 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.
| Formato | Qué empaqueta |
|---|---|
| .jar | Clases Java. Es el formato de una biblioteca y también el de un módulo de componentes de negocio |
| .war | Un módulo web: páginas, recursos estáticos, clases y bibliotecas propias, más el descriptor bajo WEB-INF |
| .ear | La 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.
| JDBC | JPA (ORM) | |
|---|---|---|
| Nivel | Bajo: se trabaja con SQL y con tablas | Alto: se trabaja con objetos |
| Quién escribe el SQL | El programador, sentencia a sentencia | La biblioteca, a partir de las clases y sus anotaciones |
| Qué se maneja | Conexiones, sentencias y un cursor de resultados | Entidades: un objeto se corresponde con una fila |
| Dónde vive | En Java SE, en el paquete java.sql | En 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: 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 = :modalidadEl ú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étodo | Qué hace |
|---|---|
| persist | Da de alta una entidad nueva |
| find | Recupera una entidad por su clave primaria |
| merge | Vuelve a poner bajo control del gestor un objeto que estaba fuera |
| remove | Da de baja la entidad |
| contains | Indica si esa entidad está gestionada en este momento |
| flush | Fuerza 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.
@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