14. Aplicação web MVC numa arquitetura de três camadas – Exemplo 1
14.1. Présentation
Até agora, limitámo-nos a exemplos com fins pedagógicos. Por isso, tinham de ser simples. Apresentamos agora uma aplicação básica, mas, ainda assim, mais rica do que todas as apresentadas até agora. Terá a particularidade de utilizar as três camadas de uma arquitetura de 3 camadas:

Convidamos o leitor a rever os princípios de uma aplicação web MVC numa arquitetura de três camadas, caso os tenha esquecido, no parágrafo 4.
A aplicação web que vamos criar permitirá gerir um grupo de pessoas através de quatro operações:
- lista das pessoas do grupo
- adição de uma pessoa ao grupo
- alteração de uma pessoa do grupo
- eliminação de uma pessoa do grupo
Estas quatro operações correspondem às operações básicas de uma tabela de base de dados. Iremos desenvolver duas versões desta aplicação:
- na versão 1, a camada [dao] não utilizará uma base de dados. As pessoas do grupo serão armazenadas num simples objeto [ArrayList], gerido internamente pela camada [dao]. Isto permitirá ao leitor testar a aplicação sem as restrições de uma base de dados.
- Na versão 2, colocaremos o grupo de pessoas numa tabela da base de dados. Mostraremos que isso será feito sem impacto na camada web da versão 1, que permanecerá inalterada.
As capturas de ecrã que se seguem mostram as páginas que a aplicação troca com o utilizador.



![]() |
![]() |
14.2. O projeto Eclipse
O projeto da aplicação chama-se [personnes-01]:

Este projeto abrange as três camadas da arquitetura de três camadas da aplicação:
![]() |
- a camada [dao] está contida no pacote [istia.st.mvc.personnes.dao]
- a camada [metier] ou [service] está contida no pacote [istia.st.mvc.personnes.service]
- a camada [web] ou [ui] está contida no pacote [istia.st.mvc.personnes.web]
- o pacote [istia.st.mvc.personnes.entites] contém os objetos partilhados entre diferentes camadas
- o pacote [istia.st.mvc.personnes.tests] contém os testes Junit das camadas [dao] e [service]
Vamos explorar sucessivamente as três camadas [dao], [service] e [web]. Como seria demasiado demorado de escrever e talvez demasiado enfadonho de ler, poderemos, por vezes, ser um pouco rápidos nas explicações, exceto quando o que for apresentado for novo.
14.3. A representação de uma pessoa
A aplicação gere um grupo de pessoas. As capturas de ecrã do parágrafo 14.1 mostraram algumas das características de uma pessoa. Formalmente, estas são representadas por uma classe [Personne]:
![]()
A classe [Personne] é a seguinte:
- uma pessoa é identificada pelas seguintes informações:
- id: um número que identifica de forma única uma pessoa
- apelido: o apelido da pessoa
- nome próprio: o seu nome próprio
- dateNaissance: a sua data de nascimento
- casada: o seu estado civil (casada ou solteira)
- nbEnfants: o número de filhos
- O atributo [version] é um atributo adicionado artificialmente para as necessidades da aplicação. Do ponto de vista dos objetos, teria sido sem dúvida preferível adicionar este atributo numa classe derivada de [Personne]. A sua necessidade surge quando se analisam os casos de utilização da aplicação web. Um deles é o seguinte:
No momento T1, um utilizador U1 acede à edição de uma pessoa P. Nesse momento, o número de filhos é 0. Altera esse número para 1, mas antes de validar a sua alteração, um utilizador U2 acede à edição da mesma pessoa P. Uma vez que U1 ainda não validou a sua alteração, U2 vê o número de filhos como 0. U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 confirmam as suas alterações por esta ordem. É a alteração de U2 que prevalecerá: o nome passará a estar em maiúsculas e o número de filhos permanecerá em zero, mesmo que U1 pense ter-no alterado para 1.
O conceito de versão de pessoa ajuda-nos a resolver este problema. Retomemos o mesmo caso de utilização:
No momento T1, um utilizador U1 acede ao modo de edição de uma pessoa P. Nesse momento, o número de filhos é 0 e a versão é V1. Ele altera o número de filhos para 1, mas antes de validar a sua alteração, um utilizador U2 inicia a edição da mesma pessoa P. Uma vez que U1 ainda não validou a sua alteração, o utilizador U2 vê o número de filhos como 0 e a versão como V1. O utilizador U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 validam as suas alterações por esta ordem. Antes de validar uma alteração, verifica-se se quem altera uma pessoa P possui a mesma versão que a pessoa P atualmente registada. Este será o caso do utilizador U1. A sua alteração é, portanto, aceite e, em seguida, altera-se a versão da pessoa modificada de V1 para V2, para registar o facto de a pessoa ter sofrido uma alteração. Ao validar a alteração de U2, verificar-se-á que este possui uma versão V1 da pessoa P, quando, na realidade, a versão atual desta é V2. Será então possível informar ao utilizador U2 que alguém o antecedeu e que deve recomeçar a partir da nova versão da pessoa P. Ele fará isso, recuperará uma pessoa P com a versão V2, que agora tem um filho, colocará o nome em maiúsculas e validará. A sua alteração será aceite se a pessoa P registada ainda tiver a versão V2. No final, as alterações feitas por U1 e U2 serão tidas em conta, enquanto que, no caso de utilização sem versão, uma das alterações se perderia.
- linhas 32-40: um construtor capaz de inicializar os campos de uma pessoa. O campo [version] é omitido.
- linhas 43-51: um construtor que cria uma cópia da pessoa que lhe é passada como parâmetro. Ficamos, assim, com dois objetos com conteúdo idêntico, mas referenciados por dois ponteiros diferentes.
- linha 55: o método [toString] é redefinido para devolver uma cadeia de caracteres que representa o estado da pessoa
14.4. A camada [dao]
A camada [dao] é constituída pelas seguintes classes e interfaces:
![]()
- [IDao] é a interface apresentada pela camada [dao]
- [DaoImpl] é uma implementação desta, em que o grupo de pessoas está encapsulado num objeto [ArrayList]
- [DaoException] é um tipo de exceções não verificadas (unchecked), lançadas pela camada [dao]
A interface [IDao] é a seguinte:
- A interface possui quatro métodos para as quatro operações que se pretende realizar sobre o grupo de pessoas:
- getAll: para obter um conjunto de pessoas
- getOne: para obter uma pessoa com um id específico
- saveOne: para adicionar uma pessoa (id=-1) ou alterar uma pessoa existente (id <> -1)
- deleteOne: para eliminar uma pessoa com um id específico
A camada [dao] pode lançar exceções. Estas serão do tipo [DaoException] :
- linha 3: a classe [DaoException], derivada de [RuntimeException], é um tipo de exceção não controlada: o compilador não nos obriga a:
- gerir este tipo de exceções com um try/catch quando chamamos um método que possa lançá-la
- incluir o marcador «throws DaoException» na assinatura de um método suscetível de lançar a exceção
Esta técnica evita que tenhamos de assinar os métodos da interface [IDao] com exceções de um tipo específico. Qualquer implementação que lance exceções não controladas será, assim, aceitável, proporcionando maior flexibilidade à arquitetura.
- linha 6: um código de erro. A camada [dao] lançará várias exceções que serão identificadas por códigos de erro diferentes. Isto permitirá que a camada responsável pelo tratamento da exceção conheça a origem exata do erro e, assim, tome as medidas adequadas. Existem outras formas de chegar ao mesmo resultado. Uma delas consiste em criar um tipo de exceção para cada tipo de erro possível, por exemplo, NomManquantException, PrenomManquantException, AgeIncorrectException, ...
- linhas 13-16: o construtor que permitirá criar uma exceção identificada por um código de erro, bem como uma mensagem de erro.
- linhas 8-10: o método que permitirá ao código de gestão de uma exceção recuperar o código de erro.
A classe [DaoImpl] implementa a interface [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 | |
Vamos apresentar apenas as linhas gerais deste código. No entanto, dedicaremos algum tempo às partes mais delicadas.
- linha 13: o objeto [ArrayList], que irá conter o grupo de pessoas
- linha 16: o identificador da última pessoa adicionada. A cada nova adição, este identificador será incrementado em 1.
A classe [DaoImpl] será instanciada numa única instância. É o que se denomina um singleton. Uma aplicação web atende os seus utilizadores de forma simultânea. Num determinado momento, existem várias threads a serem executadas pelo servidor web. Estas partilham os singletons:
- o da camada [dao]
- o da camada [service]
- os das diferentes controlas, validadores de dados, etc., da camada web
Se um singleton tiver campos privados, é preciso questionar-se imediatamente sobre o motivo de os ter. Estão justificados? Com efeito, vão ser partilhados entre diferentes threads. Se forem de leitura única, isso não representa um problema, desde que possam ser inicializados num momento em que se tenha a certeza de que existe apenas um thread ativo. Em geral, sabemos identificar esse momento. É o momento do arranque da aplicação web, quando esta ainda não começou a servir clientes. Se forem de leitura/escrita, é necessário implementar uma sincronização do acesso aos campos; caso contrário, corremos para o desastre. Iremos ilustrar este problema quando testarmos a camada [dao].
- A classe [DaoImpl] não tem construtor. Por isso, será utilizado o seu construtor por predefinição.
- linhas 19-38: o método [init] será chamado no momento da instanciação do singleton da camada [dao]. Este método cria uma lista de três pessoas.
- linhas 41-43: implementa o método [getAll] da interface [IDao]. Devolve uma referência à lista de pessoas.
- linhas 46-55: implementa o método [getOne] da interface [IDao]. O seu parâmetro é o ID da pessoa procurada.
Para a recuperar, recorre-se a um método privado [getPosition] das linhas 113-126. Este método devolve a posição na lista da pessoa procurada ou -1 se a pessoa não tiver sido encontrada.
Se a pessoa tiver sido encontrada, o método [getOne] devolve uma referência (linha 51) a uma cópia dessa pessoa e não à própria pessoa. Com efeito, quando um utilizador pretender alterar uma pessoa, as informações sobre a mesma serão solicitadas à camada [dao] e encaminhadas até à camada [web] para alteração, sob a forma de uma referência a um objeto [Personne]. Esta referência servirá de contentor para os dados introduzidos no formulário de alteração. Quando, na camada web, o utilizador enviar as suas alterações, o conteúdo do contentor de dados será alterado. Se o contentor for uma referência à pessoa real do [ArrayList] da camada [dao], então esta é alterada, mesmo que as alterações não tenham sido apresentadas às camadas [service] e [dao]. Esta última é a única habilitada a gerir a lista de pessoas. Por isso, é necessário que a camada web trabalhe com uma cópia da pessoa a ser alterada. Neste caso, a camada [dao] fornece essa cópia.
Se a pessoa procurada não for encontrada, é lançada uma exceção do tipo [DaoException] com o código de erro 2 (linha 53).
- linhas 94-104: implementam o método [deleteOne] da interface [IDao]. O seu parâmetro é o ID da pessoa a eliminar. Se a pessoa a eliminar não existir, é lançada uma exceção do tipo [DaoException] com o código de erro 2.
- linhas 58-91: implementa o método [saveOne] da interface [IDao]. O seu parâmetro é um objeto [Personne]. Se este objeto tiver um id=-1, trata-se de uma adição de pessoa. Caso contrário, trata-se de modificar a pessoa da lista com esse id com os valores do parâmetro.
- linha 60: a validade do parâmetro [Personne] é verificada por um método privado [check] definido nas linhas 129-155. Este método efetua verificações básicas sobre o valor dos diferentes campos de [Personne]. Sempre que é detetada uma anomalia, é lançado um [DaoException] com um código de erro específico. Como o método [saveOne] não trata esta exceção, esta será reenviada para o método chamador.
- linha 62: se o parâmetro [Personne] tiver o seu id igual a -1, trata-se de uma adição. O objeto [Personne] é adicionado à lista interna de pessoas (linha 66), com o primeiro ID disponível (linha 64) e um número de versão igual a 1 (linha 65).
- Se o parâmetro [Personne] tiver um [id] diferente de -1, trata-se de modificar a pessoa da lista interna que possui esse [id]. Em primeiro lugar, verifica-se (linhas 70-75) se a pessoa a modificar existe. Se não for o caso, lança-se uma exceção do tipo [DaoException] com o código de erro 2.
- Se a pessoa estiver efetivamente presente, verifica-se se a sua versão atual é a mesma que a do parâmetro [Personne], que contém as alterações a introduzir no original. Se não for esse o caso, isso significa que quem pretende efetuar a alteração na pessoa não possui a versão mais recente. Isto é-lhe comunicado através do lançamento de uma exceção do tipo [DaoException] com o código de erro 3 (linhas 79-80).
- Se tudo correr bem, as alterações são efetuadas no registo original da pessoa (linhas 85-90)
É evidente que este método tem de ser sincronizado. Por exemplo, entre o momento em que se verifica se a pessoa a modificar existe efetivamente e aquele em que a alteração vai ser efetuada, a pessoa pode ter sido eliminada da lista por outra pessoa. O método deveria, portanto, ser declarado como [synchronized], a fim de garantir que apenas um thread o execute de cada vez. O mesmo se aplica aos outros métodos da interface [IDao]. Não o fazemos, preferindo transferir essa sincronização para a camada [service]. Para destacar os problemas de sincronização, durante os testes da camada [dao], iremos interromper a execução de [saveOne] durante 10 ms (linha 83) entre o momento em que sabemos que podemos efetuar a alteração e o momento em que a efetuamos efetivamente. O thread que executa o [saveOne] perderá então o controlo do processador a favor de outro. Desta forma, aumentamos as nossas hipóteses de observar conflitos de acesso à lista de pessoas.
14.5. Testes da camada [dao]
É escrito um teste JUnit para a camada [dao]:
![]() | ![]() |
[TestDao] é o teste JUnit. Para evidenciar os problemas de acesso simultâneo à lista de pessoas, são criadas threads do tipo [ThreadDaoMajEnfants]. Estas têm como função aumentar em 1 o número de filhos de uma determinada pessoa.
O [TestDao] tem cinco testes, do [test1] ao [test5]. Apresentamos apenas dois deles, convidando o leitor a descobrir os restantes no código-fonte associado a este artigo.
- linha 9: referência à implementação da camada [dao] testada
- linhas 12-15: o construtor do teste JUnit. Este cria uma instância do tipo [DaoImpl] da camada [dao] a ser testada e inicializa-a.
O método [test1] testa os quatro métodos da interface [IDao] da seguinte forma:
- linha 3: solicita-se a lista de pessoas
- linha 6: apresenta-se essa 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]
Em seguida, o teste adiciona uma pessoa, altera-a e elimina-a. Desta forma, são utilizados os quatro métodos da interface [IDao].
- linhas 8-10: adiciona-se uma nova pessoa (id=-1).
- linha 11: recupera-se o id da pessoa adicionada, uma vez que a adição lhe atribuiu um. Antes, ela não tinha nenhum.
- linhas 13-14: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser adicionada. É importante lembrar que, se a pessoa solicitada não for encontrada, a camada [dao] lança uma exceção. Nesse caso, ocorrerá uma falha na linha 13. Este caso poderia ter sido tratado de forma mais adequada. Na linha 14, verifica-se o nome da pessoa encontrada.
- linhas 16-17: altera-se esse nome e solicita-se à camada [dao] que registe as alterações.
- linhas 19-20: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser adicionada e verifica-se o seu novo nome.
- linha 22: elimina-se a pessoa adicionada no início do teste.
- linhas 23-34: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser eliminada. Deve-se obter uma [DaoException] com o código 2.
- linhas 36-37: a lista de pessoas é solicitada novamente. Deve ser obtida a mesma lista do início do teste.
O método [test4] procura evidenciar os problemas de acesso simultâneo aos métodos da camada [dao]. Recorde-se que estes não foram sincronizados. O código do teste é o seguinte:
- linhas 3-6: adiciona-se à lista uma pessoa P sem filhos. Regista-se o seu [id] (linha 6).
- linhas 7-13: lançam-se N threads. Cada um deles irá incrementar o número de filhos da pessoa P em 1 unidade. No final, a pessoa P deverá ter N filhos.
- linhas 15-17: o método [test4], que iniciou as N threads, aguarda que estas concluam o seu trabalho antes de verificar o novo número de filhos da pessoa P.
- linhas 18-21: recupera-se a pessoa P e verifica-se se o seu número de filhos é N.
- linhas 22-35: a pessoa P é eliminada e, em seguida, verifica-se se já não existe na lista.
Na linha 11, verifica-se que os threads são do tipo [ThreadDaoMajEnfants]. O construtor deste tipo tem três parâmetros:
- o nome atribuído ao thread, para que seja possível acompanhá-lo através dos registos
- uma referência à camada [dao] para que o thread tenha acesso à mesma
- o ID da pessoa sobre a qual o thread deve trabalhar
O tipo [ThreadDaoMajEnfants] é o seguinte:
- linha 9: [ThreadDaoMajEnfants] é, de facto, um thread
- linhas 18-22: o construtor que inicializa o thread com três informações
- o nome [name] atribuído ao thread
- uma referência [dao] à camada [dao]. Note-se que, mais uma vez, estamos a trabalhar com o tipo da interface [IDao] e não com o da implementação [DaoImpl].
- o identificador [id] da pessoa sobre a qual o thread deve trabalhar
Quando [test4] inicia um thread [ThreadDaoMajEnfants] (linha 12 de test4), o método [run] (linha 25) deste último é executado:
- linhas 78-81: o método privado [suivi] permite efetuar registos no ecrã. O método [run] utiliza-o para permitir o acompanhamento da execução do thread.
- o thread procura incrementar em 1 o número de filhos da pessoa P com o identificador [id]. Esta atualização pode exigir várias tentativas. Consideremos dois threads: [TH1] e [TH2]. [TH1] solicita uma cópia da pessoa P à camada [dao]. Obtém-na e constata que esta tem a versão V1. O [TH1] é interrompido. O [TH2], que se seguia a ele, faz o mesmo e obtém a mesma versão V1 da pessoa P. O [TH2] é interrompido. O [TH2] retoma o controlo, incrementa o número de filhos de P e guarda as suas alterações. Sabemos que, nessa altura, estas são guardadas e que a versão de P passará para V2. O [TH1] terminou o seu trabalho. [TH2] retoma o controlo e faz o mesmo. A sua atualização de P será recusada, pois possui uma cópia de P com a versão V1, enquanto o P original tem agora a versão V2. O [TH2] tem, então, de repetir todo o ciclo do [lecture -> mise à jour -> sauvegarde]. É por isso que encontramos o ciclo nas linhas 32-72. Nele, o thread:
- solicita uma cópia da pessoa P a modificar (linha 34)
- aguarda 10 ms (linha 43). Isto é artificial e visa interromper o thread entre a leitura da pessoa P e a sua atualização efetiva na lista de pessoas, de modo a aumentar a probabilidade de conflitos.
- incrementa o número de filhos de P (linha 54) e guarda P (linha 56). Se o thread não tiver a versão correta de P, será lançada uma exceção pela camada [dao]. Recupera-se então o código da exceção (linha 61) para verificar se é efetivamente o código 3 (versão incorreta de P). Se não for esse o caso, a exceção é reenviada para o método chamador, que, no final, é o método de teste [test4]. Se ocorrer a exceção com código 3, então recomeça-se o ciclo [lecture -> mise à jour -> sauvegarde]. Se não ocorrer nenhuma exceção, significa que a atualização foi efetuada e o trabalho da thread está concluído.
Quais são os resultados dos testes?
Na primeira configuração testada:
- comentamos a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
- o método [test4] cria 100 threads (linha 8, parágrafo 14.5).
Obtêm-se os seguintes resultados:

Os cinco testes foram bem-sucedidos.
Na segunda configuração testada:
- descomenta-se a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
- o método [test4] cria 2 threads (linha 8, parágrafo 14.5).
Obteêm-se os seguintes resultados:
![]() | ![]() |
O teste [test4] falhou. Foram criadas duas threads, cada uma encarregada de incrementar em 1 o número de filhos de uma pessoa P que, inicialmente, tinha 0. Esperávamos, portanto, 2 filhos após a execução das duas threads, mas só temos um.
Vamos analisar os registos de ecrã do [test4] para compreender o que aconteceu:
- linha 1: o thread n.º 0 inicia o seu trabalho
- linha 2: recuperou uma cópia da pessoa P e verifica que o número de filhos é 0
- linha 3: depara-se com o [Thread.sleep(10)] do seu método [run] e, por isso, pára no tempo [1145536368171] (ms)
- linha 4: o thread n.º 1 recupera então o processador e inicia o seu trabalho
- linha 5: recuperou uma cópia da pessoa P e verifica que o número de filhos é 0
- linha 6: depara-se com o [Thread.sleep(10)] do seu método [run] e, por isso, pára
- linha 7: o thread n.º 0 recupera o processador no momento [1145536368187] (ms), c.a.d. 16 ms depois de o ter perdido.
- linha 8: o mesmo se aplica à thread n.º 1
- linha 9: o thread n.º 0 efetuou a sua atualização e alterou o número de filhos para 1
- linha 10: o thread n.º 1 fez o mesmo
A questão é saber por que razão o thread n.º 1 conseguiu efetuar a sua atualização, quando, normalmente, já não detinha a versão correta da pessoa P, que acabara de ser atualizada pelo thread n.º 0.
Em primeiro lugar, é possível observar uma anomalia entre as linhas 7 e 8: parece que o thread n.º 0 perdeu o controlo do processador entre estas duas linhas, cedendo-o ao thread n.º 1. O que estava ele a fazer nesse momento? Estava a executar o método [saveOne] da camada [dao]. Este método tem a seguinte estrutura (ver parágrafo 14.4):
- o thread n.º 0 executou o [saveOne] e chegou até à linha 8, onde foi obrigado a libertar o processador. Entretanto, leu a versão da pessoa P e esta era 1, porque a pessoa P ainda não tinha sido atualizada.
- Como o processador ficou livre, foi o thread n.º 1 que o herdou. Este, por sua vez, executou o [saveOne] e chegou até à linha 8, onde foi obrigado a libertar o processador. Entretanto, leu a versão da pessoa P e esta era 1, porque a pessoa P ainda não tinha sido atualizada.
- Como o processador ficou livre, foi o thread n.º 0 que o herdou. A partir da linha 9, efetuou a sua atualização e alterou o número de filhos para 1. Em seguida, o método [run] do thread n.º 0 terminou e o thread exibiu o registo que indicava que tinha alterado o número de filhos para 1 (linha 9).
- Como o processador ficou livre, foi a thread n.º 1 que o herdou. A partir da linha 9, efetuou a sua atualização e alterou o número de filhos para 1. Porquê 1? Porque possui uma cópia de P com o número de filhos igual a 0. É o registo (linha 5) que o indica. Em seguida, o método [run] do thread n.º 1 terminou e o thread exibiu o registo que indicava que tinha alterado o número de filhos para 1 (linha 10).
De onde vem o problema? Vem do facto de o thread n.º 0 não ter tido tempo de validar a sua alteração e, portanto, de alterar a versão da pessoa P antes de o thread n.º 1 tentar ler essa versão para saber se a pessoa P tinha mudado. Este cenário é pouco provável, mas não impossível. Foi necessário forçar o thread n.º 0 a perder o controlo do processador para que este caso surgisse com apenas dois threads. Sem este artifício, a configuração anterior não tinha conseguido reproduzir este mesmo caso com 100 threads. O teste [test4] tinha sido bem-sucedido.
Qual é a solução? Existem, sem dúvida, várias. Uma delas, simples de implementar, consiste em sincronizar o método [saveOne]:
public synchronized void saveOne(Personne personne)
A palavra-chave [synchronized] garante que apenas um thread de cada vez possa executar o método. Assim, o thread n.º 1 só será autorizado a executar o [saveOne] quando o thread n.º 0 tiver saído do mesmo. Assim, temos a certeza de que a versão da pessoa P terá sido alterada quando o thread n.º 1 entrar em [saveOne]. A sua atualização será então recusada, uma vez que não terá a versão correta de P.
São estes os quatro métodos da camada [dao] que deveriam ser sincronizados. Decidimos, no entanto, manter esta camada tal como foi descrita e adiar a sincronização para a camada [service]. Para tal, existem várias razões:
- partimos do princípio de que o acesso à camada [dao] se faz sempre através de uma camada [service]. É o que acontece na nossa aplicação web.
- pode ser necessário sincronizar também o acesso aos métodos da camada [service] por outras razões que não aquelas que nos levariam a sincronizar os da camada [dao]. Nesse caso, não é necessário sincronizar os métodos da camada [dao]. Se tivermos a certeza de que:
- todo o acesso à camada [dao] passa pela camada [service]
- que apenas um único thread de cada vez utiliza a camada [service]
então temos a garantia de que os métodos da camada [dao] não serão executados por dois threads ao mesmo tempo.
Vamos agora descobrir a camada [service].
14.6. A camada [service]
A camada [service] é constituída pelas seguintes classes e interfaces:
![]()
- [IService] é a interface apresentada pela camada [dao]
- [ServiceImpl] é uma implementação desta
A interface [IService] é a seguinte:
É idêntica à interface [IDao].
A implementação [ServiceImpl] da interface [IService] é a seguinte:
- linhas 10-19: o atributo [IDao dao] é uma referência à camada [dao]. Será inicializado pelo Spring IoC.
- linhas 22-24: implementação do método [getAll] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
- linhas 27-29: implementação do método [getOne] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
- linhas 32-34: implementação do método [saveOne] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
- linhas 37-39: implementação do método [deleteOne] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
- Todos os métodos são sincronizados (palavra-chave `synchronized`), garantindo que apenas um thread de cada vez possa utilizar a camada [service] e, consequentemente, a camada [dao].
14.7. Testes da camada [service]
É escrito um teste JUnit para a camada [service]:
![]() | ![]() |
[TestService] é o teste JUnit. Os testes realizados são estritamente idênticos aos realizados para a camada [dao]. A estrutura de [TestService] é a seguinte:
- linha 9: a camada [service] testada é do tipo [ServiceImpl].
- linhas 11-15: o construtor do teste JUnit cria uma instância da camada [service] a ser testada (linha 12), cria uma instância da camada [dao] (linha 13) e indica à camada [service] que deve utilizar esta camada [dao] (linha 14).
O método [test1] testa os quatro métodos da interface [IService] da mesma forma que o método de teste da camada [dao] com o mesmo nome. Simplesmente, acede-se à camada [service] (linhas 25, 32, 35) em vez de à camada [dao].
O método [test4] visa identificar problemas de acesso simultâneo aos métodos da camada [service]. É, mais uma vez, idêntico ao método de teste [test4] da camada [dao]. No entanto, existem alguns detalhes que diferem:
- recorre-se à camada [service] em vez da camada [dao] (linha 55)
- passa-se aos threads uma referência à camada [service] em vez de à camada [dao] (linha 61)
O tipo [ThreadServiceMajEnfants] é também praticamente idêntico ao tipo [ThreadDaoMajEnfants], com a única diferença de que trabalha com a camada [service] e não com a camada [dao]:
- linha 12: o thread funciona com a camada [service]
Realizamos os testes com a configuração que causou o problema na camada [dao]:
- descomentamos a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
- O método [test4] cria 100 threads (linha 65, parágrafo 14.7).
Os resultados obtidos são os seguintes:
![]() |
Foi a sincronização dos métodos da camada [service] que permitiu o sucesso do teste [test4].
14.8. A camada [web]
Recorde-se a arquitetura de três camadas da nossa aplicação:
![]() |
A camada [web] irá apresentar ecrãs ao utilizador para que este possa gerir o grupo de pessoas:
- lista das pessoas do grupo
- adição de uma pessoa ao grupo
- alteração de uma pessoa do grupo
- eliminação de uma pessoa do grupo
Para tal, irá basear-se na camada [service], que, por sua vez, recorrerá à camada [dao]. Já apresentámos os ecrãs geridos pela camada [web] (parágrafo 14.1). Para descrever a camada web, iremos apresentar sucessivamente:
- a sua configuração
- as suas vistas
- o seu controlador
- alguns testes
14.8.1. Configuração da aplicação web
O projeto Eclipse da aplicação é o seguinte:

- no pacote [istia.st.mvc.personnes.web], encontra-se o controlador [Application].
- As páginas JSP / JSTL encontram-se em [WEB-INF/vues].
- A pasta [lib] contém os ficheiros de terceiros necessários à aplicação. Estes estão visíveis na pasta [Web App Libraries].
[web.xml]
O ficheiro [web.xml] é o ficheiro utilizado pelo servidor web para carregar a aplicação. O seu conteúdo é o seguinte:
<?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>
<!-- Mapeamento de ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- ficheiros de início -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Página de erro inesperado -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- linhas 27-30: as URLs [/do/*] serão processadas pelo servlet [personnes]
- linhas 9-12: o servlet [personnes] é uma instância da classe [Application], uma classe que iremos criar.
- linhas 13-24: definem três parâmetros [urlList, urlEdit, urlErreurs] que identificam as URLs das páginas JSP das vistas [list, edit, erreurs].
- linhas 32-34: a aplicação tem uma página inicial predefinida [index.jsp], que se encontra na raiz da pasta da aplicação web.
- linhas 36-39: a aplicação tem uma página de erros predefinida que é apresentada quando o servidor web deteta uma exceção não gerida pela aplicação.
- linha 37: a baliza <exception-type> indica o tipo de exceção gerida pela diretiva <error-page>, neste caso o tipo [java.lang.Exception] e derivados, ou seja, todas as exceções.
- linha 38: a baliza <location> indica a página JSP a ser apresentada quando ocorre uma exceção do tipo definido por <exception-type>. A exceção ocorrida está disponível nessa página num objeto denominado «exception», se a página tiver a diretiva:
<%@ page isErrorPage="true" %>
- (continuação)
- se <exception-type> especificar um tipo T1 e se uma exceção do tipo T2, não derivada de T1, for reportada ao servidor web, este envia ao cliente uma página de exceção proprietária, geralmente pouco intuitiva. Daí a importância da baliza <error-page> no ficheiro [web.xml].
[index.jsp]
Esta página é apresentada se um utilizador solicitar diretamente o contexto da aplicação sem especificar um URL, c.a.d. Aqui, [/personnes-01]. O seu conteúdo é o seguinte:
<%@ 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] redireciona o cliente para a URL [/do/list]. Esta URL apresenta a lista de pessoas do grupo.
14.8.2. As páginas JSP / JSTL da aplicação
A vista [list.jsp]
Serve para apresentar a lista de pessoas:

O seu código é o seguinte:
<%@ 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 recebe um elemento no seu modelo:
- o elemento [personnes] associado a um objeto do tipo [ArrayList] de objetos do tipo [Personne]
- linhas 22-34: percorre-se a lista ${personas} para apresentar uma tabela HTML contendo as pessoas do grupo.
- linha 31: o URL para o qual aponta o link [Modifier] é definido pelo campo [id] da pessoa atual, para que o controlador associado ao URL [/do/edit] saiba qual é a pessoa a modificar.
- linha 32: o mesmo se aplica ao link [Supprimer].
- linha 28: para apresentar a data de nascimento da pessoa no formato JJ/MM/AAAA, utiliza-se a baliza <dt> da biblioteca de balizas [DateTime] do projeto Apache [Jakarta Taglibs]:

O ficheiro de descrição desta biblioteca de tags está definido na linha 3.
- linha 37: o link [Ajout] para adicionar uma nova pessoa tem como destino o URL [/do/edit], tal como o link [Modifier] da linha 31. É o valor -1 do parâmetro [id] que indica que se trata de uma adição e não de uma modificação.
A vista [edit.jsp]
Serve para apresentar o formulário de adição de uma nova pessoa ou de alteração de uma pessoa existente:
![]() |
O código da vista [edit.jsp] é o seguinte:
<%@ 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 apresenta um formulário para adicionar uma nova pessoa ou atualizar uma pessoa existente. Daqui em diante, e para simplificar a redação, utilizaremos apenas o termo [mise à jour]. O botão [Valider] (linha 73) aciona o POST do formulário na URL [/do/validate] (linha 16). Se o POST falhar, a vista [edit.jsp] é novamente apresentada com o(s) erro(s) que ocorreram; caso contrário, é apresentada a vista [list.jsp].
- A vista [edit.jsp], apresentada tanto num GET como num POST que falhe, recebe os seguintes elementos no seu modelo:
atributo | GET | POST |
identificador da pessoa atualizada | idem | |
a sua versão | idem | |
o seu nome próprio | nome próprio introduzido | |
o seu apelido | apelido introduzido | |
data de nascimento | data de nascimento introduzida | |
estado civil | estado civil introduzido | |
o número de filhos | número de filhos introduzido | |
em branco | uma mensagem de erro a indicar que a adição ou alteração falhou no momento do POST, provocada pelo botão [Envoyer]. Vazio se não houver erro. | |
vazio | indica um nome próprio incorreto – vazio caso contrário | |
vazio | indica um apelido incorreto – vazio caso contrário | |
vazio | indica uma data de nascimento incorreta – vazio caso contrário | |
vazio | indica um número de filhos incorreto – vazio caso contrário |
- linhas 11-15: se o POST do formulário falhar, teremos [erreurEdit!=''] e será exibida uma mensagem de erro.
- linha 16: o formulário será enviado para o URL [/do/validate]
- linha 20: o elemento [id] do modelo é apresentado
- linha 24: o elemento [version] do modelo é apresentado
- linhas 26-32: introdução do nome próprio da pessoa:
- aquando da exibição inicial do formulário (GET), ${prenom} exibe o valor atual do campo [prenom] do objeto [Personne] atualizado e ${erreurPrenom} está vazio.
- em caso de erro após o POST, volta a ser apresentado o valor introduzido ${prenom}, bem como a eventual mensagem de erro ${erreurPrenom}
- linhas 33-39: introdução do apelido da pessoa
- linhas 40-46: introdução da data de nascimento da pessoa
- linhas 47-61: introdução do estado civil da pessoa através de um botão de opção. Utiliza-se o valor do campo [marie] do objeto [Personne] para determinar qual dos dois botões de opção deve ser selecionado.
- linhas 62-68: introdução do número de filhos da pessoa
- linha 71: um campo HTML oculto, denominado [id], cujo valor corresponde ao campo [id] da pessoa que está a ser atualizada; -1 para uma adição, outro valor para uma alteração.
- linha 72: um campo oculto HTML, denominado [version], cujo valor corresponde ao campo [id] da pessoa que está a ser atualizada.
- linha 73: o botão [Valider] do tipo [Submit] do formulário
- linha 74: um link que permite regressar à lista de pessoas. Foi denominado [Annuler] porque permite sair do formulário sem o validar.
A vista [exception.jsp]
Serve para apresentar uma página a indicar que ocorreu uma exceção não gerida pela aplicação e que foi encaminhada para o servidor web.
Por exemplo, vamos eliminar uma pessoa que não existe no grupo:
![]() |
O código da vista [exception.jsp] é o seguinte:
<%@ 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 recebe uma chave no seu modelo, o elemento [exception], que corresponde à exceção que foi interceptada pelo servidor web. Para que este elemento seja incluído no modelo da página JSP pelo servidor web, é necessário que a página tenha definido a baliza da linha 3.
- linha 6: define-se o código de estado HTTP da resposta como 200. Trata-se do primeiro cabeçalho HTTP da resposta. O código 200 indica ao cliente que o seu pedido foi atendido. Normalmente, um documento HTML foi incluído na resposta do servidor. É o que acontece neste caso. Se o código de estado HTTP da resposta não for definido como 200, terá aqui o valor 500, o que significa que ocorreu um erro. Com efeito, o servidor web, ao interceptar uma exceção não gerida, considera esta situação anómala e sinaliza-a através do código 500. A reação ao código 500 difere consoante os navegadores: o Firefox apresenta o documento que pode acompanhar esta resposta, enquanto o Firefox ignora esse documento e apresenta a sua própria página. É por esta razão que substituímos o código 500 pelo código 200.
- linha 16: o texto da exceção é apresentado
- linha 18: é apresentado ao utilizador um link para regressar à lista de pessoas
A vista [erreurs.jsp]
Serve para apresentar uma página que sinaliza os erros de inicialização da aplicação, c.a.d, e os erros detetados durante a execução do método [init] do servlet do controlador. Pode tratar-se, por exemplo, da ausência de um parâmetro no ficheiro [web.xml], tal como se pode ver no exemplo abaixo:

O código da página [erreurs.jsp] é o seguinte:
<%@ 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>
A página recebe no seu modelo um elemento [erreurs], que é um objeto do tipo [ArrayList] de objetos [String], sendo estes últimos mensagens de erro. São apresentadas pelo ciclo das linhas 13-15.
14.8.3. O controlador da aplicação
O controlador [Application] está definido no pacote [istia.st.mvc.personnes.web]:
![]()
Estrut tura e inicialização do controlador
A estrutura do controlador [Application] é a seguinte:
- linhas 20-36: recuperam-se os parâmetros esperados no ficheiro [web.xml].
- linhas 39-41: o parâmetro [urlErreurs] deve estar obrigatoriamente presente, pois indica o URL da vista [erreurs] capaz de apresentar eventuais erros de inicialização. Se não existir, a aplicação é interrompida através do lançamento de uma [ServletException] (linha 40). Esta exceção será encaminhada para o servidor web e tratada pela baliza <error-page> do ficheiro [web.xml]. A vista [exception.jsp] é, portanto, apresentada:

O link [Retour à la liste] acima não funciona. Ao utilizá-lo, obtém-se a mesma resposta enquanto a aplicação não for alterada e recarregada. É útil para outros tipos de exceções, como já vimos.
- linha 43: cria uma instância [DaoImpl] que implementa a camada [dao]
- linha 44: inicializa esta instância (criação de uma lista inicial de três pessoas)
- linha 46: cria uma instância [ServiceImpl] que implementa a camada [service]
- linha 47: inicializa a camada [service], atribuindo-lhe uma referência à camada [dao]
Após a inicialização do controlador, os seus métodos dispõem de uma referência [service] à camada [service] (linha 15), que irão utilizar para executar as ações solicitadas pelo utilizador. Estas serão interceptadas pelo método [doGet], que as encaminhará para serem processadas por um método específico do controlador:
Url | Método HTTP | método do controlador |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
O método [doGet]
Este método tem como objetivo encaminhar o processamento das ações solicitadas pelo utilizador para o método correto. O seu código é o seguinte:
- linhas 7-13: verifica-se se a lista de erros de inicialização está vazia. Se não for esse o caso, é apresentada a vista [erreurs(erreurs)], que irá sinalizar o(s) erro(s).
- linha 15: recupera-se o método [get] ou [post] que o cliente utilizou para efetuar o seu pedido.
- linha 17: recupera-se o valor do parâmetro [action] da consulta.
- linhas 23-27: processamento da consulta [GET /do/list], que solicita a lista de pessoas.
- linhas 28-32: processamento da solicitação [GET /do/delete], que solicita a eliminação de uma pessoa.
- linhas 33-37: processamento da consulta [GET /do/edit], que solicita o formulário de atualização de uma pessoa.
- linhas 38-42: processamento da solicitação [POST /do/validate], que solicita a validação da pessoa atualizada.
- linha 44: se a ação solicitada não for uma das cinco anteriores, então procede-se como se fosse [GET /do/list].
O método [doListPersonnes]
Este método processa a solicitação [GET /do/list], que solicita a lista de pessoas:

O seu código é o seguinte:
- linha 5: solicita-se à camada [service] a lista de pessoas do grupo e esta é inserida no modelo sob a chave «pessoas».
- linha 7: exibe-se a vista [list.jsp] descrita no parágrafo 14.8.2.
O método [doDeletePersonne]
Este método processa a consulta [GET /do/delete?id=XX], que solicita a eliminação da pessoa com id=XX. A URL [/do/delete?id=XX] corresponde aos links [Supprimer] da vista [list.jsp]:

cujo código é o seguinte:
Na linha 12, vemos a URL [/do/delete?id=XX] do link [Supprimer]. O método [doDeletePersonne], que deve processar esta URL, deve remover a pessoa com id=XX e, em seguida, apresentar a nova lista de pessoas do grupo. O seu código é o seguinte:
- linha 5: a URL processada tem o formato [/do/delete?id=XX]. Recupera-se o valor [XX] do parâmetro [id].
- linha 7: solicita-se à camada [service] a eliminação da pessoa com o ID obtido. Não fazemos qualquer verificação. Se a pessoa que se pretende eliminar não existir, a camada [dao] lança uma exceção que é propagada pela camada [service]. Também não a tratamos aqui, no controlador. Por conseguinte, a exceção será encaminhada até ao servidor web, que, por configuração, fará com que seja apresentada a página [exception.jsp], descrita no parágrafo 14.8.2:

- linha 9: se a eliminação tiver ocorrido (sem exceção), solicita-se ao cliente que seja redirecionado para a URL relativa [list]. Como a página que acabou de ser processada é a [/do/delete], a URL de redirecionamento será [/do/list]. O navegador será, assim, levado a aceder à página [GET /do/list], o que provocará a exibição da lista de pessoas.
O método [doEditPersonne]
Este método processa a solicitação [GET /do/edit?id=XX], que solicita o formulário de atualização da pessoa com id=XX. A URL [/do/edit?id=XX] é a dos links [Modifier] e a do link [Ajout] da vista [list.jsp]:

cujo código é o seguinte:
Na linha 11, vemos o URL [/do/edit?id=XX] do link [Modifier] e, na linha 17, o URL [/do/edit?id=-1] do link [Ajout]. O método [doEditPersonne] deve apresentar o formulário de edição da pessoa com id=XX ou, caso se trate de uma adição, apresentar um formulário em branco.
![]() | ![]() |
O código do método [doEditPersonne] é o seguinte:
- o GET tem como destino uma URL do tipo [/do/edit?id=XX]. Na linha 5, recuperamos o valor de [id]. Em seguida, há dois casos:
- o id é diferente de -1. Nesse caso, trata-se de uma alteração e é necessário apresentar um formulário pré-preenchido com as informações da pessoa a ser alterada. Na linha 10, essa pessoa é solicitada à camada [service].
- Se o id for igual a -1, trata-se de uma adição e é necessário apresentar um formulário vazio. Para tal, é criado um registo vazio nas linhas 13-14.
- O objeto [Personne] obtido é inserido no modelo da página [edit.jsp] descrito no parágrafo 14.8.2. Este modelo inclui os seguintes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Estes elementos são inicializados nas linhas 17 a 30, com exceção daqueles cujo valor é a cadeia vazia [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Sabe-se que, na sua ausência no modelo, a biblioteca JSTL exibirá uma cadeia vazia como seu valor. Embora o elemento [erreurEdit] também tenha como valor uma cadeia vazia, é, no entanto, inicializado, pois é efetuada uma verificação do seu valor na página [edit.jsp].
- Assim que o modelo estiver pronto, o controlo passa para a página [edit.jsp], linhas 32-33, que irá gerar a vista [edit].
O método [doValidatePersonne]
Este método processa a solicitação [POST /do/validate] que valida o formulário de atualização. Esta solicitação POST é acionada pelo botão [Valider]:

Recorde-se os campos de preenchimento do formulário HTML da vista acima:
A consulta POST contém os parâmetros [prenom, nom, dateNaissance, marie, nbEnfants, id, version] e é enviada para o URL [/do/validate] (linha 1). É processada pelo seguinte método [doValidatePersonne]:
- linhas 8-14: o parâmetro [prenom] da solicitação POST é recuperado e a sua validade é verificada. Se estiver incorreto, o elemento [erreurPrenom] é inicializado com uma mensagem de erro e colocado nos atributos da consulta.
- linhas 16-22: procede-se de forma semelhante para o parâmetro [nom]
- linhas 24-32: procede-se de forma semelhante para o parâmetro [dateNaissance]
- linha 34: recupera-se o parâmetro [marie]. Não se verifica a sua validade porque, a priori, provém do valor de um botão de opção. Dito isto, nada impede que um programa crie um [POST /personnes-01/do/validate] acompanhado de um parâmetro [marie] inventado. Devemos, portanto, testar a validade deste parâmetro. Aqui, contamos com o nosso mecanismo de gestão de exceções, que provoca a exibição da página [exception.jsp] caso o controlador não as gere por si próprio. Assim, se a conversão do parâmetro [marie] em booleano falhar na linha 34, será gerada uma exceção que resultará no envio da página [exception.jsp] ao cliente. Este funcionamento é o que pretendemos.
- linhas 34-54: recuperamos o parâmetro [nbEnfants] e verificamos o seu valor.
- linha 56: recupera-se o parâmetro [id] sem verificar o seu valor
- linha 58: faz-se o mesmo com o parâmetro [version]
- linhas 60-65: se o formulário estiver incorreto, é exibido novamente com as mensagens de erro criadas anteriormente
- linhas 67-69: se for válido, cria-se um novo objeto [Personne] com os elementos do formulário
- linhas 70-78: o utilizador é guardado. O processo de gravação pode falhar. Num ambiente multiutilizadores, o utilizador a ser alterado pode ter sido eliminado ou já ter sido alterado por outra pessoa. Neste caso, a camada [dao] irá lançar uma exceção que é tratada aqui.
- linha 80: se não tiver ocorrido nenhuma exceção, redirecionamos o cliente para o URL [/do/list] para lhe apresentar o novo estado do grupo.
- linha 75: se tiver ocorrido uma exceção durante o gravação, solicitamos novamente a exibição do formulário inicial, passando-lhe a mensagem de erro da exceção (3.º parâmetro).
O método [showFormulaire] (linhas 84-101) constrói o modelo necessário para a página [edit.jsp] com os valores introduzidos (request.getParameter(" ... ")). Recorde-se que as mensagens de erro já foram inseridas no modelo pelo método [doValidatePersonne]. A página [edit.jsp] é apresentada nas linhas 99-100.
14.9. Testes da aplicação web
Foram apresentados vários testes no parágrafo 14.1. Convidamos o leitor a repeti-los. Apresentamos aqui outras capturas de ecrã que ilustram casos de conflitos de acesso aos dados num ambiente multiutilizador:
[Firefox] será o navegador do utilizador U1. Este solicita o URL [http://localhost:8080/personnes-01]:

[IE] será o navegador do utilizador U2. Este solicita a mesma URL:

O utilizador U1 acede à edição do perfil da pessoa [Lemarchand]:

O utilizador U2 faz o mesmo:

O utilizador U1 efetua alterações e valida:
![]() |
O utilizador U2 faz o mesmo:
![]() |
O utilizador U2 volta à lista de pessoas através do link [Annuler] do formulário:

Encontra a pessoa [Lemarchand] tal como U1 a alterou. Agora, U2 elimina [Lemarchand]:
![]() |
U1 continua a ter a sua própria lista e pretende alterar [Lemarchand] novamente:
![]() |
O U1 utiliza o link [Retour à la liste] para ver do que se trata:

Descobre que, de facto, o [Lemarchand] já não faz parte da lista...
14.10. Conclusion
Implementámos a arquitetura MVC numa arquitetura de três camadas [web, metier, dao], utilizando um exemplo básico de gestão de uma lista de pessoas. Isto permitiu-nos utilizar os conceitos que tinham sido apresentados nas secções anteriores. Na versão analisada, a lista de pessoas era mantida na memória. Em breve, iremos analisar versões em que essa lista será mantida numa tabela de base de dados.
Mas, antes disso, vamos apresentar uma ferramenta chamada Spring IoC, que facilita a integração das diferentes camadas de uma aplicação ntier.

















