Skip to content

14. Aplicativo web MVC em uma arquitetura de três camadas – Exemplo 1

14.1. Présentation

Até agora, limitamo-nos a exemplos com finalidade didática. Por isso, eles precisavam ser simples. Apresentamos agora uma aplicação básica, mas, ainda assim, mais rica do que todas as apresentadas até o momento. Ela terá a particularidade de utilizar as três camadas de uma arquitetura de três camadas:

Image

Convidamos o leitor a reler os princípios de uma aplicação web MVC em uma arquitetura de três camadas, caso os tenha esquecido, no parágrafo 4.

A aplicação web que vamos desenvolver permitirá gerenciar um grupo de pessoas por meio de quatro operações:

  • lista das pessoas do grupo
  • adição de uma pessoa ao grupo
  • alteração de uma pessoa do grupo
  • exclusão de uma pessoa do grupo

Essas quatro operações básicas correspondem às operações de uma tabela de banco de dados. Vamos desenvolver duas versões dessa aplicação:

  • na versão 1, a camada [dao] não utilizará um banco de dados. As pessoas do grupo serão armazenadas em um objeto simples [ArrayList], gerenciado internamente pela camada [dao]. Isso permitirá que o leitor teste a aplicação sem a restrição de um banco de dados.
  • Na versão 2, colocaremos o grupo de pessoas em uma tabela de banco de dados. Mostraremos que isso será feito sem impacto na camada web da versão 1, que permanecerá inalterada.

As capturas de tela a seguir mostram as páginas que o aplicativo exibe ao usuário.

Image

Image

Image

 

14.2. O projeto Eclipse

O projeto do aplicativo se chama [personnes-01]:

Image

Este projeto abrange as três camadas da arquitetura de três camadas do aplicativo:

  • 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 compartilhados 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 muito demorado escrever e talvez muito enfadonho de ler, talvez sejamos um pouco rápidos nas explicações em alguns momentos, exceto quando o que for apresentado for novo.

14.3. A representação de uma pessoa

O aplicativo gerencia um grupo de pessoas. As capturas de tela do parágrafo 14.1 mostraram algumas das características de uma pessoa. Formalmente, elas são representadas pela classe [Personne]:

Image

A classe [Personne] é a seguinte:

package istia.st.springmvc.personnes.entites;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Personne {

     // identificador único da pessoa
    private int id;
     // a versão atual
    private long version;
     // o sobrenome
    private String nom;
     // o nome de batismo
    private String prenom;
     // data de nascimento
    private Date dateNaissance;
     // estado civil
    private boolean marie = false;
     // número de filhos
    private int nbEnfants;

     // getters - setters
...

     // construtor padrão
    public Personne() {

    }

     // construtor com inicialização dos campos da pessoa
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

     // construtor de uma pessoa por cópia de outra pessoa
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


     // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • uma pessoa é identificada pelas seguintes informações:
    • id: um número que identifica uma pessoa de forma exclusiva
    • sobrenome: o sobrenome da pessoa
    • nome próprio: seu nome próprio
    • dateNaissance: sua data de nascimento
    • casada: se a pessoa é casada ou não
    • nbEnfants: o número de filhos
  • O atributo [version] é um atributo adicionado artificialmente para atender às necessidades do aplicativo. Do ponto de vista orientada a objetos, sem dúvida teria sido preferível adicionar esse atributo em uma classe derivada de [Personne]. Sua necessidade surge quando se analisam os casos de uso do aplicativo web. Um deles é o seguinte:

No momento T1, um usuário U1 acessa a edição de uma pessoa P. Nesse momento, o número de filhos é 0. Ele altera esse número para 1, mas, antes de confirmar a alteração, um usuário U2 acessa a edição da mesma pessoa P. Como U1 ainda não confirmou sua alteração, U2 vê o número de filhos como 0. U2 altera o nome da pessoa P para letras maiúsculas. Em seguida, U1 e U2 confirmam suas alterações nessa ordem. É a modificaçã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 acredite ter alterado esse valor para 1.

O conceito de versão de pessoa nos ajuda a resolver esse problema. Vamos retomar o mesmo caso de uso:

No momento T1, um usuário U1 inicia a 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 confirmar a alteração, um usuário U2 inicia a edição da mesma pessoa P. Como U1 ainda não confirmou sua alteração, U2 vê o número de filhos como 0 e a versão como V1. U2 altera o nome da pessoa P para letras maiúsculas. Em seguida, U1 e U2 validam suas alterações nessa ordem. Antes de validar uma alteração, verifica-se se quem está alterando a pessoa P possui a mesma versão que a pessoa P atualmente registrada. Esse será o caso do usuário U1. Sua alteração é, portanto, aceita e, então, altera-se a versão da pessoa modificada de V1 para V2 para indicar que a pessoa sofreu uma alteração. Ao validar a modificação de U2, perceberemos que ele possui uma versão V1 da pessoa P, enquanto que, atualmente, a versão desta é V2. Será então possível informar ao usuário U2 que alguém já agiu antes dele e que ele deve partir da nova versão da pessoa P. Ele fará isso, recuperará uma pessoa P da versão V2, que agora tem um filho, colocará o nome em maiúsculas e validará. Sua modificação será aceita se a pessoa P registrada ainda tiver a versão V2. No final, as modificações feitas por U1 e U2 serão consideradas, enquanto que, no caso de uso sem versão, uma das modificações seria perdida.

  • 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. Temos, então, dois objetos com conteúdo idêntico, mas referenciados por dois ponteiros diferentes.
  • linha 55: o método [toString] é redefinido para retornar uma sequência de caracteres representando o estado da pessoa

14.4. A camada [dao]

A camada [dao] é composta pelas seguintes classes e interfaces:

Image

  • [IDao] é a interface apresentada pela camada [dao]
  • [DaoImpl] é uma implementação dessa interface, na qual o grupo de pessoas está encapsulado em um objeto [ArrayList]
  • [DaoException] é um tipo de exceção não verificada (unchecked), lançada pela camada [dao]

A interface [IDao] é a seguinte:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // lista de todas as pessoas
    Collection getAll();
     // obter uma pessoa específica
    Personne getOne(int id);
     // adicionar/editar uma pessoa
    void saveOne(Personne personne);
     // excluir uma pessoa
    void deleteOne(int id);
}
  • A interface possui quatro métodos para as quatro operações que se deseja realizar no grupo de pessoas:
    • getAll: para obter uma coleção de pessoas
    • getOne: para obter uma pessoa com um id específico
    • saveOne: para adicionar uma pessoa (id=-1) ou modificar uma pessoa existente (id <> -1)
    • deleteOne: para excluir uma pessoa com um id específico

A camada [dao] pode gerar exceções. Essas exceções serão do tipo [DaoException] :

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

     // código de erro
    private int code;

    public int getCode() {
        return code;
    }

// fabricante
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • linha 3: a classe [DaoException], derivada de [RuntimeException], é um tipo de exceção não controlada: o compilador não nos obriga a:
    • tratar esse tipo de exceção com um try/catch ao chamar 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

Essa técnica evita que tenhamos que 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á, portanto, aceitável, proporcionando assim flexibilidade à arquitetura.

  • linha 6: um código de erro. A camada [dao] lançará diversas exceções que serão identificadas por códigos de erro diferentes. Isso permitirá que a camada responsável por gerenciar a exceção conheça a origem exata do erro e, assim, tome as medidas adequadas. Existem outras maneiras de chegar ao mesmo resultado. Uma delas é 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 e uma mensagem de erro.
  • linhas 8-10: o método que permitirá que o código de gerenciamento de uma exceção recupere o código de erro.

A classe [DaoImpl] implementa a interface [IDao]:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

     // uma lista de pessoas
    private ArrayList personnes = new ArrayList();

     // nº da próxima pessoa
    private int id = 0;

     // inicializações
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

     // lista de pessoas
    public Collection getAll() {
        return personnes;
    }

     // obter uma pessoa específica
    public Personne getOne(int id) {
         // está-se procurando a pessoa
        int i = getPosition(id);
         // foi encontrada?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

     // adicionar ou modificar uma pessoa
    public void saveOne(Personne personne) {
         // o parâmetro “pessoa” é válido?
        check(personne);
         // adição ou modificação?
        if (personne.getId() == -1) {
             // adição
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
         // alteração — procurando a pessoa
        int i = getPosition(personne.getId());
         // foi encontrada?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
         // Temos a versão correta do original?
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
         // aguardando 10 ms
         //wait(10);
         // Tudo certo — vamos fazer a modificação
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

     // exclusão de uma pessoa
    public void deleteOne(int id) {
         // procurando a pessoa
        int i = getPosition(id);
         // encontramos?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
             // excluindo a pessoa
            personnes.remove(i);
        }
    }

     // gerador de ID
    private int getNextId() {
        id++;
        return id;
    }

     // procurar uma pessoa
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
         // percorremos a lista de pessoas
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
         // resultado?
        return trouvé ? i : -1;
    }

     // verificação de uma pessoa
    private void check(Personne p) {
         // pessoa p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
         // ID
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
         // data de nascimento
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
         // número de filhos
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
         // sobrenome
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
         // nome
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

     // expectativa
    private void wait(int N) {
         // espera-se N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // exibe o rastreamento da exceção
            e.printStackTrace();
            return;
        }
    }
}

Apresentaremos apenas as linhas gerais desse código. No entanto, dedicaremos um pouco de tempo às partes mais delicadas.

  • linha 13: o objeto [ArrayList], que conterá o grupo de pessoas
  • linha 16: o identificador da última pessoa adicionada. A cada nova adição, esse identificador será incrementado em 1.

A classe [DaoImpl] será instanciada em uma única instância. Isso é o que chamamos de singleton. Um aplicativo web atende seus usuários simultaneamente. Em um determinado momento, há várias threads sendo executadas pelo servidor web. Elas compartilham os singletons:

  • o da camada [dao]
  • da camada [service]
  • os das diferentes controladoras, validadores de dados, etc., da camada web

Se um singleton tiver campos privados, é preciso questionar imediatamente por que ele os possui. Eles são justificados? Afinal, eles serão compartilhados entre diferentes threads. Se forem somente de leitura, isso não representa problema, desde que possam ser inicializados em um momento em que se tenha certeza de que há apenas um thread ativo. Geralmente, sabemos identificar esse momento. É o momento do início da aplicação web, quando ela ainda não começou a atender clientes. Se forem de leitura/gravação, é preciso implementar uma sincronização de acesso aos campos; caso contrário, corremos para o desastre. Ilustraremos esse problema quando testarmos a camada [dao].

  • A classe [DaoImpl] não possui construtor. Portanto, será utilizado seu construtor padrão.
  • linhas 19-38: o método [init] será chamado no momento da instanciação do singleton da camada [dao]. Ele cria uma lista com três pessoas.
  • linhas 41-43: implementam o método [getAll] da interface [IDao]. Ele retorna uma referência à lista de pessoas.
  • linhas 46-55: implementa o método [getOne] da interface [IDao]. Seu parâmetro é o ID da pessoa procurada.

Para recuperá-la, recorre-se a um método privado [getPosition], nas linhas 113-126. Esse método retorna a posição na lista da pessoa procurada ou -1, caso a pessoa não tenha sido encontrada.

Se a pessoa tiver sido encontrada, o método [getOne] retorna uma referência (linha 51) a uma cópia dessa pessoa e não à própria pessoa. De fato, quando um usuário quiser modificar uma pessoa, as informações sobre ela serão solicitadas à camada [dao] e encaminhadas até a camada [web] para modificação, na forma de uma referência a um objeto [Personne]. Essa referência servirá como contêiner de entradas no formulário de modificação. Quando, na camada web, o usuário enviar suas modificações, o conteúdo do contêiner de entradas será alterado. Se o contêiner for uma referência à pessoa real do [ArrayList] da camada [dao], então essa pessoa é alterada mesmo que as alterações não tenham sido apresentadas às camadas [service] e [dao]. Esta última é a única habilitada a gerenciar a lista de pessoas. Portanto, é necessário que a camada web trabalhe com uma cópia da pessoa a ser alterada. Nesse 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]. Seu parâmetro é o ID da pessoa a ser excluída. Se a pessoa a ser excluída não existir, uma exceção do tipo [DaoException] é lançada com o código de erro 2.
  • linhas 58-91: implementa o método [saveOne] da interface [IDao]. Seu parâmetro é um objeto [Personne]. Se esse 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. Esse método realiza verificações básicas sobre o valor dos diferentes campos de [Personne]. Sempre que uma anomalia é detectada, é lançado um [DaoException] com um código de erro específico. Como o método [saveOne] não lida com essa exceção, ela será repassada ao método chamador.
    • linha 62: se o parâmetro [Personne] tiver 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]. Primeiramente, verifica-se (linhas 70-75) se a pessoa a ser modificada existe. Caso contrário, lança-se uma exceção do tipo [DaoException] com o código de erro 2.
    • Se a pessoa estiver presente, verifica-se se sua versão atual é a mesma que a do parâmetro [Personne], que contém as alterações a serem feitas no original. Caso contrário, isso significa que quem deseja fazer a alteração na pessoa não possui a versão mais recente. Isso é comunicado a ele por meio 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 feitas no registro original da pessoa (linhas 85-90)

É evidente que esse método precisa ser sincronizado. Por exemplo, entre o momento em que se verifica se a pessoa a ser modificada realmente existe e o momento em que a modificação será feita, ela pode ter sido excluída da lista por outra pessoa. O método deveria, portanto, ser declarado como [synchronized] para garantir que apenas um thread por vez o execute. O mesmo se aplica aos outros métodos da interface [IDao]. Não fazemos isso, preferindo transferir essa sincronização para a camada [service]. Para destacar os problemas de sincronização, durante os testes da camada [dao], interromperemos a execução de [saveOne] por 10 ms (linha 83) entre o momento em que sabemos que podemos fazer a modificação e o momento em que a fazemos de fato. O thread que executa o [saveOne] perderá então o processador em favor de outro. Assim, aumentamos nossas chances de observar conflitos de acesso à lista de pessoas.

14.5. Testes da camada [dao]

Um teste JUnit é escrito para a camada [dao]:

[TestDao] é o teste JUnit. Para destacar os problemas de acesso simultâneo à lista de pessoas, são criadas threads do tipo [ThreadDaoMajEnfants]. Elas têm a função de aumentar em 1 o número de filhos de uma determinada pessoa.

[TestDao] possui cinco testes, de [test1] a [test5]. Apresentamos apenas dois deles; o leitor é convidado a descobrir os demais no código-fonte associado a este artigo.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

     // camada [dao]
    private DaoImpl dao;

     // construtor
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

     // lista de pessoas
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

     // teste1
    public void test1() throws ParseException {
...
    }

     // alteração/exclusão de um elemento inexistente
    public void test2() throws ParseException {
...
    }

     // gerenciamento de versões de pessoas
    public void test3() throws ParseException, InterruptedException {
...
    }

     // bloqueio otimista – acesso multithread
    public void test4() throws Exception {
...
    }

     // testes de validade de saveOne
    public void test5() throws ParseException {
    ...
}
  • linha 9: referência à implementação da camada [dao] testada
  • linhas 12-15: o construtor do teste JUnit. Ele cria uma instância do tipo [DaoImpl] da camada [dao] a ser testada e a inicializa.

O método [test1] testa os quatro métodos da interface [IDao] da seguinte maneira:

    public void test1() throws ParseException {
         // lista atual
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
         // exibição
        doListe(personnes);
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // verificação — ocorrerá uma falha se a pessoa não for encontrada
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
         // alteração
        p1.setNom("Y");
        dao.saveOne(p1);
         // verificação — ocorrerá uma falha se a pessoa não for encontrada
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
         // exclusão
        dao.deleteOne(id1);
         // verificação
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve ocorrer um erro de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // lista de pessoas
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • linha 3: solicita-se a lista de pessoas
  • linha 6: exibe-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, a modifica e a exclui. Assim, os quatro métodos da interface [IDao] são utilizados.

  • linhas 8-10: adiciona-se uma nova pessoa (id=-1).
  • linha 11: recupera-se o id da pessoa adicionada, pois a adição atribuiu um a ela. Antes, ela não tinha nenhum.
  • linhas 13-14: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser adicionada. É preciso 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. Esse caso poderia ter sido tratado de forma mais adequada. Na linha 14, verifica-se o nome da pessoa encontrada.
  • linhas 16-17: alteramos esse nome e solicitamos à camada [dao] que salve as alterações.
  • linhas 19-20: solicitamos à camada [dao] uma cópia da pessoa que acabou de ser adicionada e verificamos seu novo nome.
  • linha 22: exclui-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 excluída. Deve-se obter uma [DaoException] com código 2.
  • linhas 36-37: a lista de pessoas é solicitada novamente. Deve-se obter a mesma lista do início do teste.

O método [test4] visa destacar os problemas de acesso simultâneo aos métodos da camada [dao]. Vale lembrar que esses métodos não foram sincronizados. O código do teste é o seguinte:

    public void test4() throws Exception {
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // criação de N threads para atualizar o número de filhos
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // aguardando a conclusão dos threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // recuperação da pessoa
        p1 = dao.getOne(id1);
         // ela deve ter N filhos
        assertEquals(N, p1.getNbEnfants());
         // exclusão da pessoa p1
        dao.deleteOne(p1.getId());
         // verificação
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve haver um erro de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • linhas 3-6: adiciona-se à lista uma pessoa P sem filhos. Registra-se seu [id] (linha 6).
  • linhas 7-13: iniciam-se N threads. Cada um deles 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 até que elas concluam 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 número de filhos dela é N.
  • linhas 22-35: a pessoa P é removida e, em seguida, verifica-se se ela não existe mais na lista.

Na linha 11, vemos que os threads são do tipo [ThreadDaoMajEnfants]. O construtor desse tipo possui três parâmetros:

  1. o nome atribuído ao thread, para que seja possível acompanhá-lo por meio de logs
  2. uma referência na camada [dao] para que o thread tenha acesso a ela
  3. o ID da pessoa sobre a qual o thread deve trabalhar

O tipo [ThreadDaoMajEnfants] é o seguinte:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // nome do thread
    private String name;
     // referência na camada [dao]
    private IDao dao;
     // o ID da pessoa com quem vamos trabalhar
    private int idPersonne;

     // construtor
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // núcleo do tópico
    public void run() {
         // acompanhamento
        suivi("lancé");
         // o ciclo continua até que se consiga incrementar em 1
         // o número de filhos da pessoa idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // recupera-se uma cópia da pessoa de idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // acompanhamento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
             // espera de 10 ms para liberar o processador
            try {
                 // continuação
                suivi("début attente");
                 // interrompendo para liberar o processador
                Thread.sleep(10);
                 // acompanhamento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // espera concluída — tentando validar a cópia
             // enquanto isso, outras threads podem ter alterado o original
            int codeErreur = 0;
            try {
                 // incrementa em 1 o número de filhos dessa cópia
                personne.setNbEnfants(nbEnfants + 1);
                 // tentando modificar o original
                dao.saveOne(personne);
                 // conseguimos — o original foi alterado
                fini = true;
            } catch (DaoException ex) {
                 // recuperamos o código de erro
                codeErreur = ex.getCode();
                 // deve ser um erro da versão 3 — caso contrário, reiniciamos
                 // a exceção
                if (codeErreur != 3) {
                    throw ex;
                } else {
                     // acompanhamento
                    suivi(ex.getMessage());
                }
                 // o original foi alterado — recomeça-se tudo
            }
        }
         // acompanhamento
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // acompanhamento
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • linha 9: [ThreadDaoMajEnfants] é, de fato, um thread
  • linhas 18-22: o construtor que inicializa o thread com três informações
    1. o nome [name] atribuído ao thread
    2. uma referência [dao] à camada [dao]. Observe-se que, mais uma vez, estamos trabalhando com o tipo da interface [IDao] e não com o da implementação [DaoImpl].
    3. o identificador [id] da pessoa na qual o thread deve trabalhar

Quando [test4] inicia um thread [ThreadDaoMajEnfants] (linha 12 do test4), o método [run] (linha 25) deste último é executado:

  • linhas 78-81: o método privado [suivi] permite gerar logs na tela. O método [run] utiliza-o para permitir o acompanhamento da execução da thread.
  • o thread tentará incrementar em 1 o número de filhos da pessoa P com o identificador [id]. Essa atualização pode exigir várias tentativas. Consideremos dois threads: [TH1] e [TH2]. [TH1] solicita uma cópia da pessoa P à camada [dao]. Ele a obtém e constata que ela possui a versão V1. [TH1] é interrompido. [TH2], que vinha logo atrás, faz o mesmo e obtém a mesma versão V1 da pessoa P. [TH2] é interrompido. [TH2] retoma o controle, incrementa o número de filhos de P e salva suas alterações. Sabemos que, nesse momento, essas alterações são salvas e que a versão de P passará para V2. [TH1] concluiu seu trabalho. [TH2] retoma o controle e faz o mesmo. Sua atualização de P será recusada, pois ele possui uma cópia de P na versão V1, enquanto o P original agora está na versão V2. [TH2] precisa, então, repetir todo o ciclo de [lecture -> mise à jour -> sauvegarde]. É por isso que encontramos o loop nas linhas 32 a 72. Nele, o thread:
  • solicita uma cópia da pessoa P a ser modificada (linha 34)
  • aguarda 10 ms (linha 43). Isso é artificial e tem como objetivo interromper o thread entre a leitura da pessoa P e sua atualização efetiva na lista de pessoas, a fim de aumentar a probabilidade de conflitos.
  • incrementa o número de filhos de P (linha 54) e salva P (linha 56). Se o thread não tiver a versão correta de P, uma exceção será acionada pela camada [dao]. Em seguida, recupera-se o código da exceção (linha 61) para verificar se é realmente o código 3 (versão incorreta de P). Se não for o caso, a exceção é reenviada ao método chamador, que, no final, é o método de teste [test4]. Se ocorrer a exceção com código 3, reiniciamos o ciclo [lecture -> mise à jour -> sauvegarde]. Se não houver exceção, a atualização foi realizada 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).
         // aguardando 10 ms
         //wait(10);
  • o método [test4] cria 100 threads (linha 8, parágrafo 14.5).
         // criação de N threads para atualizar o número de filhos
        final int N = 100;

Obtêm-se os seguintes resultados:

Image

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).
         // espera de 10 ms
        wait(10);
  • o método [test4] cria 2 threads (linha 8, parágrafo 14.5).
         // criação de N threads para atualizar o número de filhos
        final int N = 2;

Obtê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 temos apenas um.

Vamos acompanhar os logs de tela do [test4] para entender o que aconteceu:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • linha 1: o thread nº 0 inicia seu trabalho
  • linha 2: ele recuperou uma cópia da pessoa P e constatou que o número de filhos dela era 0
  • linha 3: ele encontra o [Thread.sleep(10)] do seu método [run] e, portanto, para no tempo [1145536368171] (ms)
  • linha 4: a thread nº 1 recupera então o processador e inicia seu trabalho
  • linha 5: ele recuperou uma cópia da pessoa P e constata que o número de filhos dela é 0
  • linha 6: ele encontra o [Thread.sleep(10)] do seu método [run] e, portanto, é interrompido
  • linha 7: o thread nº 0 recupera o processador no tempo [1145536368187] (ms), c.a.d. 16 ms após tê-lo perdido.
  • linha 8: o mesmo ocorre com o thread nº 1
  • linha 9: o thread nº 0 fez 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 o thread nº 1 conseguiu fazer sua atualização, embora, normalmente, ele já não possuísse a versão correta da pessoa P, que acabara de ser atualizada pelo thread nº 0.

Em primeiro lugar, é possível notar uma anomalia entre as linhas 7 e 8: parece que o thread nº 0 perdeu o controle do processador entre essas duas linhas para o thread nº 1. O que ele estava fazendo nesse momento? Ele estava executando o método [saveOne] da camada [dao]. Esse método tem a seguinte estrutura (ver parágrafo 14.4):

    public void saveOne(Personne personne) {
...
         // modificação — busca pela pessoa
....
         // temos a versão correta do original?
...
         // aguardando 10 ms
        wait(10);
         // Tudo certo — estamos fazendo a modificação
    ...
}
  • o thread nº 0 executou [saveOne] e chegou até a linha 8, onde foi obrigado a liberar o processador. Nesse intervalo, ele leu a versão da pessoa P, que era 1, pois a pessoa P ainda não havia sido atualizada.
  • Como o processador ficou livre, foi o thread nº 1 que o herdou. Ele, por sua vez, executou o [saveOne] e chegou até a linha 8, onde foi obrigado a liberar o processador. Enquanto isso, ele leu a versão da pessoa P, que era 1, pois a pessoa P ainda não havia sido atualizada.
  • Como o processador ficou livre, o thread nº 0 assumiu o controle. A partir da linha 9, ele fez sua atualização e alterou o número de filhos para 1. Em seguida, o método [run] da thread nº 0 foi concluído e a thread exibiu o log informando que havia alterado o número de filhos para 1 (linha 9).
  • Como o processador ficou livre, a thread nº 1 assumiu o controle. A partir da linha 9, ela fez sua atualização e alterou o número de filhos para 1. Por que 1? Porque ela possui uma cópia de P com o número de filhos igual a 0. É o que indica o log (linha 5). Em seguida, o método [run] da thread nº 1 foi concluído e a thread exibiu o log informando que havia alterado o número de filhos para 1 (linha 10).

De onde vem o problema? Ele decorre do fato de que a thread nº 0 não teve tempo de validar sua modificação e, portanto, de alterar a versão da pessoa P antes que a thread nº 1 tentasse ler essa versão para verificar se a pessoa P havia mudado. Esse cenário é improvável, mas não impossível. Foi necessário forçar a thread nº 0 a perder o controle do processador para que ele ocorresse com apenas duas threads. Sem esse artifício, a configuração anterior não havia conseguido reproduzir esse mesmo caso com 100 threads. O teste [test4] havia sido bem-sucedido.

Qual é a solução? Sem dúvida, há várias. Uma delas, simples de implementar, é sincronizar o método [saveOne]:


    public synchronized void saveOne(Personne personne)

A palavra-chave [synchronized] garante que apenas um thread por vez possa executar o método. Assim, o thread nº 1 só terá permissão para executar [saveOne] quando o thread nº 0 tiver saído dele. Assim, temos a certeza de que a versão da pessoa P terá sido alterada quando o thread nº 1 entrar em [saveOne]. Sua atualização será então recusada, pois ele não terá a versão correta de P.

São esses os quatro métodos da camada [dao] que deveriam ser sincronizados. Decidimos, no entanto, manter essa camada tal como foi descrita e adiar a sincronização para a camada [service]. Há várias razões para isso:

  • partimos do pressuposto de que o acesso à camada [dao] sempre ocorre por meio de uma camada [service]. Esse é o caso em nossa aplicação web.
  • pode ser necessário sincronizar também o acesso aos métodos da camada [service] por motivos diferentes daqueles que nos levariam a sincronizar os da camada [dao]. Nesse caso, não há necessidade de sincronizar os métodos da camada [dao]. Se tivermos certeza de que:
  • todo acesso à camada [dao] passa pela camada [service]
  • que apenas um único thread por 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.

Agora, vamos conhecer a camada [service].

14.6. A camada [service]

A camada [service] é composta pelas seguintes classes e interfaces:

Image

  • [IService] é a interface apresentada pela camada [dao]
  • [ServiceImpl] é uma implementação dessa interface

A interface [IService] é a seguinte:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // lista de todas as pessoas
    Collection getAll();
     // obter uma pessoa específica
    Personne getOne(int id);
     // adicionar/editar uma pessoa
    void saveOne(Personne personne);
     // excluir uma pessoa
    void deleteOne(int id);
}

Ela é idêntica à interface [IDao].

A implementação [ServiceImpl] da interface [IService] é a seguinte:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

     // a camada [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // lista de pessoas
    public synchronized Collection getAll() {
        return dao.getAll();
    }

     // obter uma pessoa específica
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

     // adicionar ou alterar uma pessoa
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // exclusão de uma pessoa
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • linhas 10-19: o atributo [IDao dao] é uma referência à camada [dao]. Ele será inicializado pelo Spring IoC.
  • linhas 22-24: implementação do método [getAll] da interface [IService]. O método se limita a delegar a solicitação à camada [dao].
  • linhas 27-29: implementação do método [getOne] da interface [IService]. O método se limita a delegar a solicitação à camada [dao].
  • linhas 32-34: implementação do método [saveOne] da interface [IService]. O método limita-se a delegar a solicitação à camada [dao].
  • linhas 37-39: implementação do método [deleteOne] da interface [IService]. O método limita-se a delegar a solicitação à camada [dao].
  • Todos os métodos são sincronizados (palavra-chave `synchronized`), garantindo que apenas um thread por vez possa utilizar a camada [service] e, consequentemente, a camada [dao].

14.7. Testes da camada [service]

Um teste JUnit é escrito para a camada [service]:

[TestService] é o teste JUnit. Os testes realizados são estritamente idênticos aos realizados para a camada [dao]. O esboço de [TestService] é o seguinte:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

     // camada [service]
    private ServiceImpl service;

     // fabricante
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

     // lista de pessoas
    private void doListe(Collection personnes) {
...
    }

     // teste1
    public void test1() throws ParseException {
         // lista atual
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
         // exibição
        doListe(personnes);
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
         // verificação — ocorrerá uma falha se a pessoa não for encontrada
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

     // alteração/exclusão de um elemento inexistente
    public void test2() throws ParseException {
...
    }

     // gerenciamento de versões de pessoas
    public void test3() throws ParseException, InterruptedException {
...
    }

     // bloqueio otimista — acesso multithread
    public void test4() throws Exception {
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
         // criação de N threads para atualização do número de filhos
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

     // testes de validade de saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • 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 ela deve utilizar essa 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, acessa-se a camada [service] (linhas 25, 32, 35) em vez da camada [dao].

O método [test4] visa identificar problemas de acesso simultâneo aos métodos da camada [service]. Ele é, mais uma vez, idêntico ao método de teste [test4] da camada [dao]. No entanto, há alguns detalhes que mudam:

  • recorre-se à camada [service] em vez da camada [dao] (linha 55)
  • passamos aos threads uma referência à camada [service] em vez da camada [dao] (linha 61)

O tipo [ThreadServiceMajEnfants] também é praticamente idêntico ao tipo [ThreadDaoMajEnfants], com a única diferença de que ele opera com a camada [service] e não com a camada [dao]:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

     // nome do thread
    private String name;
     // referência na camada [service]
    private IService service;
     // o ID da pessoa com quem vamos trabalhar
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

     // acompanhamento
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • linha 12: o thread opera com a camada [service]

Realizamos os testes com a configuração que apresentou problema na camada [dao]:

  • descomentamos a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
         // espera de 10 ms
        wait(10);
  • o método [test4] cria 100 threads (linha 65, parágrafo 14.7).
         // criação de N threads para atualizar o número de filhos
        final int N = 100;

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]

Vale lembrar a arquitetura de três camadas do nosso aplicativo:

A camada [web] apresentará telas ao usuário para permitir que ele gerencie o grupo de pessoas:

  • lista de pessoas do grupo
  • adição de uma pessoa ao grupo
  • alteração de uma pessoa do grupo
  • exclusão de uma pessoa do grupo

Para isso, ela se baseará na camada [service], que, por sua vez, utilizará a camada [dao]. Já apresentamos as telas gerenciadas pela camada [web] (parágrafo 14.1). Para descrever a camada web, apresentaremos sucessivamente:

  • sua configuração
  • suas visualizações
  • seu controlador
  • alguns testes

14.8.1. Configuração do aplicativo web

O projeto Eclipse da aplicação é o seguinte:

Image

  • no pacote [istia.st.mvc.personnes.web], encontra-se o controlador [Application].
  • As páginas JSP / JSTL estão no [WEB-INF/vues].
  • A pasta [lib] contém os arquivos de terceiros necessários para o aplicativo. Eles podem ser visualizados na pasta [Web App Libraries].

[web.xml]


O arquivo [web.xml] é o arquivo utilizado pelo servidor web para carregar o aplicativo. 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>
    <!--  arquivos de página inicial -->
    <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 visualizações [list, edit, erreurs].
  • linhas 32-34: o aplicativo possui uma página inicial padrão [index.jsp], localizada na raiz da pasta do aplicativo web.
  • linhas 36-39: o aplicativo possui uma página de erros padrão que é exibida quando o servidor web detecta uma exceção não tratada pelo aplicativo.
    • linha 37: a tag <exception-type> indica o tipo de exceção tratada pela diretiva <error-page>, neste caso o tipo [java.lang.Exception] e seus derivados, ou seja, todas as exceções.
    • linha 38: a tag <location> indica a página JSP a ser exibida quando ocorrer uma exceção do tipo definido por <exception-type>. A exceção ocorrida está disponível nessa página em um objeto chamado exception, caso a página contenha a diretiva:

<%@ page isErrorPage="true" %>
  • (continuação)
    • se <exception-type> especificar um tipo T1 e uma exceção do tipo T2, não derivada de T1, for reportada ao servidor web, este enviará ao cliente uma página de exceção proprietária, geralmente pouco intuitiva. Daí a importância da tag <error-page> no arquivo [web.xml].

[index.jsp]


Essa página é exibida se um usuário solicitar diretamente o contexto do aplicativo sem especificar uma URL, c.a.d. Aqui, [/personnes-01]. 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]. Essa URL exibe a lista de pessoas do grupo.

14.8.2. As páginas JSP / JSTL do aplicativo


A visualização [list.jsp]


Ela serve para exibir a lista de pessoas:

Image

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&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</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>

  • essa visualização recebe um elemento em seu modelo:
  • o elemento [personnes] associado a um objeto do tipo [ArrayList] de objetos do tipo [Personne]
  • linhas 22-34: percorre-se a lista ${pessoas} para exibir uma tabela HTML contendo as pessoas do grupo.
  • linha 31: a URL apontada pelo link [Modifier] é configurada pelo campo [id] da pessoa atual, para que o controlador associado à URL [/do/edit] saiba qual é a pessoa a ser modificada.
  • linha 32: o mesmo procedimento é seguido para o link [Supprimer].
  • linha 28: para exibir a data de nascimento da pessoa no formato JJ/MM/AAAA, usa-se a tag <dt> da biblioteca de tags [DateTime] do projeto Apache [Jakarta Taglibs]:

Image

O arquivo de descrição dessa biblioteca de tags está definido na linha 3.

  • linha 37: o link [Ajout] para adicionar uma nova pessoa tem como destino a URL [/do/edit], assim 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 visualização [edit.jsp]


Ela serve para exibir o formulário de adição de uma nova pessoa ou de modificação de uma pessoa existente:

O código da visualização [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&eacute;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&eacute;</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>

Essa visualização apresenta um formulário para adicionar uma nova pessoa ou atualizar uma pessoa existente. Daqui em diante, 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 visualização [edit.jsp] é exibida novamente com o(s) erro(s) ocorrido(s); caso contrário, a visualização [list.jsp] é exibida.

  • A visualização [edit.jsp], exibida tanto em um GET quanto em um POST que falha, recebe os seguintes elementos em seu modelo:
atributo
GET
POST
id
identificador da pessoa atualizada
idem
version
sua versão
idem
prenom
seu nome
nome inserido
nom
sobrenome
sobrenome inserido
dateNaissance
data de nascimento
data de nascimento inserida
marie
estado civil
estado civil inserido
nbEnfants
número de filhos
número de filhos inserido
erreurEdit
em branco
uma mensagem de erro indicando que a adição ou modificação falhou no momento da execução do POST, acionada pelo botão [Envoyer]. Vazio se não houver erro.
erreurPrenom
vazio
indica um nome incorreto – vazio caso contrário
erreurNom
vazio
indica um sobrenome incorreto – vazio caso contrário
erreurDateNaissance
vazio
indica uma data de nascimento incorreta – vazio caso contrário
erreurNbEnfants
vazio
indica um número incorreto de filhos – vazio caso contrário
  • linhas 11-15: se o POST do formulário apresentar erro, teremos [erreurEdit!=''] e uma mensagem de erro será exibida.
  • linha 16: o formulário será enviado para a URL [/do/validate]
  • linha 20: o elemento [id] do modelo é exibido
  • linha 24: o elemento [version] do modelo é exibido
  • linhas 26-32: inserção do nome da pessoa:
    • na 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, exibe-se novamente o valor inserido ${prenom}, bem como a eventual mensagem de erro ${erreurPrenom}
  • linhas 33-39: inserção do sobrenome da pessoa
  • linhas 40-46: inserção da data de nascimento da pessoa
  • linhas 47-61: inserção do estado civil da pessoa por meio 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 marcado.
  • linhas 62-68: inserção do número de filhos da pessoa
  • linha 71: um campo oculto HTML denominado [id], cujo valor corresponde ao campo [id] da pessoa que está sendo atualizada; -1 para uma adição, outro valor para uma modificação.
  • linha 72: um campo oculto HTML, denominado [version], cujo valor corresponde ao campo [id] da pessoa que está sendo atualizada.
  • linha 73: o botão [Valider] do tipo [Submit] do formulário
  • linha 74: um link que permite retornar à lista de pessoas. Ele foi denominado [Annuler] porque permite sair do formulário sem validá-lo.

A visualização [exception.jsp]


Ela serve para exibir uma página informando que ocorreu uma exceção não tratada pelo aplicativo e que foi encaminhada ao servidor web.

Por exemplo, vamos excluir uma pessoa que não existe no grupo:

O código da visualização [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 &agrave; la liste</a>
    </body>
</html>

  • essa visualização recebe, em seu modelo, o elemento [exception], que corresponde à exceção interceptada pelo servidor web. Para que esse elemento seja incluído no modelo da página JSP pelo servidor web, é necessário que a página tenha definido a tag da linha 3.
  • linha 6: define-se o código de status HTTP da resposta como 200. Esse é o primeiro cabeçalho HTTP da resposta. O código 200 indica ao cliente que sua solicitação foi atendida. Geralmente, um documento HTML foi incluído na resposta do servidor. É o que ocorre neste caso. Se o código de estado HTTP da resposta não for definido como 200, ele terá aqui o valor 500, o que significa que ocorreu um erro. De fato, o servidor web, ao interceptar uma exceção não tratada, considera essa situação anormal e a sinaliza com o código 500. A reação ao código 500 varia de acordo com os navegadores: o Firefox exibe o documento que pode acompanhar essa resposta, enquanto o Firefox ignora esse documento e exibe sua própria página. É por esse motivo que substituímos o código 500 pelo código 200.
  • linha 16: o texto da exceção é exibido
  • linha 18: é oferecido ao usuário um link para voltar à lista de pessoas

A visualização [erreurs.jsp]


Ela serve para exibir uma página que sinaliza os erros de inicialização do aplicativo, c.a.d, e os erros detectados durante a execução do método [init] do servlet do controlador. Pode ser, por exemplo, a ausência de um parâmetro no arquivo [web.xml], conforme mostra o exemplo abaixo:

Image

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 em seu modelo um elemento [erreurs], que é um objeto do tipo [ArrayList] contendo objetos [String]; estes últimos são mensagens de erro. Elas são exibidas pelo loop das linhas 13 a 15.

14.8.3. O controlador da aplicação

O controlador [Application] está definido no pacote [istia.st.mvc.personnes.web]:

Image


Estrut tura e inicialização do controlador


A estrutura do controlador [Application] é a seguinte:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
     // parâmetros de instância
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

     // serviço
    ServiceImpl service=null;

     // inicialização
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
         // recuperando os parâmetros de inicialização do servlet
        ServletConfig config = getServletConfig();
         // processando os demais parâmetros de inicialização
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
             // valor do parâmetro
            valeur = config.getInitParameter(paramètres[i]);
             // o parâmetro está presente?
            if (valeur == null) {
                 // registra-se o erro
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                 // armazenamos o valor do parâmetro
                params.put(paramètres[i], valeur);
            }
        }
         // a URL da visualização [erreurs] tem um tratamento especial
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
         // instanciação da camada [dao]
        DaoImpl dao = new DaoImpl();
        dao.init();
         // instanciação da camada [service]
        service = new ServiceImpl();
        service.setDao(dao);
    }

     // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

     // exibição da lista de pessoas
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // alteração/adição de uma pessoa
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // confirmação da modificação/adição de uma pessoa
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // confirmação da alteração/adição de uma pessoa
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // exibição do formulário pré-preenchido
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

     // envio
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
         // passando o controle para o GET
        doGet(request, response);    }
}
  • linhas 20-36: recuperam-se os parâmetros esperados no arquivo [web.xml].
  • linhas 39-41: o parâmetro [urlErreurs] deve estar obrigatoriamente presente, pois indica a URL da visualização [erreurs] capaz de exibir eventuais erros de inicialização. Se ele não existir, a aplicação é interrompida com a execução de um [ServletException] (linha 40). Essa exceção será encaminhada ao servidor web e tratada pela tag <error-page> do arquivo [web.xml]. A visualização [exception.jsp] é, portanto, exibida:

Image

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 modificada e recarregada. Ele é ú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 essa instância (criação de uma lista inicial com três pessoas)
  • linha 46: cria uma instância [ServiceImpl] que implementa a camada [service]
  • linha 47: inicializa a camada [service], fornecendo-lhe uma referência à camada [dao]

Após a inicialização do controlador, seus métodos dispõem de uma referência [service] à camada [service] (linha 15), que serão utilizadas para executar as ações solicitadas pelo usuário. Essas ações serão interceptadas pelo método [doGet], que as encaminhará para um método específico do controlador:

Url
Método HTTP
método do controlador
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

O método [doGet]


Este método tem como objetivo direcionar o processamento das ações solicitadas pelo usuário para o método correto. Seu código é o seguinte:

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

         // verifica-se como ocorreu a inicialização do servlet
        if (erreursInitialisation.size() != 0) {
             // passamos o controle para a página de erros
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
             // fim
            return;
        }
         // recuperamos o método de envio da solicitação
        String méthode = request.getMethod().toLowerCase();
         // recupera-se a ação a ser executada
        String action = request.getPathInfo();
         // ação?
        if (action == null) {
            action = "/list";
        }
         // execução da ação
        if (méthode.equals("get") && action.equals("/list")) {
             // lista de pessoas
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
             // exclusão de uma pessoa
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
             // exibição do formulário de adição/alteração de uma pessoa
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
             // validação do formulário de adição/alteração de uma pessoa
            doValidatePersonne(request, response);
            return;
        }
         // outros casos
        doListPersonnes(request, response);
    }
  • linhas 7-13: verifica-se se a lista de erros de inicialização está vazia. Caso contrário, exibe-se a visualização [erreurs(erreurs)], que indicará o(s) erro(s).
  • linha 15: recupera-se o método [get] ou [post] que o cliente utilizou para fazer sua solicitação.
  • linha 17: recupera-se o valor do parâmetro [action] da consulta.
  • linhas 23-27: processamento da solicitação [GET /do/list], que solicita a lista de pessoas.
  • linhas 28-32: processamento da solicitação [GET /do/delete], que solicita a exclusão de uma pessoa.
  • linhas 33-37: processamento da solicitação [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 do dado atualizado.
  • 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:

Image

Seu código é o seguinte:

1
2
3
4
5
6
7
8
9
     // exibição da lista de pessoas
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // o modelo da visualização [list]
        request.setAttribute("personnes", service.getAll());
         // exibição da visualização [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • linha 5: solicita-se à camada [service] a lista de pessoas do grupo e insere-se essa lista no modelo sob a chave “pessoas”.
  • linha 7: exibe-se a visualização [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 exclusão da pessoa com id=XX. A URL [/do/delete?id=XX] é a dos links [Supprimer] da visualização [list.jsp]:

Image

cujo código é o seguinte:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <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>

Na linha 12, vemos a URL [/do/delete?id=XX] do link [Supprimer]. O método [doDeletePersonne], que deve processar essa URL, deve remover a pessoa com id=XX e, em seguida, exibir a nova lista de pessoas do grupo. Seu código é o seguinte:

     // validação da modificação/adição de uma pessoa
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recuperação do ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
         // exclusão da pessoa
        service.deleteOne(id);
         // redirecionamento para a lista de pessoas
        response.sendRedirect("list");
    }
  • 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 exclusão da pessoa com o ID obtido. Não realizamos nenhuma verificação. Se a pessoa que se pretende excluir não existir, a camada [dao] lança uma exceção que é repassada pela camada [service]. Também não a tratamos aqui, no controlador. Portanto, ela será repassada até o servidor web, que, por configuração, exibirá a página [exception.jsp], descrita no parágrafo 14.8.2:

Image

  • linha 9: se a exclusão ocorreu (sem exceção), solicita-se ao cliente que seja redirecionado para a URL relativa [list]. Como a página que acabou de ser processada é [/do/delete], a URL de redirecionamento será [/do/list]. O navegador será, portanto, levado a acessar [GET /do/list], o que fará com que a lista de pessoas seja exibida.

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 visualização [list.jsp]:

Image

cujo código é o seguinte:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <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>

Na linha 11, vemos a URL [/do/edit?id=XX] do link [Modifier] e, na linha 17, a URL [/do/edit?id=-1] do link [Ajout]. O método [doEditPersonne] deve exibir 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:

     // alteração/adição de uma pessoa
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recupera-se o ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
         // adição ou modificação?
        Personne personne = null;
        if (id != -1) {
             // alteração — recupera-se a pessoa a ser alterada
            personne = service.getOne(id);
        } else {
             // adição — cria-se um registro de pessoa vazio
            personne = new Personne();
            personne.setId(-1);
        }
         // insere-se o objeto [Personne] no modelo da visualização [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
         // exibição da visualização [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 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:
  1. se o id for diferente de -1, trata-se de uma modificação e é necessário exibir um formulário pré-preenchido com as informações da pessoa a ser modificada. Na linha 10, essa pessoa é solicitada à camada [service].
  2. Se o id for igual a -1, trata-se de uma adição e é necessário exibir um formulário vazio. Para isso, é criado um registro vazio nas linhas 13-14.
  • O objeto [Personne] obtido é inserido no modelo da página [edit.jsp] descrito no parágrafo 14.8.2. Esse modelo inclui os seguintes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Esses elementos são inicializados nas linhas 17 a 30, com exceção daqueles cujo valor é a string vazia [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Sabe-se que, na ausência desses elementos no modelo, a biblioteca JSTL exibirá uma string vazia como valor para eles. Embora o elemento [erreurEdit] também tenha como valor uma string vazia, ele é, no entanto, inicializado, pois é realizada uma verificação de seu valor na página [edit.jsp].
  • Assim que o modelo estiver pronto, o controle é passado para a página [edit.jsp], linhas 32-33, que irá gerar a visualização [edit].

O método [doValidatePersonne]


Este método processa a solicitação [POST /do/validate], que valida o formulário de atualização. Essa solicitação POST é acionada pelo botão [Valider]:

Image

Recordemos os campos de preenchimento do formulário HTML da visualização acima:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

A consulta POST contém os parâmetros [prenom, nom, dateNaissance, marie, nbEnfants, id, version] e é enviada para a URL [/do/validate] (linha 1). Ela é processada pelo seguinte método [doValidatePersonne]:

// validação da modificação/adição de uma pessoa
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recuperação dos elementos postados
        boolean formulaireErroné = false;
        boolean erreur;
         // o nome
        String prenom = request.getParameter("prenom").trim();
         // o nome é válido?
        if (prenom.length() == 0) {
             // registrando o erro
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
         // o sobrenome
        String nom = request.getParameter("nom").trim();
         // o nome é válido?
        if (nom.length() == 0) {
             // registrando o erro
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
         // data de nascimento
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
             // registrando o erro
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
         // estado civil
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
         // número de filhos
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
             // registra-se o erro
            erreur = true;
        }
         // número de filhos incorreto?
        if (erreur) {
             // o erro foi relatado
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
         // ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
         // versão
        long version = Long.parseLong(request.getParameter("version"));
         // o formulário está com erro?
        if (formulaireErroné) {
             // reexibe-se o formulário com as mensagens de erro
            showFormulaire(request, response, "");
             // concluído
            return;
        }
         // o formulário está correto — a pessoa é registrada
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
             // registro
            service.saveOne(personne);
        } catch (DaoException ex) {
             // o formulário é exibido novamente com a mensagem do erro ocorrido
            showFormulaire(request, response, ex.getMessage());
             // concluído
            return;
        }
         // redireciona para a lista de pessoas
        response.sendRedirect("list");
    }

     // exibição do formulário pré-preenchido
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
         // prepara-se o modelo da visualização [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
         // exibição da visualização [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • linhas 8-14: o parâmetro [prenom] da solicitação POST é recuperado e sua validade é verificada. Caso esteja incorreto, o elemento [erreurPrenom] é inicializado com uma mensagem de erro e inserido nos atributos da consulta.
  • linhas 16-22: procede-se de maneira semelhante para o parâmetro [nom]
  • linhas 24-32: procede-se de maneira semelhante para o parâmetro [dateNaissance]
  • linha 34: recupera-se o parâmetro [marie]. Não se verifica sua validade porque, a princípio, ele provém do valor de um botão de opção. Dito isso, nada impede que um programa gere um [POST /personnes-01/do/validate] acompanhado de um parâmetro [marie] inventado. Portanto, devemos testar a validade desse parâmetro. Aqui, contamos com nosso gerenciamento de exceções, que faz com que a página [exception.jsp] seja exibida caso o controlador não as gerencie por conta própria. Portanto, 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. Esse funcionamento nos satisfaz.
  • linhas 34-54: recuperamos o parâmetro [nbEnfants] e verificamos seu valor.
  • linha 56: recuperamos o parâmetro [id] sem verificar seu valor
  • linha 58: fazemos o mesmo com o parâmetro [version]
  • linhas 60-65: se o formulário estiver com erros, ele é exibido novamente com as mensagens de erro geradas anteriormente
  • linhas 67-69: se estiver válido, cria-se um novo objeto [Personne] com os elementos do formulário
  • linhas 70-78: o usuário é salvo. O salvamento pode falhar. Em um ambiente com vários usuários, o usuário a ser alterado pode ter sido excluído ou já alterado por outra pessoa. Nesse caso, a camada [dao] lançará uma exceção que é tratada aqui.
  • linha 80: se não houver exceção, redirecionamos o cliente para a URL [/do/list] para apresentar a ele o novo estado do grupo.
  • linha 75: se ocorreu uma exceção durante o salvamento, solicitamos novamente a exibição do formulário inicial, passando 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 inseridos (request.getParameter(" ... ")). Vale lembrar que as mensagens de erro já foram inseridas no modelo pelo método [doValidatePersonne]. A página [edit.jsp] é exibida nas linhas 99-100.

14.9. Testes da aplicação web

Vários testes foram apresentados no parágrafo 14.1. Convidamos o leitor a repeti-los. Mostramos aqui outras capturas de tela que ilustram casos de conflitos de acesso aos dados em um ambiente com múltiplos usuários:

[Firefox] será o navegador do usuário U1. Este solicita a URL [http://localhost:8080/personnes-01]:

Image

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

Image

O usuário U1 acessa a edição do perfil da pessoa [Lemarchand]:

Image

O usuário U2 faz o mesmo:

Image

O usuário U1 faz alterações e confirma:

O usuário U2 faz o mesmo:

O usuário U2 retorna à lista de pessoas por meio do link [Annuler] do formulário:

Image

Ele encontra a pessoa [Lemarchand] tal como U1 a modificou. Agora, U2 exclui [Lemarchand]:

U1 ainda mantém sua própria lista e deseja alterar [Lemarchand] novamente:

U1 usa o link [Retour à la liste] para ver do que se trata:

Image

Ele descobre que, de fato, [Lemarchand] não faz mais parte da lista...

14.10. Conclusion

Implementamos a arquitetura MVC em uma arquitetura de três camadas [web, metier, dao], utilizando um exemplo básico de gerenciamento de uma lista de pessoas. Isso nos permitiu utilizar os conceitos apresentados nas seções anteriores. Na versão analisada, a lista de pessoas era mantida na memória. Em breve, estudaremos versões nas quais essa lista será mantida em uma tabela de banco 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.