Skip to content

3. JPA numa arquitetura multicamadas

Para estudar o API JPA, utilizámos a seguinte arquitetura de teste:

Os nossos programas de teste eram aplicações de consola que consultavam diretamente a camada JPA. Nessa ocasião, descobrimos os principais métodos da camada JPA. Estávamos num ambiente denominado «Java SE» (Standard Edition). O JPA funciona tanto num ambiente Java SE como num ambiente Java EE5 (Enterprise Edition).

Agora que já dominamos tanto a configuração da ponte relacional/objeto como a utilização dos métodos da camada JPA, voltamos a uma arquitetura multicamadas mais clássica:

A camada [JPA] será acedida através de uma arquitetura de duas camadas [metier] e [dao]. O framework Spring [7] e, posteriormente, o contentor EJB3 de JBoss e [8] serão utilizados para ligar estas camadas entre si.

Como referimos anteriormente, o JPA está disponível nos ambientes SE e EE5. O ambiente Java EE5 fornece vários serviços na área do acesso a dados persistentes, nomeadamente pools de ligação, gestores de transações, etc. Pode ser interessante para um programador tirar partido destes serviços. O ambiente Java EE5 ainda não está muito difundido (maio de 2007). Encontra-se atualmente no servidor de aplicações Sun Application Server 9.x (Glassfish). Um servidor de aplicações é, essencialmente, um servidor de aplicações web. Se se criar uma aplicação gráfica autónoma do tipo Swing, não é possível dispor do ambiente EE nem dos serviços que este oferece. Isto constitui um problema. Começam a surgir ambientes EE «autónomos», c.a.d. que podem ser utilizados fora de um servidor de aplicações. É o caso do JBoss e do EJB3, que iremos utilizar neste documento.

Num ambiente EE5, as camadas são implementadas por objetos denominados EJB (Enterprise Java Bean). Nas versões anteriores do EE, os EJB (EJB e 2.x) são considerados difíceis de implementar, testar e, por vezes, pouco eficientes. Distinguem-se os EJB2.x «entity» e os EJB2.x «session». Resumindo, um EJB2.x «entity» corresponde a uma linha de uma tabela de base de dados e um EJB2.x «session» é um objeto utilizado para implementar as camadas [metier], [dao] de uma arquitetura multicamadas. Uma das principais críticas feitas às camadas implementadas com EJB é que estas só podem ser utilizadas no interior de contentores EJB, um serviço fornecido pelo ambiente EE. Isto torna os testes unitários problemáticos. Assim, no esquema acima, os testes unitários das camadas [metier] e [dao], construídas com EJB, exigiriam a implementação de um servidor de aplicações, uma operação bastante pesada que não incentiva realmente o programador a realizar testes com frequência.

O framework Spring surgiu em resposta à complexidade dos EJB2. O Spring fornece, num ambiente SE, um número significativo dos serviços normalmente fornecidos pelos ambientes EE. Assim, na parte «Persistência de dados» que nos interessa aqui, o Spring fornece os pools de ligação e os gestores de transações de que as aplicações necessitam. O surgimento do Spring promoveu a cultura dos testes unitários, que se tornaram, de repente, muito mais fáceis de implementar. O Spring permite a implementação das camadas de uma aplicação através de objetos Java clássicos (POJO, Plain Old/Ordinary Java Object), permitindo a reutilização destes noutro contexto. Por fim, integra numerosas ferramentas de terceiros de forma bastante transparente, nomeadamente ferramentas de persistência como o Hibernate, o Ibatis, ...

O Java EE5 foi concebido para corrigir as lacunas da especificação anterior EE. Os EJB e 2.x passaram a ser os EJB3. Estes são POJOs marcados por anotações que os tornam objetos específicos quando se encontram dentro de um contentor EJB3. Nesse contentor, o EJB3 poderá beneficiar dos serviços do contentor (pool de ligações, gestor de transações, etc.). Fora do contentor EJB3, o EJB3 torna-se um objeto Java normal. As suas anotações EJB são ignoradas.

Acima, representámos o Spring e o JBoss EJB3 como uma possível infraestrutura (framework) da nossa arquitetura multicamadas. É esta infraestrutura que fornecerá os serviços de que necessitamos: um pool de ligações e um gestor de transações.

  • Com o Spring, as camadas serão implementadas com POJOs. Estes terão acesso aos serviços do Spring (pool de ligações, gestor de transações) através da injeção de dependências nestes POJOs: durante a sua construção, o Spring injeta-lhes referências aos serviços de que irão necessitar.
  • O JBoss EJB3 é um contentor EJB capaz de funcionar fora de um servidor de aplicações. O seu princípio de funcionamento (para o programador) é análogo ao descrito para o Spring. Encontraremos poucas diferenças.

Concluiremos o documento com um exemplo de aplicação web de três camadas, básico mas, ainda assim, representativo:

3.1. Exemplo 1: Spring / JPA com a entidade Pessoa

Partimos da entidade Personne analisada no parágrafo 2.1 e integramo-la numa arquitetura multicamadas, em que a integração das camadas é feita com o Spring e a camada de persistência é implementada pelo Hibernate.

Presume-se que o leitor possua conhecimentos básicos sobre o Spring. Caso contrário, pode consultar o seguinte documento, que explica o conceito de injeção de dependências, que está no cerne do Spring:

[ref3]: Spring IoC (Inversão de Controlo) [http://tahe.developpez.com/java/springioc].

3.1.1. O projeto « » no Eclipse / Spring / Hibernate

O projeto Eclipse é o seguinte:

  • em [1]: o projeto Eclipse. Encontrar-se-á em [6] nos exemplos do tutorial [5]. Iremos importá-lo.
  • em [2]: os códigos Java das camadas apresentados em pacotes:
    • [entites]: o pacote das entidades JPA
    • [dao]: a camada de acesso aos dados — baseia-se na camada JPA
    • [service]: uma camada de serviços, mais do que de negócio. Nesta camada, será utilizado o serviço de transações dos contentores.
    • [tests]: agrupa os programas de teste.
  • em [3]: a biblioteca [jpa-spring] reúne os ficheiros JAR necessários ao Spring (ver também [7] e [8]).
  • em [4]: a pasta [conf] reúne os ficheiros de configuração do Spring para cada um dos SGBD utilizados neste tutorial.

3.1.2. As entidades JPA

Apenas existe uma entidade gerida aqui, a entidade Personne analisada no parágrafo 2.1, cuja configuração recordamos abaixo:


package entites;

...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // construtores
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
...
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // getters e setters
...
}

3.1.3. A camada [dao]

A camada [dao] apresenta a seguinte interface IDao:


package dao;

import java.util.List;

import entites.Personne;

public interface IDao {
    // obter uma pessoa através do seu identificador
    public Personne getOne(Integer id);

    // obter todas as pessoas
    public List<Personne> getAll();

    // guardar um contacto
    public Personne saveOne(Personne personne);

    // atualizar um contacto
    public Personne updateOne(Personne personne);

    // eliminar uma pessoa através do seu identificador
    public void deleteOne(Integer id);

    // obter as pessoas cujo nome corresponde a um padrão
    public List<Personne> getAllLike(String modele);

}

A implementação [Dao] desta interface é a seguinte:


package dao;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import entites.Personne;

public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // eliminar uma pessoa através do seu identificador
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    @SuppressWarnings("unchecked")
    // obter todas as pessoas
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    @SuppressWarnings("unchecked")
    // obter as pessoas cujo nome corresponde a um padrão
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // obter uma pessoa através do seu identificador
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // guardar um utilizador
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // atualizar um utilizador
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Em primeiro lugar, salienta-se a simplicidade da implementação [Dao]. Esta deve-se à utilização da camada JPA, que realiza a maior parte do trabalho de acesso aos dados.
  • linha 10: a classe [Dao] implementa a interface [IDao]
  • linha 13: o objeto do tipo [EntityManager] que será utilizado para manipular o contexto de persistência JPA. Por conveniência, por vezes iremos confundi-lo com o próprio contexto de persistência. O contexto de persistência conterá entidades Personne.
  • linha 12: em nenhuma parte do código o campo [EntityManager em] é inicializado. Será inicializado pelo Spring no arranque da aplicação. É a anotação JPA @PersistenceContext da linha 12 que solicita ao Spring que injete em em um gestor de contexto de persistência.
  • linhas 26-28: a lista de todas as pessoas é obtida através de uma consulta JPQL.
  • linhas 32-35: a lista de todas as pessoas cujo nome corresponde a um determinado modelo é obtida através de uma consulta JPQL.
  • linhas 38-40: a pessoa com esse identificador é obtida através do método find do API JPA. Devolve um ponteiro null se a pessoa não existir.
  • linhas 43-46: uma pessoa é tornada persistente pelo método `persist` do API JPA. O método torna a pessoa persistente.
  • linhas 49-51: a atualização de uma pessoa é realizada pelo método merge do API JPA. Este método só faz sentido se a pessoa assim atualizada estiver previamente desligada. O método torna persistente a pessoa assim criada.
  • linhas 16-22: a eliminação da pessoa cujo identificador nos é passado como parâmetro é feita em duas etapas:
    • linha 17: procura-se a pessoa no contexto de persistência
    • linhas 18-20: se não for encontrada, é lançada uma exceção com o código de erro 2
    • linha 21: se a pessoa for encontrada, é removida do contexto de persistência através do método remove do API JPA.
  • O que ainda não é visível, neste momento, é que cada método será executado no âmbito de uma transação iniciada pela camada [service].

A aplicação tem o seu próprio tipo de exceção denominado [DaoException]:


package dao;

@SuppressWarnings("serial")
public class DaoException extends RuntimeException {

    // código de erro
    private int code;

    public DaoException(int code) {
        super();
        this.code = code;
    }

    public DaoException(String message, int code) {
        super(message);
        this.code = code;
    }

    public DaoException(Throwable cause, int code) {
        super(cause);
        this.code = code;
    }

    public DaoException(String message, Throwable cause, int code) {
        super(message, cause);
        this.code = code;
    }

    // getter e setter

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

}
  • linha 4: [DaoException] deriva de [RuntimeException]. Trata-se, portanto, de um tipo de exceção que o compilador não nos obriga a gerir através de um try/catch nem a incluir na assinatura dos métodos. É por esta razão que [DaoException] não consta da assinatura do método [deleteOne] da interface [IDao]. Isto permite que esta interface seja implementada por uma classe que lance outro tipo de exceções, desde que esta também derive de [RuntimeException].
  • Para diferenciar os erros que podem ocorrer, utiliza-se o código de erro da linha 7. Os três construtores das linhas 14, 19 e 24 são os da classe pai [RuntimeException], aos quais foi adicionado um parâmetro: o do código de erro que se pretende atribuir à exceção.

3.1.4. A camada [metier / service]

A camada [service] apresenta a seguinte interface [IService]:


package service;

import java.util.List;

import entites.Personne;

public interface IService {
    // obter um utilizador através do seu identificador
    public Personne getOne(Integer id);

    // obter todas as pessoas
    public List<Personne> getAll();

    // guardar um utilizador
    public Personne saveOne(Personne personne);

    // Atualizar um utilizador
    public Personne updateOne(Personne personne);

    // eliminar uma pessoa através do seu identificador
    public void deleteOne(Integer id);

    // obter as pessoas cujo nome corresponde a um padrão
    public List<Personne> getAllLike(String modele);

    // eliminar várias pessoas de uma só vez
    public void deleteArray(Personne[] personnes);

    // guardar várias pessoas de uma só vez
    public Personne[] saveArray(Personne[] personnes);

    // atualizar várias pessoas de uma só vez
    public Personne[] updateArray(Personne[] personnes);

}
  • linhas 8-24: a interface [IService] inclui os métodos da interface [IDao]
  • linha 27: o método [deleteArray] permite eliminar um conjunto de pessoas numa transação: ou todas as pessoas são eliminadas, ou nenhuma.
  • linhas 30 e 33: métodos análogos ao [deleteArray] para guardar (linha 30) ou atualizar (linha 33) um conjunto de pessoas no âmbito de uma transação.

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


package service;

...

// Todos os métodos da classe são executados numa transação
@Transactional
public class Service implements IService {

    // camada [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

    // eliminar várias pessoas de uma só vez
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }

    // eliminar uma pessoa através do seu identificador
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }

    // obter todas as pessoas
    public List<Personne> getAll() {
        return dao.getAll();
    }

    // obter as pessoas cujo nome corresponde a um padrão
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }

    // obter uma pessoa através do seu identificador
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }

    // guardar várias pessoas de uma só vez
    public Personne[] saveArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.saveOne(personnes[i]);
        }
        return personnes2;
    }

    // guardar uma pessoa
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }

    // atualizar várias pessoas de uma só vez
    public Personne[] updateArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.updateOne(personnes[i]);
        }
        return personnes2;
    }

    // atualizar um utilizador
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }

}
  • linha 6: a anotação Spring @Transactional indica que todos os métodos da classe devem ser executados no âmbito de uma transação. Uma transação será iniciada antes do início da execução do método e encerrada após a execução. Se ocorrer uma exceção do tipo [RuntimeException] ou derivada durante a execução do método, um rollback automático anula toda a transação; caso contrário, um commit automático valida-a. É importante notar que o código Java não precisa de se preocupar com as transações. Estas são geridas pelo Spring.
  • linha 10: uma referência à camada [dao]. Veremos mais adiante que esta referência é inicializada pelo Spring no arranque da aplicação.
  • Os métodos de [Service] limitam-se a chamar os métodos da interface [IDao dao] da linha 10. Deixamos que o leitor analise o código. Não há dificuldades particulares.
  • Já referimos anteriormente que cada método de [Service] é executado numa transação. Esta está associada ao thread de execução do método. Neste thread, são executados métodos da camada [dao]. Estes serão automaticamente associados à transação do thread de execução. O método [deleteArray] (linha 21), por exemplo, é levado a executar N vezes o método [deleteOne] da camada [dao]. Estas N execuções decorrerão no thread de execução do método [deleteArray], ou seja, na mesma transação. Assim, serão todas validadas (commit) se tudo correr bem ou todas anuladas (rollback) se ocorrer uma exceção numa das N execuções do método [deleteOne] da camada [dao].

3.1.5. Configuração das camadas

A configuração das camadas [service], [dao] e [JPA] é assegurada pelos dois ficheiros acima referidos: [META-INF/persistence.xml] e [spring-config.xml]. Ambos os ficheiros devem estar no classpath da aplicação, o que explica a sua presença na pasta [src] do projeto Eclipse. O nome do ficheiro [spring-config.xml] pode ser escolhido livremente.

persistence.xml


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • linha 4: o ficheiro declara uma unidade de persistência denominada jpa que utiliza transações «locais», c.a.d, não fornecidas por um contentor EJB3. Estas transações são criadas e geridas pelo Spring e são objeto de configurações no ficheiro [spring-config.xml].

spring-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- camadas de aplicação -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <!-- camada de persistência JPA -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform"
                    value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- a fonte de dados DBCP -->
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- o gestor de transações -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>

    <!-- tradução de exceções -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- anotações de persistência -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>
  • linhas 2-5: a baliza raiz <beans> do ficheiro de configuração. Não comentamos os vários atributos desta baliza. É importante ter o cuidado de fazer um copiar/colar, pois um erro num destes atributos provoca erros por vezes difíceis de compreender.
  • linha 8: o bean «dao» é uma referência a uma instância da classe [dao.Dao]. Será criada uma única instância (singleton) que implementará a camada [dao] da aplicação.
  • linhas 9-11: instanciação da camada [service]. O bean «service» é uma referência a uma instância da classe [service.Service]. Será criada uma única instância (singleton) que implementará a camada [service] da aplicação. Vimos que a classe [service.Service] tinha um campo privado [IDao dao]. Este campo é inicializado na linha 10 pelo bean «dao» definido na linha 8.
  • Por fim, as linhas 8 a 11 configuraram as camadas [dao] e [service]. Veremos mais adiante em que momento e como estas serão instanciadas.
  • linhas 35-42: é definida uma fonte de dados. Já nos deparámos com o conceito de fonte de dados ao estudar as entidades JPA com o Hibernate:

No exemplo acima, o [c3p0], denominado «pool de ligações», poderia ter sido chamado de «fonte de dados». Uma fonte de dados fornece o serviço de «pool de ligações». Com o Spring, iremos utilizar uma fonte de dados diferente de [c3p0]. Trata-se de [DBCP] do projeto Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Os arquivos de [DBCP] foram colocados na biblioteca do utilizador [jpa-spring]:

 
  • linhas 38-41: para estabelecer ligações com a base de dados de destino, a fonte de dados precisa de saber o controlador JDBC utilizado (linha 38), o URL da base de dados (linha 39), o utilizador da ligação e a sua palavra-passe (linhas 40-41).
  • linhas 14-32: configuram a camada JPA
  • linhas 14-15: definem um bean do tipo [EntityManagerFactory] capaz de criar objetos do tipo [EntityManager] para gerir os contextos de persistência. A classe instanciada [LocalContainerEntityManagerFactoryBean] é fornecida pelo Spring. Necessita de um determinado número de parâmetros para ser instanciada, definidos nas linhas 16-31.
  • linha 16: a fonte de dados a utilizar para obter ligações ao SGBD. Trata-se da fonte [DBCP] definida nas linhas 35 a 42.
  • linhas 17-27: a implementação JPA a utilizar
  • linhas 18-26: definem o Hibernate (linha 19) como a implementação JPA a utilizar
  • linhas 23-24: o dialeto SQL que o Hibernate deve utilizar com o SGBD de destino, neste caso o MySQL5.
  • linha 25: solicita que, ao iniciar a aplicação, a base de dados seja gerada (drop e create).
  • linhas 28-31: definem um «carregador de classes». Não sei explicar de forma clara a função deste bean utilizado pelo EntityManagerFactory da camada JPA. Seja como for, implica passar para o JVM, que executa a aplicação, o nome de um arquivo cujo conteúdo irá gerir o carregamento das classes no arranque da aplicação. Neste caso, esse arquivo é o [spring-agent.jar], localizado na biblioteca do utilizador [jpa-spring] (ver acima). Veremos que o Hibernate não necessita deste agente, mas que o Toplink precisa dele.
  • linhas 45-50: definem o gestor de transações a utilizar
  • linha 45: indica que as transações são geridas com anotações Java (também poderiam ter sido declaradas em spring-config.xml). Trata-se, em particular, da anotação @Transactional presente na classe [Service] (linha 6).
  • linhas 46-50: o gestor de transações
  • linha 47: o gestor de transações é uma classe fornecida pelo Spring
  • linhas 48-49: o gestor de transações do Spring precisa de conhecer a classe EntityManagerFactory, que gere a camada JPA. Trata-se da classe definida nas linhas 14-32.
  • linhas 57-58: definem a classe que gere as anotações de persistência do Spring encontradas no código Java, tais como a anotação @PersistenceContext da classe [dao.Dao] (linha 12).
  • linhas 53-54: definem a classe Spring que gere, nomeadamente, a anotação @Repository, que torna uma classe assim anotada elegível para a conversão das exceções nativas do controlador JDBC de SGBD em exceções genéricas Spring do tipo [DataAccessException]. Esta conversão encapsula a exceção nativa do Jdbc num tipo [DataAccessException] com várias subclasses:

Image

Esta conversão permite que o programa cliente lide com as exceções de forma genérica, independentemente do SGBD de destino. Não utilizámos a anotação @Repository no nosso código Java. Por isso, as linhas 53-54 são desnecessárias. Deixámo-las apenas a título informativo.

Terminámos com o ficheiro de configuração do Spring. É complexo e muitas coisas permanecem obscuras. Foi extraído da documentação do Spring. Felizmente, a sua adaptação a diversas situações resume-se frequentemente a duas alterações:

  • a alteração do banco de dados de destino: linhas 38-41. Apresentaremos um exemplo com o Oracle.
  • a da implementação JPA: linhas 14-32. Apresentaremos um exemplo com o Toplink.

3.1.6. Programa cliente [InitDB]

Abordamos a criação de um primeiro cliente da arquitetura descrita anteriormente:

O código de [InitDB] é o seguinte:


package tests;

...
public class InitDB {

    // camada de serviço
    private static IService service;

    // construtor
    public static void main(String[] args) throws ParseException {
        // configuração da aplicação
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // camada de serviço
        service = (IService) ctx.getBean("service");
        // esvaziar a base de dados
        clean();
        // preenche-se a base de dados
        fill();
        // verificação visual
        dumpPersonnes();
    }

    // exibição do conteúdo da tabela
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // preenchimento da tabela
    public static void fill() throws ParseException {
        // criação de pessoas
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // que se guarda
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminação de elementos da tabela
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • linha 12: o ficheiro [spring-config.xml] é utilizado para criar um objeto [ApplicationContext ctx], que é uma imagem em memória do ficheiro. Os beans definidos em [spring-config.xml] são instanciados nesta ocasião.
  • linha 14: solicita-se ao contexto de aplicação ctx uma referência à camada [service]. Sabe-se que esta é representada por um bean denominado «service».
  • linha 16: a base de dados é esvaziada através do método clean das linhas 41-45:
    • linhas 42-44: solicita-se a lista de todas as pessoas ao contexto de persistência e percorre-se essa lista para as eliminar uma a uma. Talvez nos lembremos de que o [spring-config.xml] especifica que a base de dados deve ser gerada no arranque da aplicação. Assim, no nosso caso, a chamada ao método clean é desnecessária, uma vez que partimos de uma base vazia.
  • linha 18: o método fill preenche a base de dados. Esta é definida nas linhas 32-38:
    • linhas 34-35: são criadas duas pessoas
    • linha 37: solicita-se à camada [service] que as torne persistentes.
  • linha 20: o método dumpPersonnes apresenta as pessoas persistentes. Está definido nas linhas 24-29
    • linhas 26-28: solicita-se à camada [service] a lista de todas as pessoas persistentes e estas são apresentadas na consola.

A execução de [InitDB] produz o seguinte resultado:

1
2
3
[personnes]
[72,0,p1,Paul,31/01/2000,true,2]
[73,0,p2,Sylvie,05/07/2001,false,0]

3.1.7. Testes unitários [TestNG]

A instalação do plugin [TestNG] é descrita no parágrafo 5.2.4. O código do programa [TestNG] é o seguinte:


package tests;

....
public class TestNG {

    // camada de serviço
    private IService service;

    @BeforeClass
    public void init() {
        // registo
        log("init");
        // configuração da aplicação
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // camada de serviço
        service = (IService) ctx.getBean("service");
    }

    @BeforeMethod
    public void setUp() throws ParseException {
        // esvaziar a base de dados
        clean();
        // preenchimento da base de dados
        fill();
    }

    // registos
    private void log(String message) {
        System.out.println("----------- " + message);
    }

    // exibição do conteúdo da tabela
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // preenchimento da tabela
    public void fill() throws ParseException {
        log("fill");
        // criação de pessoas
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // que se guarda
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminação de elementos da tabela
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    @Test()
    public void test01() {
...
    }
...
}
  • linha 9: a anotação @BeforeClass indica o método a executar para inicializar a configuração necessária para os testes. É executada antes de o primeiro teste ser executado. A anotação @AfterClass, que não é utilizada aqui, indica o método a executar assim que todos os testes tiverem sido executados.
  • linhas 10-17: o método `init`, anotado com @BeforeClass, utiliza o ficheiro de configuração do Spring para instanciar as diferentes camadas da aplicação e obter uma referência à camada `[service]`. Todos os testes utilizam posteriormente essa referência.
  • linha 19: a anotação @BeforeMethod indica o método a ser executado antes de cada teste. A anotação @AfterMethod, que não é utilizada aqui, indica o método a ser executado após cada teste.
  • linhas 20-25: o método setUp, anotado com @BeforeMethod, esvazia a base de dados (clean, linhas 52-56) e, em seguida, preenche-a com duas pessoas (fill, linhas 42-49).
  • linha 59: a anotação @Test indica um método de teste a ser executado. Passamos agora a descrever esses testes.

@Test()
    public void test01() {
        log("test1");
        dump();
        // lista de pessoas
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }

    @Test()
    public void test02() {
        log("test2");
        // pesquisa de pessoas pelo nome
        List<Personne> personnes = service.getAllLike("p1%");
        assert 1 == personnes.size();
        Personne p1 = personnes.get(0);
        assert "Paul".equals(p1.getPrenom());
    }

    @Test()
    public void test03() throws ParseException {
        log("test3");
        // criação de uma nova pessoa
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // guardar a pessoa
        service.saveOne(p3);
        // solicitação repetida
        Personne loadedp3 = service.getOne(p3.getId());
        // exibi-la
        System.out.println(loadedp3);
        // verificação
        assert "p3".equals(loadedp3.getNom());
    }
  • linhas 2-8: o teste 01. É importante lembrar que, no início de cada teste, a base de dados contém duas pessoas com os nomes p1 e p2, respetivamente.
  • linha 6: solicita-se a lista de pessoas
  • linha 7: verifica-se se o número de pessoas na lista obtida é 2
  • linha 14: solicita-se a lista de pessoas cujo nome comece por p1
  • verifica-se se a lista obtida tem apenas um elemento (linha 15) e se o nome próprio da única pessoa obtida é «Paul» (linha 17)
  • linha 24: cria-se uma pessoa chamada p3
  • linha 25: guarda-se essa pessoa
  • linha 28: solicita-se novamente essa pessoa ao contexto de persistência para verificação
  • linha 32: verifica-se se a pessoa obtida tem, de facto, o nome p3.

@Test()
    public void test04() throws ParseException {
        log("test4");
        // carregar o utilizador p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // exibe-se
        System.out.println(p1);
        // verifica-se
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // alteração do nome próprio
        p1.setPrenom("x");
        // guardar
        service.updateOne(p1);
        // recarrega-se
        p1 = service.getOne(p1.getId());
        // exibe-se
        System.out.println(p1);
        // verifica-se se a versão foi incrementada
        assert (version1 + 1) == p1.getVersion();

    }
  • linha 5: solicita-se a pessoa p1
  • linha 10: verifica-se o seu nome
  • linha 11: regista-se o seu número de versão
  • linha 13: altera-se o seu nome próprio
  • linha 15: guarda-se a alteração
  • linha 17: volta a ser solicitada a pessoa p1
  • linha 21: verifica-se se o número de versão aumentou em 1

@Test()
    public void test05() {
        log("test5");
        // carrega-se a pessoa p2
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // exibe-se
        System.out.println(p2);
        // verifica-se
        assert "p2".equals(p2.getNom());
        // elimina-se a pessoa p2
        service.deleteOne(p2.getId());
        // carrega-se novamente
        p2 = service.getOne(p2.getId());
        // verifica-se se se obteve um ponteiro nulo
        assert null == p2;
        // exibe-se a tabela
        dump();
    }
  • linha 5: solicita-se a pessoa p2
  • linha 10: verifica-se o nome
  • linha 12: elimina-se a pessoa
  • linha 14: volta-se a procurar a pessoa
  • linha 16: verifica-se se não foi encontrada

@Test()
    public void test06() throws ParseException {
        log("test6");
        // é criado um registo com duas pessoas com o mesmo nome (o que viola a regra de exclusividade do nome)
        Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
        // guarda-se esta tabela — deve ocorrer uma exceção e um rollback
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // dump
        dump();
        // verificações
        assert erreur;
        // procura de pessoa com o nome p3
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // dump
        dump();
    }
  • linha 5: cria-se uma tabela com três pessoas, duas das quais têm o mesmo nome «p4». Isto viola a regra de unicidade do nome da @Entity Personne:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • linha 11: o array com as três pessoas é colocado no contexto de persistência. A adição da segunda pessoa, p4, deverá falhar. Como o método [saveArray] decorre numa transação, todas as inserções que possam ter sido feitas anteriormente serão anuladas. No final, não será feita nenhuma adição.
  • linha 18: verifica-se se o método [saveArray] lançou efetivamente uma exceção
  • linhas 20-21: verifica-se se a pessoa p3, que poderia ter sido adicionada, não o foi.

@Test()
    public void test07() {
        log("test7");
        // teste de bloqueio otimista
        // a pessoa p1 está a ser carregada
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // a exibir
        System.out.println(p1);
        // aumenta-se o número de filhos
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // guardamos p1
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // salva-se uma segunda vez — deve ocorrer uma exceção, pois p1 já não tem a versão correta
        // é o newp1 que o tem
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // verificação
        assert erreur;
        // aumenta-se o número de filhos de newp1
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // guardamos o newp1
        service.updateOne(newp1);
        // recarrega-se
        p1 = service.getOne(p1.getId());
        // verifica-se
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • linha 6: solicita-se a pessoa p1
  • linha 12: aumenta-se em 1 o número de filhos
  • linha 14: atualiza-se a pessoa p1 no contexto de persistência. O método [updateOne] torna a nova versão newp1 persistente a partir de p1. Esta difere de p1 pelo seu número de versão, que teve de ser incrementado.
  • linha 15: verifica-se o número de filhos de newp1.
  • linha 21: solicita-se novamente uma atualização da pessoa p1 a partir da versão anterior p1. Deve ocorrer uma exceção, pois p1 não é a versão mais recente da pessoa p1. Esta última versão é newp1.
  • linha 23: verifica-se se o erro ocorreu efetivamente
  • linhas 27-35: verifica-se que, se for feita uma atualização a partir da última versão newp1, tudo corre bem.

@Test()
    public void test08() {
        log("test8");
        // teste de reversão em updateArray
        // carregar a pessoa p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // a exibir
        System.out.println(p1);
        // aumenta-se o número de filhos
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // guardam-se duas alterações, sendo que a segunda deve falhar (pessoa mal inicializada)
        // devido à transação, ambas têm de ser canceladas
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // verificações
        assert erreur;
        // recarrega-se a pessoa p1
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // o número de filhos não deve ter mudado
        assert nbEnfants1 == p1.getNbenfants();
    }
  • O teste 8 é semelhante ao teste 6: verifica o rollback num updateArray que opera sobre uma tabela com duas pessoas, em que a segunda não foi inicializada corretamente. Do ponto de vista do JPA, a operação de fusão na segunda pessoa, queexiste já irá gerar uma ordem SQL insert que irá falhar devido às restrições nullable=false existentes em alguns dos campos da entidade Personne.

@Test()
    public void test09() {
        log("test9");
        // teste de reversão em deleteArray
        // dump
        dump();
        // carregamos a pessoa p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // a exibir
        System.out.println(p1);
        // são efetuadas duas eliminações, sendo que a segunda deve falhar (pessoa desconhecida)
        // devido à transação, ambas têm de ser anuladas
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // verificações
        assert erreur;
        // recarrega-se a pessoa p1
        personnes = service.getAllLike("p1%");
        // verificação
        assert 1 == personnes.size();
        // dump
        dump();
    }
  • O teste 9 é semelhante ao anterior: verifica o rollback num deleteArray que opera sobre uma tabela com duas pessoas, em que a segunda não existe. No entanto, neste caso, o método [deleteOne] da camada [dao] lança uma exceção.

// bloqueio otimista - acesso multithread
    @Test()
    public void test10() throws Exception {
        // adição de uma pessoa
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // criação de N threads para atualizar o número de filhos
        final int N = 20;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
            taches[i].start();
        }
        // aguarda-se a conclusão dos threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // recuperar a pessoa
        p3 = service.getOne(id3);
        // esta pessoa deve ter N filhos
        assert N == p3.getNbenfants();
        // eliminação da pessoa p3
        service.deleteOne(p3.getId());
        // verificação
        p3 = service.getOne(p3.getId());
        // deve haver um ponteiro nulo
        assert p3 == null;
    }
  • A ideia do teste 10 é iniciar N threads (linha 9) para incrementar em paralelo o número de filhos de uma pessoa. Pretende-se verificar se o sistema de numeração de versões resiste bem a este cenário. Foi criado precisamente para isso.
  • linhas 5-6: é criada uma pessoa chamada p3, que é depois guardada. Inicialmente, tem 0 filhos.
  • linha 7: regista-se o seu identificador.
  • linhas 9-14: são iniciadas N threads em paralelo, todas encarregadas de aumentar em 1 o número de filhos de p3.
  • linhas 16-18: aguarda-se o fim de todas as threads
  • linha 20: solicita-se a presença da pessoa p3
  • linha 22: verifica-se se ela tem agora N filhos
  • linha 24: a pessoa p3 é eliminada.

O thread [ThreadMajEnfants] é o seguinte:


package tests;

...
public class ThreadMajEnfants 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;

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

    // núcleo do tópico
    public void run() {
        // acompanhamento
        suivi("lancé");
        // repetimos o ciclo até conseguirmos 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 = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // seguimento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // incrementa em 1 o número de filhos da pessoa
            personne.setNbenfants(nbEnfants + 1);
            // espera 10 ms para libertar o processador
            try {
                // seguimento
                suivi("début attente");
                // interrompe-se para libertar o processador
                Thread.sleep(10);
                // acompanhamento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // espera concluída — tenta-se validar a cópia
            // entretanto, outras threads podem ter alterado o original
            try {
                // tenta-se alterar o original
                service.updateOne(personne);
                // concluído — o original foi alterado
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // versão incorreta do objeto: a exceção é ignorada para recomeçar
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // exceção do Spring que surge de vez em quando
            } catch (RuntimeException e3) {
                // outro tipo de exceção — a exceção é reportada
                throw e3;
            }
        }
        // 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);
    }
}
  • linhas 15-19: o construtor armazena as informações de que necessita para funcionar: o seu nome (linha 16), a referência na camada [service] que deve utilizar (linha 17) e o identificador da pessoa p cujo número de filhos deve aumentar (linha 18).
  • linhas 22-66: o método [run] executado por todas as threads em paralelo.
  • linha 29: o thread tenta repetidamente incrementar o número de filhos da pessoa p. Só pára quando consegue.
  • linha 31: a pessoa p é consultada
  • linha 36: o número de filhos é incrementado na memória
  • linhas 38-47: é feita uma pausa de 10 ms. Isto permitirá que outras threads obtenham a mesma versão da pessoa p. Assim, teremos, ao mesmo tempo, várias threads a deter a mesma versão da pessoa p e a querer modificá-la. É isso que se pretende.
  • linha 52: assim que a pausa terminar, o thread solicita à camada [service] que persista a alteração. Sabemos que, de vez em quando, ocorrerão exceções, pelo que envolvemos a operação num try/catch.
  • linha 55: os testes mostram que ocorrem exceções do tipo [javax.persistence.OptimisticLockException]. Isto é normal: trata-se da exceção lançada pela camada JPA quando um thread pretende alterar a pessoa p sem dispor da sua versão mais recente. Esta exceção é ignorada para permitir que o thread volte a tentar a operação até que seja bem-sucedido.
  • linha 57: os testes mostram que também ocorrem exceções do tipo [org.springframework.transaction.UnexpectedRollbackException]. Isto é incómodo e inesperado. Não tenho explicações a dar. Estamos agora dependentes do Spring, quando gostaríamos de ter evitado isso. Isto significa que, se executarmos a nossa aplicação no JBoss Ejb3, por exemplo, o código do thread terá de ser alterado. A exceção do Spring é aqui também ignorada para permitir que o thread tente novamente a operação de incremento.
  • linha 59: os outros tipos de exceção são reportados à aplicação.

Quando o [TestNG] é executado, obtêm-se os seguintes resultados:

Image

Os 10 testes foram concluídos com sucesso.

O teste 10 merece explicações adicionais, pois o facto de ter sido bem-sucedido tem um lado mágico. Voltemos, em primeiro lugar, à configuração da camada [dao]:


public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

  • linha 4: um objeto [EntityManager] é injetado no campo «em» graças à anotação JPA @PersistenceContext. A camada [dao] é instanciada uma única vez. Trata-se de um singleton utilizado por todas as threads que utilizam a camada JPA. Assim, o EntityManager «em» é comum a todas as threads. É possível verificar isto exibindo o valor de `em` no método [updateOne] utilizado pelas threads [ThreadMajEnfants]: obtém-se o mesmo valor para todas as threads.

Assim, podemos questionar-nos se os objetos persistentes das diferentes threads, manipulados pelo EntityManager «em» — que é o mesmo para todas as threads —, não se irão misturar e criar conflitos entre si. Um exemplo do que poderia acontecer encontra-se no [ThreadMajEnfants]:


        while (!fini) {
            // recuperamos uma cópia da pessoa de idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // acompanhamento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // incrementa em 1 o número de filhos da pessoa
            personne.setNbenfants(nbEnfants + 1);
            // espera 10 ms para libertar o processador
            try {
                // seguimento
                suivi("début attente");
                // interrompe-se para libertar o processador
                Thread.sleep(10);
                // acompanhamento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • linha 3: uma thread T1 recupera a pessoa p
  • linha 8: incrementa o número de filhos de p
  • linha 14: o thread T1 faz uma pausa

Um thread T2 assume o controlo e também executa a linha 3: solicita a mesma pessoa p que o T1. Se o contexto de persistência dos threads fosse o mesmo, a pessoa p, já presente no contexto graças ao T1, deveria ser devolvida ao T2. Com efeito, o método [getOne] utiliza o método [EntityManager].O método API acede ao JPA e este método só acede à base de dados se o objeto solicitado não fizer parte do contexto de persistência; caso contrário, devolve o objeto do contexto de persistência. Se fosse esse o caso, T1 e T2 teriam a mesma pessoa p. T2 incrementaria então o número de filhos de p em 1 novamente (linha 8). Se uma das threads conseguir efetuar a atualização após a pausa, então o número de filhos de p terá aumentado em 2 e não em 1, como previsto. Seria então de esperar que as N threads aumentassem o número de filhos não para N, mas para um valor superior. No entanto, não é esse o caso. Pode-se, assim, concluir que T1 e T2 não têm a mesma referência p. Verifica-se isso fazendo com que as threads exibam a morada de p: esta é diferente para cada uma delas.

Parece, portanto, que as threads:

  • partilhem o mesmo gestor de contexto de persistência (EntityManager)
  • mas cada um tenha um contexto de persistência próprio.

Trata-se apenas de suposições e a opinião de um especialista seria útil neste caso.

3.1.8. Alterar para SGBD

Para alterar o SGBD, basta substituir o ficheiro [src/spring-config.xml] [2] pelo ficheiro [spring-config.xml] do SGBD em questão, na pasta [conf] [1].

O ficheiro [spring-config.xml] da Oracle é, por exemplo, o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- a fonte de dados DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
...
</beans>

Apenas algumas linhas diferem do mesmo ficheiro utilizado anteriormente para o MySQL5:

  • linha 14: o dialeto SQL que o Hibernate deve utilizar
  • linhas 25-28: as características da ligação JDBC com o SGBD

Convida-se o leitor a repetir os testes descritos para o MySQL5 com outros SGBD.

3.1.9. Alterar a implementação JPA

Voltemos à arquitetura dos testes anteriores:

Substituímos a implementação JPA / Hibernate por uma implementação JPA / Toplink. Como o Toplink não utiliza as mesmas bibliotecas que o Hibernate, utilizamos um novo projeto Eclipse:

  • em [1]: o projeto Eclipse. É idêntico ao anterior. Apenas mudam o ficheiro de configuração [spring-config.xml] [2] e a biblioteca [jpa-toplink], que substitui a biblioteca [jpa-hibernate].
  • em [3]: a pasta com os exemplos deste tutorial. Em [4], o projeto Eclipse a importar.

O ficheiro de configuração [spring-config.xml] para o Toplink passa a ser o seguinte:


<?xml version="1.0" encoding="UTF-8"?>

<!-- o JVM deve ser executado com o argumento -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- camadas de aplicação -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- a fonte de dados DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- o gestor de transações -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- tradução das exceções -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- persistência -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

São necessárias poucas alterações para passar do Hibernate para o Toplink:

  • linha 19: a implementação JPA é agora feita pelo Toplink
  • linha 23: a propriedade [databasePlatform] tem um valor diferente do que no Hibernate: o nome de uma classe específica do Toplink. Onde encontrar esse nome foi explicado no parágrafo 2.1.15.2.

É tudo. Note-se a facilidade com que se pode alterar o SGBD ou a implementação JPA com o Spring.

No entanto, ainda não terminámos completamente. Quando executamos o [InitDB], por exemplo, surge uma exceção que não é fácil de compreender:


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use 

A mensagem de erro na linha 1 leva-nos a consultar a documentação do Spring. Aí descobrimos um pouco mais sobre o papel desempenhado por uma declaração obscura do ficheiro [spring-config.xml]:


    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
</bean>

A linha 1 da exceção faz referência a uma classe denominada [InstrumentationLoadTimeWeaver], classe essa que se encontra na linha 13 do ficheiro de configuração do Spring. A documentação do Spring explica que esta classe é necessária, em certos casos, para carregar as classes da aplicação e que, para que seja utilizada, a JVM deve ser executada com um agente. Este agente é fornecido pelo Spring e chama-se [spring-agent]:

  • o ficheiro [spring-agent.jar] encontra-se na pasta <exemplos>/lib [1]. É fornecido com a distribuição Spring 2.x (ver parágrafo 5.11).
  • No [3], cria-se uma configuração de execução [Run/Run...]
  • em [4], cria-se uma configuração de execução Java (existem vários tipos de configurações de execução)
  • em [5], seleciona-se o separador [Main]
  • em [6], atribui-se um nome à configuração
  • em [7], nomeia-se o projeto Eclipse a que esta configuração se refere (utilizar o botão «Browse»)
  • em [8], nomeia-se a classe Java que contém o método [main] (utilizar o botão «Browse»)
  • em [9], passa-se para o separador [Arguments]. Neste separador, é possível especificar dois tipos de argumentos:
    • em [9], os argumentos passados ao método [main]
    • em [10], os que são passados para o JVM, que irá executar o código. O agente Spring é definido através do parâmetro -javaagent:valor do JVM. O valor corresponde ao caminho do ficheiro [spring-agent.jar].
  • no [11]: valida-se a configuração
  • no [12]: a configuração é criada
  • em [13]: executa-se

Feito isto, o [InitDB] é executado e apresenta os mesmos resultados que com o Hibernate. Para o [TestNG], deve proceder-se da mesma forma:

  • em [1], cria-se uma configuração de execução [Run/Run...]
  • em [2], cria-se uma configuração de execução TestNG
  • em [3], seleciona-se o separador [Test]
  • em [4], atribui-se um nome à configuração
  • em [5], nomeia-se o projeto Eclipse a que esta configuração se refere (utilizar o botão «Browse»)
  • em [6], nomeia-se a classe de testes (utilizar o botão «Browse»)
  • em [7], acede-se ao separador [Arguments].
  • em [8]: define-se o argumento -javaagent do JVM.
  • no [9]: valida-se a configuração
  • no [10]: a configuração é criada
  • em [11]: executa-se

Feito isto, o [TestNG] é executado e apresenta os mesmos resultados que com o Hibernate.

3.2. Exemplo 2: JBoss EJB3 / JPA com a entidade Pessoa

Retomamos o mesmo exemplo anterior, mas executamo-lo num contentor EJB3, o de JBoss:

Um contentor Ejb3 é normalmente integrado num servidor de aplicações. O JBoss fornece um contentor Ejb3 «autónomo» que pode ser utilizado fora de um servidor de aplicações. Vamos descobrir que este fornece serviços semelhantes aos fornecidos pelo Spring. Tentaremos verificar qual destes contentores se revela mais prático.

A instalação do contentor JBoss EJB3 é descrita no parágrafo 5.12.

3.2.1. O projeto Eclipse / JBoss EJB3 / Hibernate

O projeto Eclipse é o seguinte:

  • em [1]: o projeto Eclipse. Encontrar-se-á em [6] nos exemplos do tutorial [5]. Iremos importá-lo.
  • em [2]: os códigos Java das camadas apresentados em pacotes:
    • [entites]: o pacote das entidades JPA
    • [dao]: a camada de acesso aos dados — baseia-se na camada JPA
    • [service]: uma camada de serviços, mais do que de negócio. Nesta camada, será utilizado o serviço de transações do contentor EJB3.
    • [tests]: agrupa os programas de teste.
  • em [3]: a biblioteca [jpa-jbossejb3] reúne os ficheiros JAR necessários para o JBoss EJB3 (ver também [7] e [8]).
  • em [4]: a pasta [conf] reúne os ficheiros de configuração para cada um dos SGBD utilizados neste tutorial. Existem sempre dois: o [persistence.xml], que configura a camada JPA, e o [jboss-config.xml], que configura o contentor Ejb3.

3.2.2. As entidades JPA

Aqui é gerida apenas uma entidade, a entidade Personne analisada anteriormente no parágrafo 3.1.2.

3.2.3. A camada [dao]

A camada [dao] apresenta a interface [IDao] descrita anteriormente no parágrafo 3.1.3.

A implementação [Dao] desta interface é a seguinte:


package dao;

...
@Stateless
public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // eliminar uma pessoa através do seu identificador
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    // obter todas as pessoas
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    // obter as pessoas cujo nome corresponde a um padrão
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // obter uma pessoa através do seu identificador
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // guardar um utilizador
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // atualizar uma pessoa
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Este código é em todos os aspetos idêntico ao que tínhamos com o Spring. Apenas as anotações Java mudam e é isso que vamos comentar.
  • linha 4: a anotação @Stateless torna a classe [Dao] um EJB sem estado. A anotação @Stateful torna uma classe um EJB com estado. Um EJB com estado possui campos privados cujo valor deve ser mantido ao longo do tempo. Um exemplo clássico é o de uma classe que contém informações relacionadas com o utilizador web de uma aplicação. Uma instância desta classe está associada a um utilizador específico e, quando o thread de execução de um pedido desse utilizador termina, a instância deve ser mantida para estar disponível no próximo pedido do mesmo cliente. Um EJB @Stateless não tem estado. Se retomarmos o mesmo exemplo, no final do thread de execução de um pedido de um utilizador, o EJB @Stateless irá juntar-se a um pool de EJBs @Stateless e fica disponível para o thread de execução de um pedido de outro utilizador.
  • Para o programador, o conceito de EJB3 @Stateless é semelhante ao do singleton do Spring. Utilizá-lo-á nos mesmos casos.
  • Linha 7: a anotação @PersistenceContext é a mesma que a encontrada na versão Spring da camada [dao]. Ela designa o campo que irá receber o EntityManager, o que permitirá à camada [dao] manipular o contexto de persistência.
  • linha 11: a anotação @TransactionAttribute aplicada a um método serve para configurar a transação na qual o método será executado. Aqui estão alguns valores possíveis para esta anotação:
    • TransactionAttributeType.REQUIRED: o método deve ser executado numa transação. Se já tiver sido iniciada uma transação, as operações de persistência do método ocorrem nessa transação. Caso contrário, é criada e iniciada uma transação.
    • TransactionAttributeType.REQUIRES_NEW: o método deve ser executado numa transação nova. Esta é criada e iniciada.
    • TransactionAttributeType.MANDATORY: o método deve ser executado numa transação existente. Se esta não existir, é lançada uma exceção.
    • TransactionAttributeType.NEVER: o método nunca é executado numa transação.
    • ...

A anotação poderia ter sido colocada na própria classe:


@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {

O atributo é então aplicado a todos os métodos da classe.

3.2.4. A camada [metier / service]

A camada [service] apresenta a interface [IService] analisada anteriormente no parágrafo 3.1.4. A implementação [Service] da interface [IService] é idêntica à implementação analisada anteriormente no parágrafo 3.1.4, com exceção de três detalhes:



@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {

    // camada [dao]
    @EJB
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

  • linha 2: a classe [Service] é um EJB sem estado
  • linha 3: todos os métodos da classe [Service] devem ser executados numa transação
  • linhas 7-8: uma referência ao EJB da camada [dao] será injetada pelo contentor EJB no campo [IDao dao] da linha 8. É a anotação @EJB da linha 7 que solicita esta injeção. O objeto injetado deve ser um EJB. Esta é uma diferença importante em relação ao Spring, onde qualquer tipo de objeto pode ser injetado noutro objeto.

3.2.5. Configuração das camadas

A configuração das camadas [service], [dao] e [JPA] é assegurada pelos seguintes ficheiros:

  • [META-INF/persistence.xml] configura a camada JPA
  • O [jboss-config.xml] configura o contentor Ejb3. Este, por sua vez, utiliza os ficheiros [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Estes últimos ficheiros são fornecidos com o JBoss Ejb3 e garantem uma configuração por predefinição que, normalmente, não é alterada. O programador só se interessa pelo ficheiro [jboss-config.xml]

Vamos analisar os dois ficheiros de configuração:

persistence.xml


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- o fornecedor JPA é o Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- a DataSource JTA é gerida pelo ambiente Java EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- pesquisa de entidades da camada JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- registos SQL do Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- o tipo de SGBD gerido -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />

            <!-- recriação de todas as tabelas (drop+create) aquando da implementação da unidade de persistência -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Este ficheiro é semelhante aos que já vimos ao analisar as entidades JPA. Configura uma camada JPA do Hibernate. As novidades são as seguintes:

  • linha 5: a unidade de persistência jpa não possui o atributo transaction-type que sempre tínhamos até agora:

<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />

Na ausência de valor, o atributo transaction-type tem o valor por defeito «JTA» (para a Java Transaction API), o que indica que o gestor de transações é fornecido por um contentor EJB3. Um gestor «JTA» pode fazer mais do que um gestor «RESOURCE_LOCAL»: pode gerir transações que abrangem várias ligações. Com o JTA, é possível abrir uma transação t1 numa ligação c1 sobre um SGBD 1, uma transação t2 numa ligação c2 com um SGBD 2 e ser capaz de considerar (t1,t2) como uma única transação na qual ou todas as operações são bem-sucedidas (commit) ou nenhuma (rollback).

Aqui, estamos a trabalhar com o gestor JTA do contentor JBoss EJB3.

  • Linha 11: declara a fonte de dados que o gestor JTA deve utilizar. Esta é indicada sob a forma de um nome JNDI (Java Naming and Directory Interface). Esta fonte de dados está definida em [jboss-config.xml].

jboss-config.xml


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- fábrica da DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- nome JNDI da DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- base de dados gerida -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- propriedades do conjunto de ligações -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>

        <!-- gestor de transações, neste caso JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- gestor do cache do Hibernate -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- propriedades de instanciação JNDI? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>

    <!-- o DataSource é solicitado a uma fábrica -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>

</deployment>
  • linha 3: a baliza raiz do ficheiro é <deployment>. Este ficheiro de implementação destina-se essencialmente a configurar a fonte de dados java:/datasource que foi declarada em persistence.xml.
  • A fonte de dados é definida pelo bean «datasource» da linha 38. Verifica-se que a fonte de dados é obtida (linha 40) a partir de uma «factory» definida pelo bean «datasourceFactory» da linha 7. Para obter a fonte de dados da aplicação, o cliente terá de chamar o método [getDatasource] da classe factory (linha 39).
  • linha 7: a factory, que fornece a fonte de dados, é uma classe JBoss.
  • linha 9: o nome JNDI da fonte de dados. Deve ser o mesmo nome que o declarado na baliza <jta-data-source> do ficheiro persistence.xml. Com efeito, a camada JPA irá utilizar este nome JNDI para solicitar a fonte de dados.
  • linhas 12-15: algo mais clássico: as características JDBC da ligação ao SGBD
  • linhas 18-21: configuração do conjunto de ligações interno do contentor JBoss EJB3.
  • linhas 24-26: o gestor JTA. A classe [TransactionManager] injetada na linha 25 está definida no ficheiro [embedded-jboss-beans.xml].
  • linhas 28-30: o cache do Hibernate, um conceito que ainda não abordámos. A classe [CachedConnectionManager], injetada na linha 29, está definida no ficheiro [embedded-jboss-beans.xml]. Note-se que a configuração depende agora do Hibernate, o que nos causará problemas quando quisermos migrar para o Toplink.
  • linhas 32-34: configuração do serviço JNDI.

Terminámos com o ficheiro de configuração do JBoss EJB3. É complexo e muitas coisas continuam obscuras. Foi extraído do [ref1]. No entanto, seremos capazes de o adaptar a outro SGBD (linhas 12-15 de jboss-config.xml, linha 24 de persistence.xml). A migração para o Toplink não foi possível devido à falta de exemplos.

3.2.6. Programa cliente [InitDB]

Vamos abordar a criação de um primeiro cliente da arquitetura descrita anteriormente:

O código de [InitDB] é o seguinte:


package tests;

...
public class InitDB {

    // camada de serviço
    private static IService service;

    // construtor
    public static void main(String[] args) throws ParseException, NamingException {
        // inicia-se o contentor EJB3 JBoss
        // os ficheiros de configuração ejb3-interceptors-aop.xml e embedded-jboss-beans.xml são utilizados
        EJB3StandaloneBootstrap.boot(null);

        // Criação dos beans específicos da aplicação
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Implantar todos os EJBs encontrados no classpath (lento, analisa todos)
        // EJB3StandaloneBootstrap.scanClasspath();

        // São implementados todos os EJB encontrados no classpath da aplicação
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // Inicializa-se o contexto JNDI. O ficheiro jndi.properties é utilizado
        InitialContext initialContext = new InitialContext();

        // instanciação da camada de serviço
        service = (IService) initialContext.lookup("Service/local");
        // Esvazia-se a base de dados
        clean();
        // preenche-se a base de dados
        fill();
        // verificação visual
        dumpPersonnes();
        // paragem do contentor EJB
        EJB3StandaloneBootstrap.shutdown();

    }

    // exibição do conteúdo da tabela
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // preenchimento da tabela
    public static void fill() throws ParseException {
        // criação de pessoas
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // que se guarda
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminação de elementos da tabela
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • A forma de iniciar o contentor JBoss EJB3 foi encontrada em [ref1].
  • linha 13: o contentor é iniciado. [EJB3StandaloneBootstrap] é uma classe do contentor.
  • linha 16: a unidade de implementação configurada por [jboss-config.xml] é implementada no contentor: o gestor JTA, a fonte de dados, o conjunto de ligações, a cache do Hibernate e o serviço JNDI são configurados.
  • linha 22: solicita-se ao contentor que analise a pasta «bin» do projeto Eclipse para localizar os EJB. Os EJB das camadas [service] e [dao] serão encontrados e geridos pelo contentor.
  • linha 25: é inicializado um contexto JNDI. Este irá servir-nos para localizar os EJBs.
  • linha 28: o EJB correspondente à classe [Service] da camada [service] é solicitado ao serviço JNDI. É possível aceder a um EJB localmente (local) ou através da rede (remoto). Aqui, o nome «Service/local» do EJB procurado refere-se à classe [Service] da camada [service] para um acesso local.
  • Agora, a aplicação está implementada e temos uma referência à camada [service]. Estamos na mesma situação que após a linha 11 abaixo do código [InitDB] da versão Spring. Encontramos, assim, o mesmo código nas duas versões.

public class InitDB {

    // camada de serviço
    private static IService service;

    // construtor
    public static void main(String[] args) throws ParseException {
        // configuração da aplicação
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // camada de serviço
        service = (IService) ctx.getBean("service");
        // esvaziar a base de dados
        clean();
        // preenche-se a base de dados
        fill();
        // verifica-se visualmente
        dumpPersonnes();
    }
...
  • linha 36 (JBoss EJB3): paramos o contentor EJB3.

A execução de [InitDB] produz os seguintes resultados:

16:07:00,781  INFO LocalTxDataSource:117 - Bound datasource to JNDI name 'java:/datasource'
...
16:07:01,171  INFO Version:94 - Hibernate EntityManager 3.2.0.CR1
...
16:07:01,296  INFO Ejb3Configuration:94 - Processing PersistenceUnitInfo [
    name: jpa
    ...]
16:07:01,312  INFO Ejb3Configuration:94 - found EJB3 Entity bean: entites.Personne
...
16:07:01,375  INFO Configuration:94 - Reading mappings from resource: META-INF/orm.xml
16:07:01,375  INFO Ejb3Configuration:94 - [PersistenceUnit: jpa] no META-INF/orm.xml found
16:07:01,421  INFO AnnotationBinder:94 - Binding entity from annotated class: entites.Personne
16:07:01,468  INFO EntityBinder:94 - Bind entity entites.Personne on table jpa01_hb_personne
...
16:07:01,859  INFO SettingsFactory:94 - RDBMS: MySQL, version: 5.0.41-community-nt
16:07:01,859  INFO SettingsFactory:94 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.0.5 ( $Date: 2007-03-01 00:01:06 +0100 (Thu, 01 Mar 2007) $, $Revision: 6329 $ )
16:07:01,890  INFO Dialect:94 - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
16:07:01,890  INFO TransactionFactoryFactory:94 - Transaction strategy: org.hibernate.ejb.transaction.JoinableCMTTransactionFactory
...
16:07:02,234  INFO SchemaExport:94 - Running hbm2ddl schema export
16:07:02,234  INFO SchemaExport:94 - exporting generated schema to database
16:07:02,343  INFO SchemaExport:94 - schema export complete
...
16:07:02,562  INFO EJBContainer:479 - STARTED EJB: dao.Dao ejbName: Dao
...
16:07:02,593  INFO EJBContainer:479 - STARTED EJB: service.Service ejbName: Service
...
[personnes]-------------------------------------------------------------------
[1,0,p1,Paul,31/01/2000,true,2]
[2,0,p2,Sylvie,05/07/2001,false,0]

Convidamos o leitor a consultar estes registos. Neles encontram-se informações interessantes sobre o funcionamento do contentor EJB3.

3.2.7. Testes unitários [TestNG]

O código do programa [TestNG] é o seguinte:


package tests;

...
public class TestNG {

    // camada de serviço
    private IService service = null;

    @BeforeClass
    public void init() throws NamingException, ParseException {
        // registo
        log("init");
        // inicia-se o contentor EJB3 JBoss
        // os ficheiros de configuração ejb3-interceptors-aop.xml e embedded-jboss-beans.xml são utilizados
        EJB3StandaloneBootstrap.boot(null);

        // Criação dos beans específicos da aplicação
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Implantar todos os EJBs encontrados no classpath (lento, analisa todos)
        // EJB3StandaloneBootstrap.scanClasspath();

        // São implementados todos os EJB encontrados no classpath da aplicação
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // Inicializa-se o contexto JNDI. O ficheiro jndi.properties é utilizado
        InitialContext initialContext = new InitialContext();

        // instanciação da camada de serviço
        service = (IService) initialContext.lookup("Service/local");
        // esvazia-se a base de dados
        clean();
        // preenche-se a base de dados
        fill();
        // verificação visual
        dumpPersonnes();
    }

    @AfterClass
    public void terminate() {
        // registo
        log("terminate");
        // Desligar o contentor EJB
        EJB3StandaloneBootstrap.shutdown();
    }

    @BeforeMethod
    public void setUp() throws ParseException {
...
    }

...
}
  • O método init (linhas 10-37), que serve para configurar o ambiente necessário para os testes, retoma o código explicado anteriormente em [InitDB].
  • O método `terminate` (linhas 40-45), que é executado no final dos testes (presença da anotação @AfterClass), encerra o contentor EJB3 (linha 44).
  • Todo o resto é idêntico ao que era na versão Spring.

Os testes são bem-sucedidos:

Image

3.2.8. Alterar para SGBD

Para alterar o SGBD, basta substituir o conteúdo da pasta [META-INF] [2] pelo conteúdo da pasta SGBD na pasta [conf] [1]. Tomemos como exemplo o servidor SQL:

O ficheiro [persistence.xml] é o seguinte:


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- o fornecedor JPA é o Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- a DataSource JTA é gerida pelo ambiente Java EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- pesquisa de entidades da camada JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- registos SQL do Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- o tipo de SGBD gerido -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />

            <!-- recriação de todas as tabelas (drop+create) aquando da implementação da unidade de persistência -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Apenas uma linha foi alterada:

  • linha 24: o dialeto SQL que o Hibernate deve utilizar

O ficheiro [jboss-config.xml] do servidor SQL é, por sua vez, o seguinte:


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- fábrica da DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- nome JNDI da DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- base de dados gerida -->
        <property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
        <property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- propriedades do conjunto de ligações -->
    ...
    </bean>

</deployment>

Apenas as linhas 12 a 15 foram alteradas: estas indicam as características da nova ligação JDBC.

Convida-se o leitor a repetir, com outros ficheiros SGBD, os testes descritos para o ficheiro MySQL5.

3.2.9. Alterar a implementação JPA

Tal como foi referido anteriormente, não encontrámos nenhum exemplo de utilização do contentor JBoss EJB3 com o TopLink. Até à data (junho de 2007), ainda não sei se esta configuração é possível.

3.3. Outros exemplos

Vamos resumir o que foi feito com a entidade Personne. Construímos três arquiteturas para realizar os mesmos testes:

1 - uma implementação Spring / Hibernate

2 - uma implementação Spring / Toplink

3 - uma implementação Jboss Ejb3 / Hibernate

Os exemplos do tutorial abordam estas três arquiteturas, juntamente com outras entidades analisadas na primeira parte do tutorial:

Categoria - Artigo

  • em [1]: a versão Spring / Hibernate
  • em [2]: a versão Spring / Toplink
  • em [3]: a versão Jboss Ejb3 / Hibernate

Pessoa - Morada - Atividade

  • em [1]: a versão Spring / Hibernate
  • em [2]: a versão Spring / Toplink
  • em [3]: a versão JBoss EJB3 / Hibernate

Estes exemplos não introduzem novidades no que diz respeito à arquitetura. Situam-se simplesmente num contexto em que há várias entidades a gerir com relações um-para-vários ou vários-para-vários entre si, algo que não se verificava nos exemplos com a entidade Personne.

3.4. Exemplo 3: Spring / JPA numa aplicação web

3.4.1. Apresentação

Retomamos aqui uma aplicação apresentada no seguinte documento:

[ref4]: Noções básicas de desenvolvimento web MVC em Java [http://tahe.developpez.com/java/baseswebmvc/].

Este documento apresenta os fundamentos do desenvolvimento web MVC em Java. Para compreender o exemplo que se segue, o leitor deve possuir esses conhecimentos básicos. A aplicação web utilizará o servidor Tomcat. A instalação deste e a sua utilização no Eclipse são apresentadas no parágrafo 5.3.

A aplicação tinha sido desenvolvida com uma camada [dao] baseada na ferramenta Ibatis / SqlMap [http://ibatis.apache.org/], que assegurava a ponte entre o modelo relacional e o modelo orientada a objetos. Limitar-nos-emos a substituir o Ibatis pelo JPA. A arquitetura da aplicação será a seguinte:

A aplicação web que vamos desenvolver 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 são as quatro operações básicas de uma tabela de base de dados. As capturas de ecrã que se seguem mostram as páginas que a aplicação apresenta ao utilizador.

 

3.4.2. O projeto Eclipse

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

  • em [1]: o projeto web. Trata-se de um projeto Eclipse do tipo [Dynamic Web Project] [2]. Encontra-se em [4], na pasta [3] dos exemplos do tutorial. Vamos importá-lo.
  • no [5]: as fontes e a configuração das camadas [service, dao, jpa]. Mantemos o que já foi desenvolvido no [dao, entites, service] do projeto Eclipse [hibernate-spring-personnes-metier-dao], analisado no parágrafo 3.1.1. Desenvolvemos apenas a camada [web], aqui representada pelo pacote [web]. Além disso, mantemos os ficheiros de configuração [persistence.xml, spring-config.xml] deste projeto, com a única diferença de que iremos utilizar o Postgres SGBD, o que se traduz nas seguintes alterações no [spring-config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
...
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
        </property>
    ...
    </bean>

    <!-- a fonte de dados DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql:jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
....
</beans>

As linhas 8 e 16-19 foram adaptadas ao Postgres.

  • No [6]: a pasta [WebContent] contém as páginas JSP do projeto, bem como as bibliotecas necessárias. Estas últimas são apresentadas no [8]
  • A aplicação pode ser utilizada com vários SGBD. Basta alterar o ficheiro [spring-config.xml]. A pasta [conf] [7] contém o ficheiro [spring-config.xml] adaptado a vários SGBD.

3.4.3. A camada [web]

A nossa aplicação tem a seguinte arquitetura multicamadas:

A camada [web] irá apresentar ecrãs ao utilizador para lhe permitir gerir o grupo de pessoas:

  • lista de 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] (ponto 3.4.1). Para descrever a camada web, iremos apresentar sucessivamente:

  • a sua configuração
  • as suas vistas
  • o seu controlador
  • alguns testes

3.4.3.1. Configuração da aplicação web

Voltemos à arquitetura do projeto Eclipse:

 
  • no pacote [web], encontra-se o controlador da aplicação web: a classe [Application].
  • As páginas JSP / JSTL da aplicação encontram-se em [WEB-INF/vues].
  • A pasta [WEB-INF/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>spring-jpa-hibernate-personnes-crud</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>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 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 23-26: as URLs [/do/*] serão processadas pelo servlet [personnes]
  • linhas 7-8: o servlet [personnes] é uma instância da classe [Application], uma classe que iremos criar.
  • linhas 9-20: definem três parâmetros [urlList, urlEdit, urlErreurs] que identificam as URLs das páginas JSP das vistas [list, edit, erreurs].
  • linhas 28-30: a aplicação tem uma página inicial predefinida [index.jsp], que se encontra na raiz da pasta da aplicação web.
  • linhas 32-35: 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 apresentar 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, [/spring-jpa-hibernate-personnes-crud]. 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 (linha 4) o cliente para a URL [/do/list]. Esta URL apresenta a lista de pessoas do grupo.

3.4.3.2. As páginas JSP / JSTL da aplicação


A vista [list.jsp]


Serve para apresentar a lista de pessoas:

Image

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"/>">
            <c:if test="${erreurs!=null}">
                <h3>Les erreurs suivantes se sont produites :</h3>
                <ul>
                    <c:forEach items="${erreurs}" var="erreur">
                        <li><c:out value="${erreur}"/></li>
                    </c:forEach>
                </ul>
            <hr>
        </c:if>
        <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>
  • Esta vista recebe dois elementos no seu modelo:
    • o elemento [personnes] associado a um objeto do tipo [List] de objetos do tipo [Personne]: uma lista de pessoas.
    • o elemento opcional [erreurs] associado a um objeto do tipo [List], que contém objetos do tipo [String]: uma lista de mensagens de erro.
  • linhas 31-43: percorre-se a lista ${pessoas} para apresentar uma tabela HTML contendo as pessoas do grupo.
  • linha 40: 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 41: o mesmo se aplica ao link [Supprimer].
  • linha 37: 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]:

Image

O ficheiro de descrição desta biblioteca de tags está definido na linha 3.

  • linha 46: o link [Ajout] para adicionar uma nova pessoa tem como destino a URL [/do/edit], tal como o link [Modifier] da linha 40. É o valor -1 do parâmetro [id] que indica que se trata de uma adição e não de uma modificação.
  • linhas 10-18: se o elemento ${erros} estiver presente no modelo, então são apresentadas as mensagens de erro que ele contém.

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&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>

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], exibida tanto num GET como num POST que falhe, recebe os seguintes elementos no seu modelo:
atributo
GET
POST
id
identificador da pessoa
atualizada
idem
version
a sua versão
idem
prenom
o seu nome próprio
nome próprio introduzido
nom
o seu apelido
apelido introduzido
datenaissance
data de nascimento
data de nascimento introduzida
marie
estado civil
estado civil introduzido
nbenfants
o número de filhos
número de filhos introduzido
erreurEdit
em branco
uma mensagem de erro a indicar que a adição
ou da modificação no momento da execução do POST, acionado
pelo botão [Envoyer]. Vazio se não houver erro.
erreurPrenom
vazio
indica um nome próprio incorreto – vazio caso contrário
erreurNom
vazio
indica um apelido incorreto – vazio caso contrário
erreurDateNaissance
vazio
indica uma data de nascimento incorreta – vazio caso contrário
erreurNbEnfants
vazio
indica um número de filhos incorreto – vazio caso contrário
  • linhas 11-15: se o POST do formulário falhar, obter-se-á [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:
    • na exibição inicial do formulário (GET), ${prenom} apresenta 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 nome 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 &agrave; 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 HTTP 500 difere consoante os navegadores: o Firefox apresenta o documento HTML que pode acompanhar esta resposta, enquanto o IE 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:

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 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.

3.4.3.3. O controlador da aplicação

O controlador [Application] está definido no pacote [web]:

Image


Estrut tura e inicialização do controlador


A estrutura do controlador [Application] é a seguinte:


package web;

...


@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
    private IService service = null;

    // inicialização
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // recuperam-se os parâmetros de inicialização do servlet
        ServletConfig config = getServletConfig();
        // processam-se os restantes 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 existe?
            if (valeur == null) {
                // regista-se o erro
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // armazena-se o valor do parâmetro
                params.put(paramètres[i], valeur);
            }
        }
        // a URL da vista [erreurs] tem um tratamento específico
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // configuração da aplicação
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // camada de serviço
        service = (IService) ctx.getBean("service");
        // esvazia-se a base de dados
        clean();
        // preenche-se a base de dados
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }

    // preenchimento da tabela
    public void fill() throws ParseException {
        // criação de pessoas
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // que se guarda
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminação de elementos da tabela
    public void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

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

    // visualizaçã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 {
...
    }

    // eliminaçã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 {
...
    }

    // visualizaçã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 {
        // passa o controlo para o GET
        doGet(request, response);
    }

}
  • linhas 21-34: recuperam-se os parâmetros esperados no ficheiro [web.xml].
  • linhas 37-39: 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 39). 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:

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 alterada e recarregada. É útil para outros tipos de exceções, como já vimos.

  • linhas 40-43: utilizam o ficheiro de configuração do Spring para recuperar uma referência à camada [service]. Após a inicialização do controlador, os seus métodos dispõem de uma referência [service] na 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
/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 encaminhar o processamento das ações solicitadas pelo utilizador para o método correto. O seu código é o seguinte:


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

        
// verifica-se como decorreu a inicialização do servlet
        if (erreursInitialisation.size() != 0) {
            // passamos o controlo 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 executar
        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")) {
            // eliminação de uma pessoa
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // apresentaçã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. 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:

Image

O seu código é o seguinte:


    // exibição da lista de pessoas
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // o modelo da vista [list]
        request.setAttribute("personnes", service.getAll());
        // Exibição da vista [list]
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • linha 4: solicita-se à camada [service] a lista de pessoas do grupo e esta é inserida no modelo sob a chave «pessoas».
  • linha 6: exibe-se a vista [list.jsp] descrita no parágrafo 3.4.3.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]:

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 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:


// eliminação de uma pessoa
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // recuperar o ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
        // eliminação da pessoa
        service.deleteOne(id);
        // redirecionamento para a lista de pessoas
        response.sendRedirect("list");
    }
  • linha 4: o URL processado tem o formato [/do/delete?id=XX]. Recupera-se o valor [XX] do parâmetro [id].
  • linha 6: 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 3.4.3.2:

Image

  • 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]:

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 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.

  • no [1] acima, o formulário de adição e, no [2], o formulário de modificação.

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 alteração?
        Personne personne = null;
        if (id != -1) {
            // alteração - recupera-se a pessoa a alterar
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // adição - cria-se um registo de pessoa vazio
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // coloca-se o objeto [Personne] na sessão do utilizador
        request.getSession().setAttribute("personne", personne);
        // e no modelo da vista [edit]
        request.setAttribute("erreurEdit", "");
        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.isMarie());
        request.setAttribute("nbenfants", personne.getNbenfants());
        // exibição da vista [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 4, recuperamos o valor de [id]. Em seguida, há dois casos:
    1. 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 9, essa pessoa é solicitada à camada [service].
    2. 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 na linha 14.
    3. Em ambos os casos, os elementos [id, version] do modelo da página [edit.jsp], descrito no parágrafo 3.4.3.2, são inicializados.
  • O objeto [Personne] obtido é inserido no modelo da página [edit.jsp]. 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 19-31, 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], linha 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]:

Image

Recorde-se os campos de preenchimento do formulário HTML da vista acima:


<form method="post" action="<c:url value="/do/validate"/>">
...
                        <input type="text" value="${nom}" name="nom" size="20">
...
                        <input type="text" value="${datenaissance}" name="datenaissance">
...
                        <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>
...
                        <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">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>

A consulta POST contém os parâmetros [prenom, nom, datenaissance, marie, nbenfants, id] e é enviada para o URL [/do/validate] (linha 1). É processada pelo seguinte método [doValidatePersonne]:


// validação da alteração/adição de uma pessoa
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // recuperam-se os elementos lançados
        boolean formulaireErroné = false;
        boolean erreur;
        // o nome próprio
        String prenom = request.getParameter("prenom").trim();
        // o nome próprio é válido?
        if (prenom.length() == 0) {
            // registar o erro
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // o apelido
        String nom = request.getParameter("nom").trim();
        // nome próprio válido?
        if (nom.length() == 0) {
            // regista-se 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) {
            // regista-se o erro
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // estado civil
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // 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) {
            // regista-se o erro
            erreur = true;
        }
        // número de filhos incorreto?
        if (erreur) {
            // o erro é assinalado
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // identificação da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
        // O formulário está incorreto?
        if (formulaireErroné) {
            // o formulário é apresentado novamente com as mensagens de erro
            showFormulaire(request, response, "");
            // concluído
            return;
        }
        // O formulário está correto — atualiza-se a pessoa que foi colocada na sessão
        // com as informações enviadas pelo cliente
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // persistência
        try {
            if (id == -1) {
                // criação
                service.saveOne(personne);
            } else {
                // atualização
                service.updateOne(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 vista [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 vista [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • linhas 7-13: 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 15-21: procede-se de forma semelhante para o parâmetro [nom]
  • linhas 23-30: procede-se de forma semelhante para o parâmetro [datenaissance]
  • linha 32: 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 /.../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] para booleano falhar na linha 32, será gerada uma exceção que resultará no envio da página [exception.jsp] ao cliente. Este funcionamento é o que pretendemos.
  • linhas 34-50: recuperamos o parâmetro [nbenfants] e verificamos o seu valor.
  • linha 52: recupera-se o parâmetro [id] sem verificar o seu valor
  • linhas 54-59: se o formulário estiver incorreto, é exibido novamente com as mensagens de erro criadas anteriormente
  • linhas 62-67: se for válido, cria-se um novo objeto [Personne] com os elementos do formulário
  • linhas 69-82: 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 84: 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 79: 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 88-97) 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 na linha 99.

3.4.4. Testes da aplicação web

Foram apresentados vários testes no parágrafo 3.4.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 último solicita o URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

Image

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

Image

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

Image

O utilizador U2 faz o mesmo:

Image

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 [Retour à la liste] do formulário:

Image

Encontra a pessoa [Lemarchand] tal como U1 a alterou (casada, 2 filhos). O número de versão do p2 mudou. Agora, o U2 elimina o [p2]:

O U1 continua a ter a sua própria lista e pretende alterar novamente o [p2]:

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

Image

Descobre que, de facto, o [p2] já não faz parte da lista...

3.4.5. Versão 2

Alteramos ligeiramente a versão anterior para utilizar os arquivos das camadas [service, dao, jpa], em vez dos seus códigos-fonte:

  • em [1]: o novo projeto Eclipse. De notar o desaparecimento dos pacotes [service, dao, entites]. Estes foram encapsulados no arquivo [service-dao-jpa-personne.jar] [2], colocado em [WEB-INF/lib].
  • A pasta do projeto encontra-se em [4]. Vamos importá-la.

Não há mais nada a fazer. Quando a nova aplicação web é iniciada e se solicita a lista de pessoas, obtém-se a seguinte resposta:

 

O Hibernate não encontra a entidade [Personne]. Para resolver este problema, é necessário declarar explicitamente em [persistence.xml] as entidades geridas:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • linha 7: a entidade Personne é declarada.

Feito isto, a exceção desaparece:

 

3.4.6. Alterar a implementação JPA

  • para [1]: o novo projeto Eclipse
  • em [2]: as bibliotecas Toplink substituíram as bibliotecas Hibernate
  • a pasta do projeto está em [4]. Iremos importá-la.

A mudança de implementação JPA implica apenas algumas alterações no ficheiro [spring-config.xml]. Nada mais muda. As alterações introduzidas no ficheiro [spring-config.xml] foram explicadas no parágrafo 3.1.9:


<?xml version="1.0" encoding="UTF-8"?>

<!-- o JVM deve ser executado com o argumento -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...    
            <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
    </bean>
...
</beans>

São necessárias poucas alterações para passar do Hibernate para o Toplink:

  • linha 11: a implementação JPA é agora feita pelo Toplink
  • linha 13: a propriedade [databasePlatform] tem um valor diferente do que no Hibernate: o nome de uma classe específica do Toplink. Onde encontrar esse nome foi explicado no parágrafo 2.1.15.2.

É tudo. Note-se a facilidade com que se pode alterar o SGBD ou a implementação JPA com o Spring. No entanto, ainda não terminámos completamente. Ao executar a aplicação, surge uma exceção:

 

Reconhece-se aqui um problema já encontrado e descrito no parágrafo 3.1.9. Este problema é resolvido ao iniciar o JVM com um agente Spring. Para tal, altera-se a configuração de arranque do Tomcat:

  • para [1]: selecionou-se a opção [Run / Run...] para alterar a configuração do Tomcat
  • para [2]: selecionámos o separador [Arguments]
  • em [3]: adicionámos o parâmetro -javaagent tal como descrito no parágrafo 3.1.9.

Feito isto, é possível solicitar a lista de pessoas:

Image

3.5. Outros exemplos

Gostaríamos de ter mostrado um exemplo web em que o contentor Spring fosse substituído pelo contentor Jboss Ejb3 analisado no parágrafo 3.2:

  • em [1]: o projeto Eclipse
  • em [3]: a sua localização na pasta de exemplos. Vamos importá-lo.

Retomámos a configuração [jboss-config.xml, persistence.xml] descrita no parágrafo 3.2 e, em seguida, alterámos o método [init] do controlador [Application.java] da seguinte forma:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // recuperam-se os parâmetros de inicialização do servlet
            ServletConfig config = getServletConfig();
            // processam-se os restantes 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 existe?
                if (valeur == null) {
                    // regista-se o erro
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // armazena-se o valor do parâmetro
                    params.put(paramètres[i], valeur);
                }
            }
            // a URL da vista [erreurs] tem um tratamento específico
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // configuração da aplicação
            // inicia-se o contentor EJB3 JBoss
            // os ficheiros de configuração ejb3-interceptors-aop.xml e embedded-jboss-beans.xml são utilizados
            EJB3StandaloneBootstrap.boot(null);

            // Criação dos beans específicos da aplicação
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

            // São implementados todos os EJB encontrados no classpath da aplicação
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();

            // Inicializa-se o contexto JNDI. O ficheiro jndi.properties é utilizado
            InitialContext initialContext = new InitialContext();

            // instanciação da camada de serviço
            service = (IService) initialContext.lookup("Service/local");
            // esvazia-se a base de dados
            clean();
            // preenche-se a base de dados
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • linhas 28-38: inicia-se o contentor Ejb3. Este substitui o contentor Spring.
  • linha 41: solicita-se uma referência à camada [service] da aplicação.

À primeira vista, estas são as únicas alterações a efetuar. Durante a execução, surge o seguinte erro:

 

Não consegui perceber onde estava exatamente o problema. A exceção reportada pelo Tomcat parece indicar que o objeto denominado «TransactionManager» foi solicitado ao serviço JNDI e que este não o reconheceu. Deixo aos leitores a tarefa de encontrar uma solução para este problema. Se for encontrada uma solução, esta será integrada no documento.