14. Aplicación web MVC en una arquitectura de tres capas – Ejemplo 1
14.1. Présentation
Hasta ahora, nos hemos limitado a ejemplos con fines didácticos. Por ello, debían ser sencillos. Ahora presentamos una aplicación básica, pero sin embargo más completa que todas las presentadas hasta ahora. Tendrá la particularidad de utilizar las tres capas de una arquitectura de tres capas:

Se invita al lector a repasar los principios de una aplicación web MVC en una arquitectura de tres capas, si los ha olvidado, en el párrafo 4.
La aplicación web que vamos a desarrollar permitirá administrar un grupo de personas mediante cuatro operaciones:
- lista de personas del grupo
- agregar una persona al grupo
- modificar a una persona del grupo
- eliminar a una persona del grupo
Estas cuatro operaciones son las básicas que se realizan en una tabla de base de datos. Escribiremos dos versiones de esta aplicación:
- en la versión 1, la capa [dao] no utilizará una base de datos. Las personas del grupo se almacenarán en un simple objeto [ArrayList] administrado internamente por la capa [dao]. Esto permitirá al lector probar la aplicación sin las limitaciones de una base de datos.
- En la versión 2, colocaremos el grupo de personas en una tabla de base de datos. Mostraremos que esto se hará sin afectar la capa web de la versión 1, que permanecerá sin cambios.
Las siguientes capturas de pantalla muestran las páginas que la aplicación intercambia con el usuario.



![]() |
![]() |
14.2. El proyecto Eclipse
El proyecto de la aplicación se llama [personnes-01]:

Este proyecto abarca las tres capas de la arquitectura de tres capas de la aplicación:
![]() |
- la capa [dao] está contenida en el paquete [istia.st.mvc.personnes.dao]
- la capa [metier] o [service] se encuentra en el paquete [istia.st.mvc.personnes.service]
- la capa [web] o [ui] está contenida en el paquete [istia.st.mvc.personnes.web]
- el paquete [istia.st.mvc.personnes.entites] contiene los objetos compartidos entre diferentes capas
- el paquete [istia.st.mvc.personnes.tests] contiene las pruebas JUnit de las capas [dao] y [service]
Vamos a explorar sucesivamente las tres capas [dao], [service] y [web]. Como sería demasiado largo de escribir y tal vez demasiado aburrido de leer, es posible que a veces seamos un poco breves en las explicaciones, salvo cuando lo que se presente sea nuevo.
14.3. La representación de una persona
La aplicación administra un grupo de personas. Las capturas de pantalla del párrafo 14.1 mostraron algunas de las características de una persona. Formalmente, estas se representan mediante una clase [Personne]:
![]()
La clase [Personne] es la siguiente:
- Una persona se identifica mediante la siguiente información:
- id: un número que identifica de manera única a una persona
- apellido: el apellido de la persona
- nombre: su nombre
- dateNaissance: su fecha de nacimiento
- estado civil: si está casada o no
- nbEnfants: su número de hijos
- El atributo [version] es un atributo agregado artificialmente para las necesidades de la aplicación. Desde una perspectiva orientada a objetos, sin duda habría sido preferible agregar este atributo en una clase derivada de [Personne]. Su necesidad surge al analizar los casos de uso de la aplicación web. Uno de ellos es el siguiente:
En el momento T1, un usuario U1 ingresa al modo de edición de una persona P. En ese momento, el número de hijos es 0. Cambia este número a 1, pero antes de que valide su modificación, un usuario U2 ingresa para modificar a la misma persona P. Dado que U1 aún no ha validado su modificación, U2 ve que el número de hijos es 0. U2 cambia el nombre de la persona P a mayúsculas. Luego, U1 y U2 validan sus modificaciones en ese orden. La modificación de U2 será la que prevalezca: el nombre se escribirá en mayúsculas y el número de hijos se mantendrá en cero, aunque U1 crea haberlo cambiado a 1.
El concepto de versión de persona nos ayuda a resolver este problema. Tomemos el mismo caso de uso:
En el momento T1, un usuario U1 ingresa al modo de edición de una persona P. En ese momento, el número de hijos es 0 y la versión es V1. Cambia el número de hijos a 1, pero antes de que valide su modificación, un usuario U2 ingresa para modificar la misma persona P. Dado que U1 aún no ha validado su modificación, U2 ve que el número de hijos es 0 y que la versión es V1. U2 cambia el nombre de la persona P a mayúsculas. Luego, U1 y U2 validan sus modificaciones en ese orden. Antes de validar una modificación, se verifica que quien modifica a la persona P tenga la misma versión que la persona P actualmente registrada. Este será el caso del usuario U1. Por lo tanto, se acepta su modificación y se cambia la versión de la persona modificada de V1 a V2 para indicar que la persona ha sufrido un cambio. Al validar la modificación de U2, nos daremos cuenta de que tiene una versión V1 de la persona P, mientras que actualmente la versión de esta es V2. Entonces podremos informarle al usuario U2 que alguien lo ha precedido y que debe comenzar de nuevo con la nueva versión de la persona P. Él lo hará, recuperará una persona P con la versión V2 que ahora tiene un hijo, escribirá el nombre en mayúsculas y validará. Su modificación se aceptará si la persona P registrada sigue teniendo la versión V2. Al final, se tomarán en cuenta las modificaciones realizadas por U1 y U2, mientras que en el caso de uso sin versión, una de las modificaciones se perdía.
- líneas 32-40: un constructor capaz de inicializar los campos de una persona. Se omite el campo [version].
- líneas 43-51: un constructor que crea una copia de la persona que se le pasa como parámetro. De este modo, se obtienen dos objetos con contenido idéntico, pero a los que se hace referencia mediante dos punteros diferentes.
- línea 55: el método [toString] se redefine para devolver una cadena de caracteres que representa el estado de la persona
14.4. La capa [dao]
La capa [dao] está compuesta por las siguientes clases e interfaces:
![]()
- [IDao] es la interfaz que presenta la capa [dao]
- [DaoImpl] es una implementación de esta, en la que el grupo de personas está encapsulado en un objeto [ArrayList]
- [DaoException] es un tipo de excepciones no verificadas (unchecked), lanzadas por la capa [dao]
La interfaz [IDao] es la siguiente:
- La interfaz cuenta con cuatro métodos para las cuatro operaciones que se desean realizar sobre el grupo de personas:
- getAll: para obtener una colección de personas
- getOne: para obtener una persona con un id específico
- saveOne: para agregar una persona (id=-1) o modificar una persona existente (id <> -1)
- deleteOne: para eliminar una persona con un id específico
La capa [dao] puede generar excepciones. Estas serán del tipo [DaoException] :
- línea 3: la clase [DaoException], derivada de [RuntimeException], es un tipo de excepción no controlada: el compilador no nos obliga a:
- manejar este tipo de excepciones con un try / catch al llamar a un método que pueda lanzarla
- incluir el marcador «throws DaoException» en la firma de un método que pueda lanzar la excepción
Esta técnica nos evita tener que declarar los métodos de la interfaz [IDao] con excepciones de un tipo específico. Cualquier implementación que lance excepciones no controladas será entonces aceptable, lo que aporta flexibilidad a la arquitectura.
- línea 6: un código de error. La capa [dao] lanzará diversas excepciones que se identificarán mediante diferentes códigos de error. Esto permitirá a la capa que decida manejar la excepción conocer el origen exacto del error y, de este modo, tomar las medidas adecuadas. Existen otras formas de lograr el mismo resultado. Una de ellas es crear un tipo de excepción para cada tipo de error posible, por ejemplo, NomManquantException, PrenomManquantException, AgeIncorrectException, ...
- líneas 13-16: el constructor que permitirá crear una excepción identificada por un código de error y un mensaje de error.
- líneas 8-10: el método que permitirá al código de manejo de una excepción recuperar el código de error.
La clase [DaoImpl] implementa la interfaz [IDao]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Solo presentaremos las líneas generales de este código. Sin embargo, nos detendremos un poco en las partes más delicadas.
- línea 13: el objeto [ArrayList] que contendrá el grupo de personas
- línea 16: el identificador de la última persona agregada. Cada vez que se agregue una nueva persona, este identificador se incrementará en 1.
La clase [DaoImpl] se instanciará en un único ejemplar. A esto se le llama un singleton. Una aplicación web atiende a sus usuarios de manera simultánea. En un momento dado, hay varios hilos ejecutándose en el servidor web. Estos comparten los singletons:
- el de la capa [dao]
- el de la capa [service]
- los de los distintos controladores, validadores de datos, etc., de la capa web
Si un singleton tiene campos privados, hay que preguntarse de inmediato por qué los tiene. ¿Están justificados? De hecho, serán compartidos entre diferentes subprocesos. Si son de solo lectura, no hay problema siempre y cuando puedan inicializarse en un momento en el que se tenga la certeza de que solo hay un subproceso activo. Por lo general, sabemos cómo identificar ese momento. Es el inicio de la aplicación web, cuando aún no ha comenzado a atender a los clientes. Si son de lectura y escritura, entonces hay que implementar una sincronización del acceso a los campos; de lo contrario, nos dirigimos hacia una catástrofe. Ilustraremos este problema cuando probemos la capa [dao].
- La clase [DaoImpl] no tiene constructor. Por lo tanto, se utilizará su constructor por defecto.
- líneas 19-38: el método [init] se llamará al instanciar el singleton de la capa [dao]. Este crea una lista de tres personas.
- líneas 41-43: implementa el método [getAll] de la interfaz [IDao]. Devuelve una referencia a la lista de personas.
- líneas 46-55: implementa el método [getOne] de la interfaz [IDao]. Su parámetro es el ID de la persona buscada.
Para recuperarla, se invoca un método privado [getPosition] de las líneas 113-126. Este método devuelve la posición en la lista de la persona buscada o -1 si no se ha encontrado a la persona.
Si se ha encontrado a la persona, el método [getOne] devuelve una referencia (línea 51) a una copia de esa persona y no a la persona misma. De hecho, cuando un usuario quiera modificar una persona, la información sobre ella se solicitará a la capa [dao] y se transmitirá hasta la capa [web] para su modificación, en forma de una referencia a un objeto [Personne]. Esta referencia servirá como contenedor de los datos ingresados en el formulario de modificación. Cuando, en la capa web, el usuario envíe sus modificaciones, se modificará el contenido del contenedor de datos. Si el contenedor es una referencia a la persona real del objeto [ArrayList] de la capa [dao], entonces esta se modifica aunque los cambios no se hayan aplicado a las capas [service] y [dao]. Esta última es la única autorizada para administrar la lista de personas. Por lo tanto, la capa web debe trabajar con una copia de la persona que se va a modificar. En este caso, la capa [dao] proporciona esta copia.
Si no se encuentra la persona buscada, se lanza una excepción de tipo [DaoException] con el código de error 2 (línea 53).
- líneas 94-104: implementa el método [deleteOne] de la interfaz [IDao]. Su parámetro es el ID de la persona que se va a eliminar. Si la persona que se va a eliminar no existe, se genera una excepción de tipo [DaoException] con el código de error 2.
- líneas 58-91: implementa el método [saveOne] de la interfaz [IDao]. Su parámetro es un objeto [Personne]. Si este objeto tiene un id=-1, se trata de agregar una persona. De lo contrario, se trata de modificar la persona de la lista que tiene ese id con los valores del parámetro.
- línea 60: la validez del parámetro [Personne] se verifica mediante un método privado [check] definido en las líneas 129-155. Este método realiza verificaciones básicas sobre el valor de los distintos campos de [Personne]. Cada vez que se detecta una anomalía, se lanza un [DaoException] con un código de error específico. Como el método [saveOne] no maneja esta excepción, la reenviará al método que lo invocó.
- Línea 62: si el parámetro [Personne] tiene un ID igual a -1, entonces se trata de una adición. El objeto [Personne] se agrega a la lista interna de personas (línea 66), con el primer ID disponible (línea 64) y un número de versión igual a 1 (línea 65).
- Si el parámetro [Personne] tiene un [id] distinto de -1, se trata de modificar la persona de la lista interna que tiene ese [id]. En primer lugar, se verifica (líneas 70-75) que la persona a modificar exista. Si no es así, se lanza una excepción de tipo [DaoException] con el código de error 2.
- Si la persona sí existe, se verifica que su versión actual sea la misma que la del parámetro [Personne], que contiene las modificaciones que se deben aplicar al original. Si no es así, significa que quien desea modificar la persona no cuenta con la última versión. Se le notifica esto lanzando una excepción de tipo [DaoException] con el código de error 3 (líneas 79-80).
- Si todo sale bien, las modificaciones se realizan en el registro original de la persona (líneas 85-90)
Es evidente que este método debe estar sincronizado. Por ejemplo, entre el momento en que se verifica que la persona a modificar se encuentra efectivamente ahí y el momento en que se realizará la modificación, es posible que otra persona haya eliminado a esa persona de la lista. Por lo tanto, el método debería declararse como [synchronized] para garantizar que solo un hilo lo ejecute a la vez. Lo mismo ocurre con los demás métodos de la interfaz [IDao]. No lo hacemos así, ya que preferimos trasladar esta sincronización a la capa [service]. Para poner de relieve los problemas de sincronización, durante las pruebas de la capa [dao] detendremos la ejecución de [saveOne] durante 10 ms (línea 83) entre el momento en que sabemos que podemos realizar la modificación y el momento en que realmente la hacemos. El hilo que ejecuta [saveOne] perderá entonces el control del procesador a favor de otro. De esta manera, aumentamos nuestras posibilidades de que surjan conflictos de acceso a la lista de personas.
14.5. Pruebas de la capa [dao]
Se escribe una prueba JUnit para la capa [dao]:
![]() | ![]() |
[TestDao] es la prueba JUnit. Para poner de manifiesto los problemas de acceso concurrente a la lista de personas, se crean subprocesos del tipo [ThreadDaoMajEnfants]. Estos se encargan de incrementar en 1 el número de hijos de una persona determinada.
[TestDao] cuenta con cinco pruebas, desde [test1] hasta [test5]. Solo presentamos dos de ellas; invitamos al lector a descubrir las demás en el código fuente asociado a este artículo.
- línea 9: referencia a la implementación de la capa [dao] que se está probando
- líneas 12-15: el constructor de la prueba JUnit. Crea una instancia de tipo [DaoImpl] de la capa [dao] que se va a probar y la inicializa.
El método [test1] prueba los cuatro métodos de la interfaz [IDao] de la siguiente manera:
- línea 3: se solicita la lista de personas
- línea 6: se muestra dicha lista
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]
A continuación, la prueba agrega una persona, la modifica y la elimina. De esta manera, se utilizan los cuatro métodos de la interfaz [IDao].
- líneas 8-10: se agrega una nueva persona (id=-1).
- línea 11: se recupera el id de la persona agregada, ya que al agregarla se le asignó uno. Antes no lo tenía.
- líneas 13-14: se solicita a la capa [dao] una copia de la persona que acaba de agregarse. Hay que recordar que, si no se encuentra a la persona solicitada, la capa [dao] genera una excepción. En ese caso, se producirá un error en la línea 13. Se podría haber manejado este caso de manera más adecuada. En la línea 14, se verifica el nombre de la persona encontrada.
- Líneas 16-17: se modifica ese nombre y se le pide a la capa [dao] que guarde los cambios.
- líneas 19-20: se solicita a la capa [dao] una copia de la persona que acaba de agregarse y se verifica su nuevo nombre.
- línea 22: se elimina la persona agregada al inicio de la prueba.
- líneas 23-34: se solicita a la capa [dao] una copia de la persona que acaba de ser eliminada. Se debe obtener una [DaoException] con código 2.
- líneas 36-37: se vuelve a solicitar la lista de personas. Se debe obtener la misma que al inicio de la prueba.
El método [test4] busca poner de manifiesto los problemas de acceso concurrente a los métodos de la capa [dao]. Recordemos que estos no han sido sincronizados. El código de la prueba es el siguiente:
- líneas 3-6: se agrega a la lista una persona P sin hijos. Se registra su [id] (línea 6).
- líneas 7-13: se inician N subprocesos. Cada uno de ellos incrementará el número de hijos de la persona P en 1 unidad. Al final, la persona P deberá tener N hijos.
- líneas 15-17: el método [test4], que inició los N subprocesos, espera a que estos terminen su trabajo antes de consultar el nuevo número de hijos de la persona P.
- líneas 18-21: se recupera a la persona P y se verifica que su número de hijos sea N.
- líneas 22-35: se elimina a la persona P y luego se verifica que ya no exista en la lista.
En la línea 11, vemos que los hilos son de tipo [ThreadDaoMajEnfants]. El constructor de este tipo tiene tres parámetros:
- el nombre asignado al hilo, para poder dar seguimiento a través de los registros
- una referencia a la capa [dao] para que el hilo tenga acceso a ella
- el ID de la persona en la que debe trabajar el hilo
El tipo [ThreadDaoMajEnfants] es el siguiente:
- línea 9: [ThreadDaoMajEnfants] es, efectivamente, un hilo
- líneas 18-22: el constructor que inicializa el hilo con tres datos
- el nombre [name] asignado al hilo
- una referencia [dao] a la capa [dao]. Cabe señalar que, una vez más, estamos trabajando con el tipo de la interfaz [IDao] y no con el de la implementación [DaoImpl].
- el identificador [id] de la persona en la que debe trabajar el hilo
Cuando [test4] inicia un hilo [ThreadDaoMajEnfants] (línea 12 de test4), se ejecuta el método [run] (línea 25) de este:
- líneas 78-81: el método privado [suivi] permite generar registros en pantalla. El método [run] lo utiliza para permitir el seguimiento del hilo durante su ejecución.
- El hilo intentará incrementar en 1 el número de hijos de la persona P con el identificador [id]. Esta actualización puede requerir varios intentos. Tomemos dos hilos: [TH1] y [TH2]. [TH1] solicita una copia de la persona P a la capa [dao]. La obtiene y constata que tiene la versión V1. [TH1] se interrumpe. [TH2], que lo seguía, hace lo mismo y obtiene la misma versión V1 de la persona P. [TH2] se interrumpe. [TH2] retoma el control, incrementa el número de hijos de P y guarda sus modificaciones. Sabemos que, en ese momento, estas se guardan y que la versión de P pasará a ser V2. [TH1] ha terminado su trabajo. [TH2] retoma el control y hace lo mismo. Su actualización de P será rechazada porque tiene una copia de P con la versión V1, mientras que el P original ahora tiene la versión V2. [TH2] debe entonces repetir todo el ciclo de [lecture -> mise à jour -> sauvegarde]. Por eso, encontramos el bucle de las líneas 32 a 72. En él, el hilo:
- solicita una copia de la persona P para modificarla (línea 34)
- espera 10 ms (línea 43). Esto es artificial y tiene como objetivo interrumpir el hilo entre la lectura de la persona P y su actualización efectiva en la lista de personas, con el fin de aumentar la probabilidad de conflictos.
- incrementa el número de hijos de P (línea 54) y guarda a P (línea 56). Si el hilo no tiene la versión correcta de P, la capa [dao] lanzará una excepción. A continuación, se recupera el código de la excepción (línea 61) para verificar que sea efectivamente el código 3 (versión incorrecta de P). Si no es así, se vuelve a lanzar la excepción al método que la invocó, que en este caso es el método de prueba [test4]. Si se produce la excepción con código 3, se reinicia el ciclo [lecture -> mise à jour -> sauvegarde]. Si no se produce ninguna excepción, significa que la actualización se ha realizado y que el trabajo del hilo ha finalizado.
¿Qué resultados arrojan las pruebas?
En la primera configuración probada:
- se comenta la instrucción de espera en el método [saveOne] de [DaoImpl] (línea 83, párrafo 14.4).
- El método [test4] crea 100 subprocesos (línea 8, párrafo 14.5).
Se obtienen los siguientes resultados:

Las cinco pruebas se realizaron con éxito.
En la segunda configuración probada:
- se descomenta la instrucción de espera en el método [saveOne] de [DaoImpl] (línea 83, párrafo 14.4).
- El método [test4] crea 2 subprocesos (línea 8, párrafo 14.5).
Se obtienen los siguientes resultados:
![]() | ![]() |
La prueba [test4] falló. Se crearon dos subprocesos, cada uno encargado de incrementar en 1 el número de hijos de una persona P que, al inicio, tenía 0. Por lo tanto, se esperaba tener 2 hijos después de la ejecución de los dos subprocesos, pero solo hay uno.
Revisemos los registros de pantalla de [test4] para entender qué pasó:
- línea 1: el hilo n.º 0 comienza su trabajo
- línea 2: ha obtenido una copia de la persona P y encuentra que su número de hijos es 0
- línea 3: encuentra el [Thread.sleep(10)] de su método [run] y, por lo tanto, se detiene en el tiempo [1145536368171] (ms)
- línea 4: el hilo n.º 1 recupera entonces el procesador y comienza su trabajo
- línea 5: ha recuperado una copia de la persona P y encuentra que su número de hijos es 0
- línea 6: llega al [Thread.sleep(10)] de su método [run] y, por lo tanto, se detiene
- línea 7: el hilo n.º 0 recupera el procesador en el momento [1145536368187] (ms), c.a.d. 16 ms después de haberlo perdido.
- línea 8: lo mismo ocurre con el hilo n.º 1
- línea 9: el hilo n.º 0 realizó su actualización y aumentó el número de hijos a 1
- línea 10: el hilo n.º 1 hizo lo mismo
La pregunta es: ¿por qué el hilo n.º 1 pudo realizar su actualización si, en teoría, ya no tenía la versión correcta de la persona P, que acababa de ser actualizada por el hilo n.º 0?
En primer lugar, se observa una anomalía entre las líneas 7 y 8: parece que el hilo n.º 0 perdió el control del procesador entre estas dos líneas a favor del hilo n.º 1. ¿Qué estaba haciendo en ese momento? Estaba ejecutando el método [saveOne] de la capa [dao]. Este tiene la estructura siguiente (véase el párrafo 14.4):
- El hilo n.º 0 ejecutó [saveOne] y llegó hasta la línea 8, donde se vio obligado a liberar el procesador. Mientras tanto, leyó la versión de la persona P y era 1 porque la persona P aún no se había actualizado.
- Al quedar libre el procesador, el hilo n.º 1 lo heredó. Este, a su vez, ejecutó [saveOne] y llegó hasta la línea 8, donde se vio obligado a liberar el procesador. Mientras tanto, leyó la versión de la persona P y era 1 porque la persona P aún no se había actualizado.
- Al quedar libre el procesador, el hilo n.º 0 lo heredó. A partir de la línea 9, realizó su actualización y cambió el número de hijos a 1. Luego, el método [run] del hilo n.º 0 terminó y el hilo mostró el registro que indicaba que había cambiado el número de hijos a 1 (línea 9).
- Al quedar libre el procesador, el hilo n.º 1 lo heredó. A partir de la línea 9, realizó su actualización y cambió el número de hijos a 1. ¿Por qué 1? Porque tiene una copia de P con un número de hijos igual a 0. Así lo indica el registro (línea 5). Luego, el método [run] del hilo n.º 1 terminó y el hilo mostró el registro que indicaba que había cambiado el número de hijos a 1 (línea 10).
¿De dónde viene el problema? Se debe a que el hilo n.º 0 no tuvo tiempo de validar su modificación y, por lo tanto, de cambiar la versión de la persona P antes de que el hilo n.º 1 intentara leer esa versión para saber si la persona P había cambiado. Este caso es poco probable, pero no imposible. Fue necesario forzar al hilo n.º 0 a perder el control del procesador para que se presentara con solo dos hilos. Sin este artificio, la configuración anterior no había logrado reproducir este mismo caso con 100 hilos. La prueba [test4] había sido exitosa.
¿Cuál es la solución? Sin duda hay varias. Una de ellas, fácil de implementar, es sincronizar el método [saveOne]:
public synchronized void saveOne(Personne personne)
La palabra clave [synchronized] garantiza que solo un hilo a la vez pueda ejecutar el método. De este modo, al hilo n.º 1 solo se le permitirá ejecutar [saveOne] cuando el hilo n.º 0 haya salido de él. De esta manera, se garantiza que la versión de la persona P habrá cambiado cuando el hilo n.º 1 entre en [saveOne]. Su actualización será rechazada porque no tendrá la versión correcta de P.
Son los cuatro métodos de la capa [dao] los que habría que sincronizar. Sin embargo, decidimos mantener esta capa tal como se ha descrito y trasladar la sincronización a la capa [service]. Para ello hay varias razones:
- suponemos que el acceso a la capa [dao] siempre se realiza a través de una capa [service]. Este es el caso en nuestra aplicación web.
- puede ser necesario sincronizar también el acceso a los métodos de la capa [service] por razones distintas a las que nos llevarían a sincronizar los de la capa [dao]. En este caso, no es necesario sincronizar los métodos de la capa [dao]. Si se tiene la certeza de que:
- todo acceso a la capa [dao] pasa por la capa [service]
- que solo un hilo a la vez utiliza la capa [service]
entonces tenemos la seguridad de que los métodos de la capa [dao] no serán ejecutados por dos hilos al mismo tiempo.
Ahora descubrimos la capa [service].
14.6. La capa [service]
La capa [service] está compuesta por las siguientes clases e interfaces:
![]()
- [IService] es la interfaz que presenta la capa [dao]
- [ServiceImpl] es una implementación de esta
La interfaz [IService] es la siguiente:
Es idéntica a la interfaz [IDao].
La implementación [ServiceImpl] de la interfaz [IService] es la siguiente:
- líneas 10-19: el atributo [IDao dao] es una referencia a la capa [dao]. Será inicializado por Spring IoC.
- líneas 22-24: implementación del método [getAll] de la interfaz [IService]. El método simplemente delega la solicitud a la capa [dao].
- líneas 27-29: implementación del método [getOne] de la interfaz [IService]. El método simplemente delega la solicitud a la capa [dao].
- líneas 32-34: implementación del método [saveOne] de la interfaz [IService]. El método simplemente delega la solicitud a la capa [dao].
- líneas 37-39: implementación del método [deleteOne] de la interfaz [IService]. El método simplemente delega la solicitud a la capa [dao].
- Todos los métodos están sincronizados (palabra clave `synchronized`), lo que garantiza que solo un hilo a la vez pueda utilizar la capa [service] y, por lo tanto, la capa [dao].
14.7. Pruebas de la capa [service]
Se escribe una prueba JUnit para la capa [service]:
![]() | ![]() |
[TestService] es la prueba de JUnit. Las pruebas realizadas son estrictamente idénticas a las realizadas para la capa [dao]. El esqueleto de [TestService] es el siguiente:
- líneas 9: la capa [service] probada es del tipo [ServiceImpl].
- líneas 11-15: el generador de la prueba JUnit crea una instancia de la capa [service] que se va a probar (línea 12), crea una instancia de la capa [dao] (línea 13) e indica a la capa [service] que debe utilizar esta capa [dao] (línea 14).
El método [test1] prueba los cuatro métodos de la interfaz [IService] de la misma manera que el método de prueba de la capa [dao] del mismo nombre. Simplemente, se accede a la capa [service] (líneas 25, 32, 35) en lugar de a la capa [dao].
El método [test4] busca identificar problemas de acceso concurrente a los métodos de la capa [service]. Este método, una vez más, es idéntico al método de prueba [test4] de la capa [dao]. Sin embargo, hay algunos detalles que cambian:
- se hace referencia a la capa [service] en lugar de a la capa [dao] (línea 55)
- se pasa a los hilos una referencia a la capa [service] en lugar de a la capa [dao] (línea 61)
El tipo [ThreadServiceMajEnfants] también es prácticamente idéntico al tipo [ThreadDaoMajEnfants], con la única diferencia de que trabaja con la capa [service] y no con la capa [dao]:
- línea 12: el hilo trabaja con la capa [service]
Realizamos las pruebas con la configuración que causó problemas en la capa [dao]:
- descomentamos la instrucción de espera en el método [saveOne] de [DaoImpl] (línea 83, párrafo 14.4).
- El método [test4] crea 100 subprocesos (línea 65, párrafo 14.7).
Los resultados obtenidos son los siguientes:
![]() |
La sincronización de los métodos de la capa [service] fue lo que permitió que la prueba [test4] se realizara con éxito.
14.8. La capa [web]
Recordemos la arquitectura de tres capas de nuestra aplicación:
![]() |
La capa [web] ofrecerá pantallas al usuario para que pueda administrar el grupo de personas:
- lista de personas del grupo
- agregar una persona al grupo
- modificar a una persona del grupo
- eliminar a una persona del grupo
Para ello, se basará en la capa [service], la cual a su vez recurrirá a la capa [dao]. Ya hemos presentado las pantallas gestionadas por la capa [web] (párrafo 14.1). Para describir la capa web, presentaremos sucesivamente:
- su configuración
- sus vistas
- su controlador
- algunas pruebas
14.8.1. Configuración de la aplicación web
El proyecto Eclipse de la aplicación es el siguiente:

- En el paquete [istia.st.mvc.personnes.web] se encuentra el controlador [Application].
- Las páginas JSP / JSTL se encuentran en [WEB-INF/vues].
- La carpeta [lib] contiene los archivos de terceros necesarios para la aplicación. Estos se pueden ver en la carpeta [Web App Libraries].
[web.xml]
El archivo [web.xml] es el archivo que utiliza el servidor web para cargar la aplicación. Su contenido es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>mvc-personnes-01</display-name>
<!-- ServletPersonne -->
<servlet>
<servlet-name>personnes</servlet-name>
<servlet-class>
istia.st.mvc.personnes.web.Application
</servlet-class>
<init-param>
<param-name>urlEdit</param-name>
<param-value>/WEB-INF/vues/edit.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/WEB-INF/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlList</param-name>
<param-value>/WEB-INF/vues/list.jsp</param-value>
</init-param>
</servlet>
<!-- Asignación de ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- archivos de inicio -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Página de error inesperado -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- líneas 27-30: las URL [/do/*] serán procesadas por el servlet [personnes]
- líneas 9-12: el servlet [personnes] es una instancia de la clase [Application], una clase que vamos a crear.
- líneas 13-24: definen tres parámetros [urlList, urlEdit, urlErreurs] que identifican las URL de las páginas JSP de las vistas [list, edit, erreurs].
- líneas 32-34: la aplicación tiene una página de inicio predeterminada [index.jsp] que se encuentra en la raíz de la carpeta de la aplicación web.
- líneas 36-39: la aplicación cuenta con una página de errores predeterminada que se muestra cuando el servidor web detecta una excepción no manejada por la aplicación.
- línea 37: la etiqueta <exception-type> indica el tipo de excepción que maneja la directiva <error-page>; en este caso, el tipo [java.lang.Exception] y sus derivados, es decir, todas las excepciones.
- línea 38: la etiqueta <location> indica la página JSP que se debe mostrar cuando ocurre una excepción del tipo definido por <exception-type>. La excepción que ocurrió está disponible en esta página en un objeto llamado exception si la página tiene la directiva:
<%@ page isErrorPage="true" %>
- (continuación)
- si <exception-type> especifica un tipo T1 y una excepción de tipo T2, que no deriva de T1, se remite al servidor web, este envía al cliente una página de excepción propia que, por lo general, no es muy intuitiva. De ahí la importancia de la etiqueta <error-page> en el archivo [web.xml].
[index.jsp]
Esta página se muestra si un usuario solicita directamente el contexto de la aplicación sin especificar una URL, c.a.d. Aquí [/personnes-01]. Su contenido es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/do/list"/>
[index.jsp] redirige al cliente a la URL [/do/list]. Esta URL muestra la lista de personas del grupo.
14.8.2. Las páginas JSP / JSTL de la aplicación
La vista [list.jsp]
Sirve para mostrar la lista de personas:

Su código es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>Liste des personnes</h2>
<table border="1">
<tr>
<th>Id</th>
<th>Version</th>
<th>Prénom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</th>
<th>Nombre d'enfants</th>
<th></th>
</tr>
<c:forEach var="personne" items="${personnes}">
<tr>
<td><c:out value="${personne.id}"/></td>
<td><c:out value="${personne.version}"/></td>
<td><c:out value="${personne.prenom}"/></td>
<td><c:out value="${personne.nom}"/></td>
<td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
<td><c:out value="${personne.marie}"/></td>
<td><c:out value="${personne.nbEnfants}"/></td>
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
- Esta vista recibe un elemento en su modelo:
- el elemento [personnes] asociado a un objeto de tipo [ArrayList] de objetos de tipo [Personne]
- líneas 22-34: se recorre la lista ${personas} para mostrar una tabla HTML que contiene a las personas del grupo.
- línea 31: la URL a la que apunta el enlace [Modifier] se configura mediante el campo [id] de la persona actual, para que el controlador asociado a la URL [/do/edit] sepa qué persona se debe modificar.
- línea 32: se hace lo mismo para el enlace [Supprimer].
- línea 28: para mostrar la fecha de nacimiento de la persona en el formato JJ/MM/AAAA, se utiliza la etiqueta <dt> de la biblioteca de etiquetas [DateTime] del proyecto Apache [Jakarta Taglibs]:

El archivo de descripción de esta biblioteca de etiquetas se define en la línea 3.
- Línea 37: el enlace [Ajout] para agregar una nueva persona tiene como destino la URL [/do/edit], al igual que el enlace [Modifier] de la línea 31. Es el valor -1 del parámetro [id] el que indica que se trata de una adición y no de una modificación.
La vista [edit.jsp]
Sirve para mostrar el formulario para agregar una persona nueva o modificar una persona existente:
![]() |
El código de la vista [edit.jsp] es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="../ressources/standard.jpg">
<h2>Ajout/Modification d'une personne</h2>
<c:if test="${erreurEdit != ''}">
<h3>Echec de la mise à jour :</h3>
L'erreur suivante s'est produite : ${erreurEdit}
<hr>
</c:if>
<form method="post" action="<c:url value="/do/validate"/>">
<table border="1">
<tr>
<td>Id</td>
<td>${id}</td>
</tr>
<tr>
<td>Version</td>
<td>${version}</td>
</tr>
<tr>
<td>Prénom</td>
<td>
<input type="text" value="${prenom}" name="prenom" size="20">
</td>
<td>${erreurPrenom}</td>
</tr>
<tr>
<td>Nom</td>
<td>
<input type="text" value="${nom}" name="nom" size="20">
</td>
<td>${erreurNom}</td>
</tr>
<tr>
<td>Date de naissance (JJ/MM/AAAA)</td>
<td>
<input type="text" value="${dateNaissance}" name="dateNaissance">
</td>
<td>${erreurDateNaissance}</td>
</tr>
<tr>
<td>Marié</td>
<td>
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<td>Nombre d'enfants</td>
<td>
<input type="text" value="${nbEnfants}" name="nbEnfants">
</td>
<td>${erreurNbEnfants}</td>
</tr>
</table>
<br>
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
</body>
</html>
Esta vista presenta un formulario para agregar una persona nueva o actualizar una ya existente. De aquí en adelante, y para simplificar la redacción, utilizaremos únicamente el término [mise à jour]. El botón [Valider] (línea 73) activa el POST del formulario en la URL [/do/validate] (línea 16). Si el POST falla, se vuelve a mostrar la vista [edit.jsp] con el error o los errores que se hayan producido; de lo contrario, se muestra la vista [list.jsp].
- La vista [edit.jsp], que se muestra tanto en un GET como en un POST que falla, recibe los siguientes elementos en su plantilla:
atributo | GET | POST |
identificador de la persona actualizada | ídem | |
su versión | ídem | |
su nombre | nombre ingresado | |
su apellido | apellido ingresado | |
su fecha de nacimiento | fecha de nacimiento ingresada | |
su estado civil | estado civil ingresado | |
su número de hijos | número de hijos ingresado | |
vacío | un mensaje de error que indica que la adición o modificación falló al ejecutar POST, provocada por el botón [Envoyer]. Vacío si no hay error. | |
vacío | indica un nombre incorrecto; de lo contrario, está vacío | |
vacío | indica un apellido incorrecto; si no, está vacío | |
vacío | indica una fecha de nacimiento incorrecta; si no, está vacío | |
vacío | indica un número de hijos incorrecto; si no, queda en blanco |
- líneas 11-15: si el POST del formulario falla, aparecerá [erreurEdit!=''] y se mostrará un mensaje de error.
- línea 16: el formulario se enviará a la URL [/do/validate]
- línea 20: se muestra el elemento [id] de la plantilla
- línea 24: se muestra el elemento [version] de la plantilla
- líneas 26-32: ingreso del nombre de la persona:
- al mostrar el formulario por primera vez (GET), ${nombre} muestra el valor actual del campo [prenom] del objeto [Personne] actualizado y ${erreurPrenom} está vacío.
- En caso de error después de POST, se vuelve a mostrar el valor ingresado ${prenom}, así como el posible mensaje de error ${erreurPrenom}
- líneas 33-39: ingreso del apellido de la persona
- líneas 40-46: ingreso de la fecha de nacimiento de la persona
- líneas 47-61: ingreso del estado civil de la persona mediante un botón de radio. Se utiliza el valor del campo [marie] del objeto [Personne] para determinar cuál de los dos botones de radio debe marcarse.
- líneas 62-68: ingreso del número de hijos de la persona
- línea 71: un campo oculto HTML denominado [id], cuyo valor corresponde al campo [id] de la persona que se está actualizando; -1 para un agregado, cualquier otro valor para una modificación.
- línea 72: un campo oculto HTML llamado [version], cuyo valor es el campo [id] de la persona que se está actualizando.
- línea 73: el botón [Valider], de tipo [Submit], del formulario
- línea 74: un enlace que permite regresar a la lista de personas. Se ha denominado [Annuler] porque permite salir del formulario sin validarlo.
La vista [exception.jsp]
Sirve para mostrar una página que indica que se ha producido una excepción no manejada por la aplicación y que se ha reportado al servidor web.
Por ejemplo, eliminemos una persona que no existe en el grupo:
![]() |
El código de la vista [exception.jsp] es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
<%
response.setStatus(200);
%>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>MVC - personnes</h2>
L'exception suivante s'est produite :
<%= exception.getMessage()%>
<br><br>
<a href="<c:url value="/do/list"/>">Retour à la liste</a>
</body>
</html>
- Esta vista recibe una clave en su modelo: el elemento [exception], que es la excepción que fue interceptada por el servidor web. Para que el servidor web incluya este elemento en el modelo de la página JSP, la página debe tener definida la etiqueta de la línea 3.
- Línea 6: se establece en 200 el código de estado HTTP de la respuesta. Este es el primer encabezado HTTP de la respuesta. El código 200 le indica al cliente que su solicitud ha sido atendida. Por lo general, se ha incluido un documento HTML en la respuesta del servidor. Este es el caso aquí. Si no se establece en 200 el código de estado HTTP de la respuesta, aquí tendrá el valor 500, lo que significa que se ha producido un error. De hecho, al haber interceptado una excepción no manejada, el servidor web considera que esta situación es anómala y la señala con el código 500. La reacción ante el código 500 varía según el navegador: Firefox muestra el documento que puede acompañar a esta respuesta, mientras que ignora dicho documento y muestra su propia página. Por esta razón, hemos reemplazado el código 500 por el código 200.
- línea 16: se muestra el texto de la excepción
- línea 18: se ofrece al usuario un enlace para regresar a la lista de personas
La vista [erreurs.jsp]
Sirve para mostrar una página que informa sobre los errores de inicialización de la aplicación, c.a.d, y los errores detectados durante la ejecución del método [init] del servlet del controlador. Esto puede deberse, por ejemplo, a la falta de un parámetro en el archivo [web.xml], como se muestra en el siguiente ejemplo:

El código de la página [erreurs.jsp] es el siguiente:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li>${erreur}</li>
</c:forEach>
</ul>
</body>
</html>
La página recibe en su plantilla un elemento [erreurs], que es un objeto de tipo [ArrayList] de objetos [String]; estos últimos son mensajes de error. Se muestran mediante el bucle de las líneas 13 a 15.
14.8.3. El controlador de la aplicación
El controlador [Application] se define en el paquete [istia.st.mvc.personnes.web]:
![]()
Estructu tura e inicialización del controlador
La estructura del controlador [Application] es la siguiente:
- líneas 20-36: se recuperan los parámetros esperados del archivo [web.xml].
- líneas 39-41: el parámetro [urlErreurs] debe estar presente obligatoriamente, ya que indica la URL de la vista [erreurs] capaz de mostrar los posibles errores de inicialización. Si no existe, se interrumpe la aplicación al ejecutar una [ServletException] (línea 40). Esta excepción se transmitirá al servidor web y será gestionada por la etiqueta <error-page> del archivo [web.xml]. Por lo tanto, se muestra la vista [exception.jsp]:

El enlace [Retour à la liste] anterior no funciona. Al utilizarlo, se obtiene la misma respuesta mientras la aplicación no se haya modificado y recargado. Es útil para otros tipos de excepciones, como ya lo hemos visto.
- línea 43: crea una instancia [DaoImpl] que implementa la capa [dao]
- línea 44: inicializa esta instancia (creación de una lista inicial de tres personas)
- línea 46: crea una instancia [ServiceImpl] que implementa la capa [service]
- línea 47: inicializa la capa [service] asignándole una referencia a la capa [dao]
Tras la inicialización del controlador, sus métodos disponen de una referencia [service] a la capa [service] (línea 15), que utilizarán para ejecutar las acciones solicitadas por el usuario. Estas serán interceptadas por el método [doGet], que las hará procesar por un método específico del controlador:
Url | Método HTTP | método del controlador |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
El método [doGet]
El objetivo de este método es dirigir el procesamiento de las acciones solicitadas por el usuario hacia el método correcto. Su código es el siguiente:
- líneas 7-13: se verifica que la lista de errores de inicialización esté vacía. Si no es así, se muestra la vista [erreurs(erreurs)], que indicará el error o los errores.
- línea 15: se recupera el método [get] o [post] que el cliente utilizó para realizar su solicitud.
- línea 17: se recupera el valor del parámetro [action] de la consulta.
- líneas 23-27: procesamiento de la solicitud [GET /do/list] que solicita la lista de personas.
- líneas 28-32: procesamiento de la solicitud [GET /do/delete], que solicita la eliminación de una persona.
- líneas 33-37: procesamiento de la solicitud [GET /do/edit], que solicita el formulario para actualizar una persona.
- líneas 38-42: procesamiento de la solicitud [POST /do/validate], que solicita la validación de la persona actualizada.
- línea 44: si la acción solicitada no es ninguna de las cinco anteriores, entonces se procede como si fuera [GET /do/list].
El método [doListPersonnes]
Este método procesa la solicitud [GET /do/list], que solicita la lista de personas:

Su código es el siguiente:
- línea 5: se solicita a la capa [service] la lista de personas del grupo y se inserta esta en el modelo bajo la clave «personas».
- línea 7: se muestra la vista [list.jsp] descrita en el párrafo 14.8.2.
El método [doDeletePersonne]
Este método procesa la consulta [GET /do/delete?id=XX], que solicita la eliminación de la persona con id=XX. La URL [/do/delete?id=XX] es la de los enlaces [Supprimer] de la vista [list.jsp]:

cuyo código es el siguiente:
En la línea 12, se ve la URL [/do/delete?id=XX] del enlace [Supprimer]. El método [doDeletePersonne], que debe procesar esta URL, debe eliminar a la persona con id=XX y luego mostrar la nueva lista de personas del grupo. Su código es el siguiente:
- línea 5: la URL procesada tiene el formato [/do/delete?id=XX]. Se recupera el valor [XX] del parámetro [id].
- línea 7: se le solicita a la capa [service] que elimine a la persona con el identificador obtenido. No realizamos ninguna verificación. Si la persona que se busca eliminar no existe, la capa [dao] lanza una excepción que la capa [service] deja pasar. Tampoco la manejamos aquí, en el controlador. Por lo tanto, se propagará hasta el servidor web, el cual, según su configuración, mostrará la página [exception.jsp], descrita en el párrafo 14.8.2:

- línea 9: si se ha llevado a cabo la eliminación (sin excepciones), se le pide al cliente que se redirija a la URL relativa [list]. Como la que acaba de procesarse es [/do/delete], la URL de redirección será [/do/list]. Por lo tanto, el navegador se verá obligado a realizar una [GET /do/list], lo que provocará que se muestre la lista de personas.
El método [doEditPersonne]
Este método procesa la solicitud [GET /do/edit?id=XX], que solicita el formulario de actualización de la persona con id=XX. La URL [/do/edit?id=XX] es la de los enlaces [Modifier] y la del enlace [Ajout] de la vista [list.jsp]:

cuyo código es el siguiente:
En la línea 11, se ve la URL [/do/edit?id=XX] del enlace [Modifier] y, en la línea 17, la URL [/do/edit?id=-1] del enlace [Ajout]. El método [doEditPersonne] debe mostrar el formulario de edición de la persona con id=XX o, si se trata de un nuevo registro, presentar un formulario en blanco.
![]() | ![]() |
El código del método [doEditPersonne] es el siguiente:
- el GET tiene como destino una URL del tipo [/do/edit?id=XX]. En la línea 5, recuperamos el valor de [id]. A continuación, hay dos casos:
- El id es distinto de -1. En ese caso, se trata de una modificación y hay que mostrar un formulario prellenado con la información de la persona que se va a modificar. En la línea 10, se solicita esta persona a la capa [service].
- Si id es igual a -1, se trata de una adición y hay que mostrar un formulario vacío. Para ello, se crea una persona vacía en las líneas 13 y 14.
- El objeto [Personne] obtenido se coloca en la plantilla de la página [edit.jsp] descrita en el párrafo 14.8.2. Esta plantilla incluye los siguientes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Estos elementos se inicializan en las líneas 17 a 30, a excepción de aquellos cuyo valor es la cadena vacía [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Se sabe que, en caso de que no estén presentes en la plantilla, la biblioteca JSTL mostrará una cadena vacía como su valor. Aunque el elemento [erreurEdit] también tiene como valor una cadena vacía, se inicializa de todos modos porque se realiza una comprobación de su valor en la página [edit.jsp].
- Una vez que el modelo está listo, el control pasa a la página [edit.jsp], líneas 32-33, que generará la vista [edit].
El método [doValidatePersonne]
Este método procesa la solicitud [POST /do/validate] que valida el formulario de actualización. Esta solicitud POST se activa mediante el botón [Valider]:

Recordemos los campos de entrada del formulario HTML de la vista anterior:
La solicitud POST contiene los parámetros [prenom, nom, dateNaissance, marie, nbEnfants, id, version] y se envía a la URL [/do/validate] (línea 1). Es procesada por el siguiente método [doValidatePersonne]:
- líneas 8-14: se recupera el parámetro [prenom] de la solicitud POST y se verifica su validez. Si resulta incorrecto, el elemento [erreurPrenom] se inicializa con un mensaje de error y se coloca en los atributos de la consulta.
- líneas 16-22: se procede de manera similar con el parámetro [nom]
- líneas 24-32: se procede de manera similar con el parámetro [dateNaissance]
- línea 34: se recupera el parámetro [marie]. No se verifica su validez porque, a priori, proviene del valor de un botón de radio. Dicho esto, nada impide que un programa genere un [POST /personnes-01/do/validate] acompañado de un parámetro [marie] inventado. Por lo tanto, deberíamos verificar la validez de este parámetro. Aquí nos basamos en nuestro manejo de excepciones, que provoca que se muestre la página [exception.jsp] si el controlador no las maneja por sí mismo. Así pues, si la conversión del parámetro [marie] a un valor booleano falla en la línea 34, se generará una excepción que dará lugar al envío de la página [exception.jsp] al cliente. Este funcionamiento nos conviene.
- líneas 34-54: se recupera el parámetro [nbEnfants] y se verifica su valor.
- línea 56: se recupera el parámetro [id] sin verificar su valor
- línea 58: se hace lo mismo con el parámetro [version]
- líneas 60-65: si el formulario contiene errores, se vuelve a mostrar con los mensajes de error generados anteriormente
- líneas 67-69: si es válido, se crea un nuevo objeto [Personne] con los elementos del formulario
- líneas 70-78: se guarda el usuario. El proceso de guardado puede fallar. En un entorno multiusuario, es posible que el usuario que se va a modificar haya sido eliminado o ya haya sido modificado por otra persona. En ese caso, la capa [dao] lanzará una excepción que se maneja aquí.
- línea 80: si no se ha producido ninguna excepción, se redirige al cliente a la URL [/do/list] para mostrarle el nuevo estado del grupo.
- línea 75: si se produjo una excepción durante el guardado, se solicita nuevamente que se vuelva a mostrar el formulario inicial, pasándole el mensaje de error de la excepción (tercer parámetro).
El método [showFormulaire] (líneas 84-101) genera la plantilla necesaria para la página [edit.jsp] con los valores ingresados (request.getParameter(" ... ")). Recordemos que los mensajes de error ya han sido insertados en la plantilla mediante el método [doValidatePersonne]. La página [edit.jsp] se muestra en las líneas 99-100.
14.9. Las pruebas de la aplicación web
En el párrafo 14.1 se presentaron varias pruebas. Invitamos al lector a reproducirlas. Aquí mostramos otras capturas de pantalla que ilustran los casos de conflictos de acceso a los datos en un entorno multiusuario:
[Firefox] será el navegador del usuario U1. Este solicita la URL [http://localhost:8080/personnes-01]:

[IE] será el navegador del usuario U2. Este solicita la misma URL:

El usuario U1 ingresa para modificar el perfil de la persona [Lemarchand]:

El usuario U2 hace lo mismo:

El usuario U1 realiza modificaciones y las valida:
![]() |
El usuario U2 hace lo mismo:
![]() |
El usuario U2 regresa a la lista de personas mediante el enlace [Annuler] del formulario:

Encuentra a la persona [Lemarchand] tal como la modificó U1. Ahora, U2 elimina a [Lemarchand]:
![]() |
U1 sigue teniendo su propia lista y quiere modificar [Lemarchand] nuevamente:
![]() |
U1 usa el enlace [Retour à la liste] para ver de qué se trata:

Descubre que, efectivamente, [Lemarchand] ya no forma parte de la lista...
14.10. Conclusion
Hemos implementado la arquitectura MVC en una arquitectura de tres capas [web, metier, dao] utilizando un ejemplo básico de gestión de una lista de personas. Esto nos permitió aplicar los conceptos que se habían presentado en las secciones anteriores. En la versión analizada, la lista de personas se mantenía en memoria. Próximamente analizaremos versiones en las que esta lista se almacenará en una tabla de base de datos.
Pero antes, vamos a presentar una herramienta llamada Spring IoC, que facilita la integración de las diferentes capas de una aplicación ntier.

















