Skip to content

3. JPA numa arquitetura multicamadas

Para estudar a 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. Ao fazê-lo, explorámos os principais métodos da camada JPA. Estávamos a trabalhar num ambiente denominado «Java SE» (Standard Edition). A JPA funciona tanto em ambientes Java SE como Java EE5 (Enterprise Edition).

Agora que temos uma boa compreensão tanto da configuração da ponte relacional/objeto como da utilização dos métodos da camada JPA, regressamos a uma arquitetura multicamadas mais tradicional:

A camada [JPA] será acedida através de uma arquitetura de duas camadas composta pelas camadas [negócio] e [DAO]. O framework Spring [7], seguido do contentor JBoss EJB3 [8], será utilizado para ligar estas camadas entre si.

Mencionámos anteriormente que o JPA está disponível tanto em ambientes SE como EE5. O ambiente Java EE5 fornece inúmeros serviços para aceder a dados persistentes, incluindo pools de ligação, gestores de transações e muito mais. Pode ser vantajoso para um programador tirar partido destes serviços. O ambiente Java EE5 ainda não foi amplamente adotado (maio de 2007). Está atualmente disponível no Sun Application Server 9.x (Glassfish). Um servidor de aplicações é essencialmente um servidor de aplicações web. Se criar uma aplicação gráfica autónoma utilizando o Swing, não poderá utilizar o ambiente EE e os serviços que este fornece. Isto constitui um problema. Começamos a ver ambientes EE «autónomos», ou seja, aqueles que podem ser utilizados fora de um servidor de aplicações. É o caso do JBoss EJB3, que iremos utilizar neste documento.

Num ambiente EE5, as camadas são implementadas por objetos chamados EJBs (Enterprise Java Beans). Nas versões anteriores do EE, os EJBs (EJB 2.x) eram considerados difíceis de implementar e testar e, por vezes, apresentavam um desempenho inferior ao esperado. Distinguimos entre beans EJB 2.x «entidade» e beans EJB 2.x «sessão». Em resumo, um EJB 2.x «entidade» corresponde a uma linha de uma tabela de base de dados, e um EJB 2.x «sessão» é um objeto utilizado para implementar as camadas [negócio] e [DAO] de uma arquitetura multicamadas. Uma das principais críticas às camadas implementadas com EJBs é que estas só podem ser utilizadas dentro de contentores EJB, um serviço fornecido pelo ambiente EE. Isto torna os testes unitários problemáticos. Assim, no diagrama acima, os testes unitários das camadas [negócio] e [DAO] construídas com EJBs exigiriam a configuração de um servidor de aplicações, uma operação bastante complicada que não incentiva realmente o programador a realizar testes com frequência.

O framework Spring foi criado em resposta à complexidade do EJB2. O Spring fornece, num ambiente SE, um número significativo dos serviços normalmente fornecidos por ambientes EE. Assim, na secção «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 fomentou uma cultura de testes unitários, que de repente se tornaram muito mais fáceis de implementar. O Spring permite a implementação de camadas de aplicação utilizando objetos Java padrão (POJOs, Plain Old/Ordinary Java Objects), possibilitando a sua reutilização noutros contextos. Por fim, integra inúmeras ferramentas de terceiros de forma bastante transparente, nomeadamente ferramentas de persistência como o Hibernate, o iBatis, ...

O Java EE 5 foi concebido para resolver as lacunas da especificação EE anterior. O EJB 2.x evoluiu para o EJB 3. Trata-se de POJOs anotados com tags que os designam como objetos especiais quando se encontram dentro de um contentor EJB 3. Dentro do contentor, o EJB 3 pode tirar partido dos serviços do contentor (pool de ligações, gestor de transações, etc.). Fora do contentor EJB 3, o EJB 3 torna-se um objeto Java padrão. As suas anotações EJB são ignoradas.

Acima, representámos o Spring e o JBoss EJB3 como uma possível infraestrutura (framework) para a nossa arquitetura multicamadas. É esta infraestrutura que irá 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 utilizando POJOs. Estes irão aceder aos serviços do Spring (pool de ligações, gestor de transações) através da injeção de dependências nestes POJOs: ao criá-los, o Spring injeta 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 (na perspetiva do programador) é análogo ao descrito para o Spring. Encontraremos poucas diferenças.

Concluiremos este documento com um exemplo de uma aplicação web de três camadas — básica, mas representativa:

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

Pegamos na entidade Pessoa discutida na Secção 2.1 e integramo-la numa arquitetura multicamadas, em que as camadas são integradas utilizando o Spring e a camada de persistência é implementada pelo Hibernate.

Presume-se que o leitor tenha um conhecimento básico do Spring. Caso contrário, pode ler o documento seguinte, 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 « » do Eclipse/Spring/Hibernate

O projeto Eclipse é o seguinte:

  • em [1]: o projeto Eclipse. Pode ser encontrado em [6], nos exemplos do tutorial [5]. Vamos importá-lo.
  • em [2]: o código Java para as camadas apresentadas nos pacotes:
    • [entities]: o pacote de entidades JPA
    • [dao]: a camada de acesso a dados — baseada na camada JPA
    • [service]: uma camada de serviço, em vez de uma camada de negócios. Iremos utilizar aqui o serviço de transações do contentor.
    • [tests]: contém os programas de teste.
  • em [3]: a biblioteca [jpa-spring] contém os JARs necessários ao Spring (ver também [7] e [8]).
  • em [4]: a pasta [conf] contém os ficheiros de configuração do Spring para cada um dos SGBDs utilizados neste tutorial.

3.1.2. Entidades JPA

Aqui é gerida apenas uma entidade, a entidade Pessoa abordada na Secção 2.1, cuja configuração é apresentada 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;
 
    // manufacturers
    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 and setters
...
}

3.1.3. A camada [ DAO]

A camada [DAO] fornece a seguinte interface IDao:


package dao;
 
import java.util.List;
 
import entites.Personne;
 
public interface IDao {
    // find a person via his/her login
    public Personne getOne(Integer id);
 
    // get all the people
    public List<Personne> getAll();
 
    // save a person
    public Personne saveOne(Personne personne);
 
    // update a person
    public Personne updateOne(Personne personne);
 
    // delete a person via his/her login
    public void deleteOne(Integer id);
 
    // get people whose name corresponds to a model
    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;
 
    // supprimer une personne via son identifiant
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }
 
    @SuppressWarnings("unchecked")
    // obtenir toutes les personnes
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }
 
    @SuppressWarnings("unchecked")
    // obtenir les personnes dont le nom correspond àun modèle
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // obtenir une personne via son identifiant
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }
 
    // sauvegarder une personne
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }
 
    // mettre à jour une personne
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }
 
}
  • Primeiro, repare na simplicidade da implementação de [Dao]. Isto deve-se à utilização da camada JPA, que trata da maior parte do trabalho de acesso aos dados.
  • Linha 10: A classe [Dao] implementa a interface [IDao]
  • Linha 13: O objeto [EntityManager] será utilizado para manipular o contexto de persistência JPA. Por conveniência, por vezes referir-nos-emos a ele como o próprio contexto de persistência. O contexto de persistência conterá entidades Person.
  • Linha 12: Em nenhum ponto do código o campo [EntityManager em] é inicializado. Será inicializado pelo Spring quando a aplicação for iniciada. É a anotação JPA @PersistenceContext na linha 12 que instrui o Spring a injetar um gestor de contexto de persistência em em.
  • 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 cujos nomes correspondem a um determinado padrão é recuperada através de uma consulta JPQL.
  • Linhas 38–40: A pessoa com um determinado ID é recuperada utilizando o método `find` da API JPA. Retorna um ponteiro nulo se a pessoa não existir.
  • Linhas 43–46: Uma pessoa é tornada persistente utilizando o método `persist` da API JPA. O método torna a pessoa persistente.
  • Linhas 49–51: Uma pessoa é atualizada utilizando o método `merge` da API JPA. Este método só faz sentido se a pessoa que está a ser atualizada tiver sido previamente desligada. O método torna a pessoa criada desta forma persistente.
  • Linhas 16–22: A eliminação da pessoa cujo ID é passado como parâmetro é feita em duas etapas:
    • Linha 17: É procurada 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 for encontrada, é removida do contexto de persistência utilizando o método remove da API JPA.
  • O que não é visível neste momento é que cada método será executado dentro 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 {
 
    // error code
    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 and setter
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
}
  • Linha 4: [DaoException] estende [RuntimeException]. É, portanto, um tipo de exceção que o compilador não exige que seja tratada com um bloco try/catch nem incluída nas assinaturas dos métodos. Por est e razão, [DaoException] não está incluída na assinatura do método [deleteOne] da interface [IDao]. Isto permite que a interface seja implementada por uma classe que lance um tipo diferente de exceção, desde que também derive de [RuntimeException].
  • Para distinguir entre os erros que podem ocorrer, utilizamos o código de erro na linha 7. Os três construtores nas linhas 14, 19 e 24 são os da classe pai [RuntimeException], aos quais adicionámos um parâmetro: o código de erro que queremos atribuir à exceção.

3.1.4. A camada [serviço de negócios/ ]

A camada [de serviço] fornece a seguinte interface [IService]:


package service;
 
import java.util.List;
 
import entites.Personne;
 
public interface IService {
    // find a person via his/her login
    public Personne getOne(Integer id);
 
    // get all the people
    public List<Personne> getAll();
 
    // save a person
    public Personne saveOne(Personne personne);
 
    // update a person
    public Personne updateOne(Personne personne);
 
    // delete a person via his/her login
    public void deleteOne(Integer id);
 
    // get people whose names match a model
    public List<Personne> getAllLike(String modele);
 
    // delete several people at once
    public void deleteArray(Personne[] personnes);
 
    // save several people at once
    public Personne[] saveArray(Personne[] personnes);
 
    // update several people at once
    public Personne[] updateArray(Personne[] personnes);
 
}
  • linhas 8–24: a interface [IService] herda os métodos da interface [IDao]
  • linha 27: o método [deleteArray] permite eliminar um conjunto de pessoas dentro de uma transação: ou todas as pessoas são eliminadas ou nenhuma é.
  • Linhas 30 e 33: Métodos análogos a [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;
 
...
 
// all class methods take place in a transaction
@Transactional
public class Service implements IService {
 
    // layer [dao]
    private IDao dao;
 
    public IDao getDao() {
        return dao;
    }
 
    public void setDao(IDao dao) {
        this.dao = dao;
    }
 
    // delete several people at once
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }
 
    // delete a person via his/her login
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }
 
    // get all the people
    public List<Personne> getAll() {
        return dao.getAll();
    }
 
    // get people whose names match a model
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }
 
    // find a person via his/her login
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }
 
    // save several people at once
    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;
    }
 
    // save a person
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }
 
    // update several people at once
    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;
    }
 
    // update a person
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }
 
}
  • Linha 6: A anotação @Transactional do Spring indica que todos os métodos da classe devem ser executados dentro de uma transação. Uma transação será iniciada antes do método começar a ser executado e encerrada após a execução. Se ocorrer uma exceção do tipo [RuntimeException] ou de uma subclasse durante a execução do método, um rollback automático cancela toda a transação; caso contrário, um commit automático valida-a. Note que o código Java não precisa de se preocupar com 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 quando a aplicação é iniciada.
  • Os métodos de [Service] simplesmente chamam os métodos da interface [IDao dao] a partir da linha 10. Deixaremos que o leitor analise o código. Não há dificuldades particulares.
  • Mencionámos anteriormente que cada método de [Service] é executado dentro de uma transação. Esta transação está associada ao thread de execução do método. Dentro deste 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, precisa de executar o método [deleteOne] da camada [dao] N vezes. Estas N execuções ocorrerão dentro da thread de execução do método [deleteArray] e, portanto, dentro da mesma transação. Assim, serão todas confirmadas se tudo correr bem, ou todas revertidas se ocorrer uma exceção em qualquer uma das N execuções do método [deleteOne] na camada [dao].

3.1.5. Configuração da camada

A configuração das camadas [service], [dao] e [JPA] é gerida pelos dois ficheiros acima mencionados: [META-INF/persistence.xml] e [spring-config.xml]. Ambos os ficheiros devem estar no classpath da aplicação, razão pela qual se encontram na pasta [src] do projeto Eclipse. O nome do ficheiro [spring-config.xml] é arbitrário.

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», ou seja, transações não fornecidas por um contentor EJB3. Estas transações são criadas e geridas pelo Spring e são configuradas 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">
 
    <!-- application layers -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>
 
    <!-- persistence layer 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>
 
    <!-- data source 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>
 
    <!-- transaction manager -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>
 
    <!-- translation of exceptions -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
    <!-- persistence annotations -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
</beans>
  • linhas 2-5: a tag raiz <beans> do ficheiro de configuração. Não iremos comentar os vários atributos desta tag. Certifique-se de os copiar e colar com cuidado, pois um erro em qualquer um destes atributos pode causar erros que, por vezes, são 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.
  • Em última análise, as linhas 8–11 configuraram as camadas [dao] e [service]. Veremos mais tarde quando e como elas serão instanciadas.
  • Linhas 35–42: É definida uma fonte de dados. Já nos deparámos com o conceito de fonte de dados quando estudámos as entidades JPA com o Hibernate:

Acima, [c3p0], referido como um «pool de ligações», poderia ter sido chamado de «fonte de dados». Uma fonte de dados fornece o serviço de «pool de conexões». Com o Spring, utilizaremos uma fonte de dados diferente de [c3p0]. Trata-se do [DBCP] do projeto Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Os arquivos [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 nome de utilizador da ligação e a respetiva palavra-passe (linhas 40–41).
  • linhas 14–32: configurar a camada JPA
  • linhas 14–15: definir um bean [EntityManagerFactory] capaz de criar objetos [EntityManager] para gerir contextos de persistência. A classe instanciada [LocalContainerEntityManagerFactoryBean] é fornecida pelo Spring. Requer vários parâmetros para ser instanciada, definidos nas linhas 16–31.
  • linha 16: a fonte de dados a utilizar para obter ligações ao SGBD. Esta é a fonte [DBCP] definida nas linhas 35–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 a base de dados seja gerada (drop e create) quando a aplicação for iniciada.
  • Linhas 28–31: definem um «carregador de classes». Não consigo explicar claramente o papel deste bean utilizado pelo EntityManagerFactory na camada JPA. No entanto, envolve passar para a JVM que executa a aplicação o nome de um arquivo cujo conteúdo irá gerir o carregamento de classes quando a aplicação for iniciada. Aqui, esse arquivo é [spring-agent.jar], localizado na biblioteca do utilizador [jpa-spring] (ver acima). Veremos que o Hibernate não precisa deste agente, mas o Toplink precisa.
  • Linhas 45–50: definem o gestor de transações a ser utilizado
  • Linha 45: indica que as transações são geridas utilizando anotações Java (também poderiam ter sido declaradas no spring-config.xml). Especificamente, isto refere-se à anotação @Transactional encontrada 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 o EntityManagerFactory que gere a camada JPA. Este é o definido 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 na classe [dao.Dao] (linha 12).
  • Linhas 53–54: definem a classe do Spring que gere, em particular, a anotação @Repository, que torna uma classe assim anotada elegível para a tradução de exceções nativas do controlador JDBC do SGBD em exceções genéricas do Spring do tipo [DataAccessException]. Esta tradução encapsula a exceção JDBC nativa num tipo [DataAccessException] com várias subclasses:

Image

Esta tradução permite que o programa cliente lide com exceções de forma genérica, independentemente do DBMS de destino. Não utilizámos a anotação @Repository no nosso código Java. Por conseguinte, as linhas 53–54 são desnecessárias. Deixámo-las simplesmente para fins informativos.

Terminámos o ficheiro de configuração do Spring. É complexo e muitos aspetos permanecem pouco claros. Foi retirado da documentação do Spring. Felizmente, adaptá-lo a várias situações resume-se frequentemente a duas modificações:

  • a base de dados de destino: linhas 38–41. Iremos fornecer um exemplo para Oracle.
  • a implementação JPA: linhas 14–32. Apresentaremos um exemplo com o TopLink.

3.1.6. Programa cliente [ InitDB]

Vamos agora escrever um primeiro cliente para a arquitetura descrita acima:

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


package tests;
 
...
public class InitDB {
 
    // service layer
    private static IService service;
 
    // manufacturer
    public static void main(String[] args) throws ParseException {
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
    }
 
    // table content display
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }
 
    // table filling
    public static void fill() throws ParseException {
        // creating people
        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);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }

    // deleting table items
    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 representação em memória do ficheiro. Os beans definidos em [spring-config.xml] são instanciados neste momento.
  • linha 14: o contexto de aplicação ctx é solicitado a fornecer uma referência à camada [service]. Sabemos que esta é representada por um bean denominado «service».
  • Linha 16: a base de dados é limpa utilizando o método clean nas linhas 41–45:
    • Linhas 42–44: Solicitamos a lista de todos os utilizadores ao contexto de persistência e percorremos essa lista para os eliminar um a um. Deve lembrar-se de que o [spring-config.xml] especifica que a base de dados deve ser gerada quando a aplicação é iniciada. Portanto, no nosso caso, chamar o método `clean` é desnecessário, uma vez que estamos a começar com uma base de dados vazia.
  • Linha 18: O método `fill` preenche a base de dados. Isto está definido nas linhas 32–38:
    • Linhas 34–35: São criadas duas pessoas
    • linha 37: a camada [service] é solicitada a torná-las persistentes.
  • Linha 20: O método `dumpPersonnes` exibe as pessoas persistentes. Está definido nas linhas 24–29
    • Linhas 26–28: Solicitamos a lista de todas as pessoas persistentes à camada [service] e exibimo-las 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 na secção 5.2.4. O código do programa [TestNG] é o seguinte:


package tests;
 
....
public class TestNG {
 
    // service layer
    private IService service;
 
    @BeforeClass
    public void init() {
        // log
        log("init");
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
    }
 
    @BeforeMethod
    public void setUp() throws ParseException {
        // empty the base
        clean();
        // fill it
        fill();
    }
 
    // logs
    private void log(String message) {
        System.out.println("----------- " + message);
    }
 
    // table content display
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }
 
    // table filling
    public void fill() throws ParseException {
        log("fill");
        // creating people
        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);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }
 
    // deleting table items
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
 
    @Test()
    public void test01() {
...
    }
...
}
  • linha 9: A anotação @BeforeClass designa o método a ser executado para inicializar a configuração necessária para os testes. É executado antes da execução do primeiro teste. A anotação @AfterClass, que não é utilizada aqui, designa o método a ser executado após a conclusão de todos os testes.
  • linhas 10–17: O método init, anotado com @BeforeClass, utiliza o ficheiro de configuração do Spring para instanciar as várias camadas da aplicação e obter uma referência à camada [service]. Todos os testes utilizam então esta referência.
  • Linha 19: A anotação @BeforeMethod designa o método a ser executado antes de cada teste. A anotação @AfterMethod, não utilizada aqui, designa o método a ser executado após cada teste.
  • Linhas 20–25: O método setUp, anotado com @BeforeMethod, limpa a base de dados (limpar linhas 52–56) e, em seguida, preenche-a com duas pessoas (preencher linhas 42–49).
  • Linha 59: A anotação @Test designa um método de teste a ser executado. Vamos agora descrever estes testes.

@Test()
    public void test01() {
        log("test1");
        dump();
        // list of persons
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }
 
    @Test()
    public void test02() {
        log("test2");
        // search for people by name
        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");
        // create a new person
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // we keep it
        service.saveOne(p3);
        // we ask for it again
        Personne loadedp3 = service.getOne(p3.getId());
        // we display it
        System.out.println(loadedp3);
        // check
        assert "p3".equals(loadedp3.getNom());
    }
  • linhas 2–8: Teste 01. Lembre-se de que, no início de cada teste, a base de dados contém duas pessoas chamadas p1 e p2.
  • linha 6: solicitamos a lista de pessoas
  • linhas 7: verificamos se o número de pessoas na lista devolvida é 2
  • linha 14: solicitamos a lista de pessoas cujo apelido começa por p1
  • Verificamos se a lista resultante tem apenas um elemento (linha 15) e se o primeiro nome da única pessoa encontrada é «Paul» (linha 17)
  • linha 24: criamos uma pessoa chamada p3
  • linha 25: persistimos essa pessoa
  • linha 28: recuperamo-la do contexto de persistência para verificação
  • linha 32: verificamos se a pessoa recuperada tem, de facto, o nome p3.

@Test()
    public void test04() throws ParseException {
        log("test4");
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // we check
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // change the first name
        p1.setPrenom("x");
        // we save
        service.updateOne(p1);
        // recharge
        p1 = service.getOne(p1.getId());
        // we display it
        System.out.println(p1);
        // check that the version has been incremented
        assert (version1 + 1) == p1.getVersion();
 
    }
  • linha 5: perguntamos pela pessoa p1
  • linha 10: verificamos o nome dela
  • linha 11: anotamos o número da versão
  • linha 13: alteramos o seu primeiro nome
  • linha 15: guardamos a alteração
  • linha 17: solicitamos a pessoa p1 novamente
  • linha 21: verificar se o número da versão aumentou em 1

@Test()
    public void test05() {
        log("test5");
        // we load person p2
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // we display it
        System.out.println(p2);
        // we check
        assert "p2".equals(p2.getNom());
        // delete person p2
        service.deleteOne(p2.getId());
        // recharge it
        p2 = service.getOne(p2.getId());
        // check that a null pointer has been obtained
        assert null == p2;
        // table is displayed
        dump();
    }
  • linha 5: perguntamos pela pessoa p2
  • linha 10: verificamos o nome dela
  • linha 12: eliminamos essa pessoa
  • linha 14: perguntamos por ela novamente
  • linha 16: verificamos se não a encontrámos

@Test()
    public void test06() throws ParseException {
        log("test6");
        // on crée un tableau de 2 personnes de même nom (enfreint la règle d'unicité du nom)
        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)};
        // on sauvegarde ce tableau - on doit obtenir une exception et un rollback
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // dump
        dump();
        // vérifications
        assert erreur;
        // recherche personne de nom p3
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // dump
        dump();
    }
  • Linha 5: Criamos uma matriz com três pessoas, duas das quais têm o mesmo nome «p4». Isto viola a regra de exclusividade para o nome da entidade @Entity Person:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • linha 11: o conjunto de três pessoas é colocado no contexto de persistência. A adição da segunda pessoa, p4, deve falhar. Uma vez que o método [saveArray] é executado dentro de uma transação, quaisquer inserções feitas anteriormente serão revertidas. Em última análise, não serão feitas quaisquer adições.
  • Linha 18: Verificamos se [saveArray] realmente lançou uma exceção
  • Linhas 20–21: Verificamos que a pessoa p3, que poderia ter sido adicionada, não foi adicionada.

@Test()
    public void test07() {
        log("test7");
        // test optimistic locking
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // increase the number of children
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // save p1
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // we save a second time - we should have an exception because p1 no longer has the correct version
        // newp1 has it
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // check
        assert erreur;
        // we increase the number of newp1 children
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // save newp1
        service.updateOne(newp1);
        // recharge
        p1 = service.getOne(p1.getId());
        // we check
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • Linha 6: Solicitar a pessoa p1
  • linha 12: aumentamos o número de filhos em 1
  • linha 14: atualizamos a pessoa p1 no contexto de persistência. O método [updateOne] torna a nova versão newp1 persistente a partir de p1. Ela difere de p1 pelo seu número de versão, que deve ter sido incrementado.
  • linha 15: verificamos o número de filhos de newp1.
  • linha 21: solicitamos uma atualização da pessoa p1 com base na versão antiga p1. Deve ocorrer uma exceção porque p1 não é a versão mais recente da pessoa p1. A versão mais recente é newp1.
  • Linha 23: Verificamos se o erro ocorreu efetivamente
  • Linhas 27–35: Verificamos que, se for realizada uma atualização a partir da versão mais recente newp1, tudo funciona corretamente.

@Test()
    public void test08() {
        log("test8");
        // test rollback on updateArray
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // increase the number of children
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // save 2 modifications, the 2nd of which must fail (person incorrectly initialized)
        // because of the transaction, both must be cancelled
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // checks
        assert erreur;
        // we recharge person p1
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // her number of children must not have changed
        assert nbEnfants1 == p1.getNbenfants();
    }
  • O Teste 8 é semelhante ao Teste 6: verifica o rollback numa operação `updateArray` realizada numa matriz de duas pessoas, em que a segunda pessoa não foi inicializada corretamente. Do ponto de vista do JPA, a operação `merge` na segunda pessoa — que ainda não existe — irá gerar uma instrução SQL `insert` que falhará devido às restrições `nullable=false` em alguns dos campos da entidade `Person`.

@Test()
    public void test09() {
        log("test9");
        // test rollback on deleteArray
        // dump
        dump();
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // we make 2 deletions, the 2nd of which must fail (unknown person)
        // because of the transaction, both must be cancelled
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // checks
        assert erreur;
        // we recharge person p1
        personnes = service.getAllLike("p1%");
        // check
        assert 1 == personnes.size();
        // dump
        dump();
    }
  • O Teste 9 é semelhante ao anterior: verifica o rollback numa operação `deleteArray` numa matriz de duas pessoas, em que a segunda pessoa não existe. No entanto, neste caso, o método `[deleteOne]` na camada `[dao]` lança uma exceção.

// optimistic locking - multi-threaded access
    @Test()
    public void test10() throws Exception {
        // add a person
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // creation of N threads for updating the number of children
        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();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // we pick up the person
        p3 = service.getOne(id3);
        // she must have N children
        assert N == p3.getNbenfants();
        // delete person p3
        service.deleteOne(p3.getId());
        // check
        p3 = service.getOne(p3.getId());
        // we must have a null pointer
        assert p3 == null;
    }
  • A ideia por trás do Teste 10 é iniciar N threads (linha 9) para incrementar o número de filhos de uma pessoa em paralelo. Queremos verificar se o sistema de números de versão consegue lidar com este cenário. Foi criado para este fim.
  • Linhas 5–6: É criada e persistida uma pessoa chamada p3. Inicialmente, ela tem 0 filhos.
  • Linha 7: Registamos o seu identificador.
  • Linhas 9–14: Iniciamos N threads em paralelo, cada uma com a tarefa de incrementar o número de filhos de p3 em 1.
  • Linhas 16–18: aguardamos que todas as threads terminem
  • linha 20: solicitamos a visualização da pessoa p3
  • Linha 22: verificamos se p3 tem agora N filhos
  • linha 24: a pessoa p3 é eliminada.

A thread [ThreadMajEnfants] é a seguinte:


package tests;
 
...
public class ThreadMajEnfants extends Thread {
    // thread name
    private String name;
 
    // reference on the [service] layer
    private IService service;
 
    // the id of the person we're going to work on
    private int idPersonne;
 
    // manufacturer
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }
 
    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // we loop until we have succeeded in incrementing by 1
        // person's number of children idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // follow-up
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // increments the number of children by 1
            personne.setNbenfants(nbEnfants + 1);
            // 10 ms wait to abandon processor
            try {
                // follow-up
                suivi("début attente");
                // we pause to let the processor
                Thread.sleep(10);
                // follow-up
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            try {
                // we try to modify the original
                service.updateOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // incorrect object version: exception ignored to start again
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // with the occasional Spring exception
            } catch (RuntimeException e3) {
                // another type of exception - it is reassembled
                throw e3;
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
 
    // follow-up
    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 à camada [service] que deve utilizar (linha 17) e o identificador da pessoa p cujo número de filhos deve incrementar (linha 18).
  • linhas 22–66: o método [run] executado por todas as threads em paralelo.
  • linha 29: a thread tenta repetidamente incrementar o número de filhos da pessoa p. Ela só pára quando consegue.
  • linha 31: a pessoa p é consultada
  • linha 36: o seu 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, ao mesmo tempo, várias threads terão a mesma versão da pessoa p e desejarão modificá-la. Este é o comportamento desejado.
  • Linha 52: Assim que a pausa terminar, a thread solicita à camada [service] que persista a alteração. Sabemos que ocorrerão exceções de vez em quando, por isso envolvemos a operação num bloco try/catch.
  • Linha 55: Os testes mostram que encontramos exceções do tipo [javax.persistence.OptimisticLockException]. Isto é normal: é a exceção lançada pela camada JPA quando uma thread tenta modificar a pessoa p sem ter a sua versão mais recente. Esta exceção é ignorada para permitir que a thread repita a operação até que seja bem-sucedida.
  • Linha 57: Os testes mostram que também obtemos exceções do tipo [org.springframework.transaction.UnexpectedRollbackException]. Isto é irritante e inesperado. Eu não tenho nenhuma explicação para isto. Estamos agora dependentes do Spring, apesar de querermos evitar isso. Isto significa que, se executarmos a nossa aplicação no JBoss EJB3, por exemplo, o código da thread terá de ser alterado. A exceção do Spring também é ignorada aqui para permitir que a thread repita a operação de incremento.
  • Linha 59: Outros tipos de exceção são propagados para a aplicação.

Quando o [TestNG] é executado, obtemos os seguintes resultados:

Image

Todos os 10 testes foram concluídos com sucesso.

O Teste 10 merece uma explicação mais aprofundada, pois o facto de ter sido aprovado tem um certo aspeto mágico. Vamos primeiro rever a configuração da camada [dao]:


public class Dao implements IDao {
 
    @PersistenceContext
    private EntityManager em;
 
  • Linha 4: um objeto [EntityManager] é injetado no campo em utilizando a anotação @PersistenceContext do JPA. A camada [dao] é instanciada apenas uma vez. Trata-se de um singleton utilizado por todas as threads que utilizam a camada JPA. Assim, o EntityManager em é partilhado por todas as threads. Isto pode ser verificado exibindo o valor de em no método [updateOne] utilizado pelas threads [ThreadMajEnfants]: o valor é o mesmo para todas as threads.

Consequentemente, poder-se-ia questionar se os objetos persistentes das diferentes threads, geridos pelo EntityManager em — que é o mesmo para todas as threads — não se misturariam e criariam conflitos entre si. Um exemplo do que poderia acontecer pode ser encontrado em ThreadMajEnfants:


        while (!fini) {
            // on récupère une copie de la personne d'idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // suivi
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // incrémente de 1 le nbre d'enfants de la personne
            personne.setNbenfants(nbEnfants + 1);
            // attente de 10 ms pour abandonner le processeur
            try {
                // suivi
                suivi("début attente");
                // on s'interrompt pour laisser le processeur
                Thread.sleep(10);
                // suivi
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • Linha 3: Um thread T1 recupera a pessoa p
  • linha 8: incrementa o número de filhos de p
  • linha 14: o segmento T1 faz uma pausa

Um segmento de execução 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 segmentos de execução fosse o mesmo, a pessoa p — já presente no contexto graças ao T1 — deveria ser devolvida ao T2. De facto, o método [getOne] utiliza o método [EntityManager].find da API 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 a partir 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 atualizar com sucesso os dados após a pausa, então o número de filhos de p teria aumentado em 2 e não em 1, como esperado. Poder-se-ia então esperar que as N threads definissem o número de filhos não como N, mas como um valor superior. No entanto, não é esse o caso. Podemos, portanto, concluir que T1 e T2 não têm a mesma referência p. Verificamos isto fazendo com que as threads apresentem o endereço de p: é diferente para cada uma delas.

Parece, portanto, que as threads:

  • partilham o mesmo gestor de contexto de persistência (EntityManager)
  • mas cada uma tem o seu próprio contexto de persistência.

Estas são apenas suposições, e a opinião de um especialista seria útil neste caso.

3.1.8. Alterar o SGBD

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

O ficheiro [spring-config.xml] para o 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>
 
    <!-- data source 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 foram alteradas em comparação com o 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 ao SGBD

Os leitores são encorajados a repetir os testes descritos para o MySQL 5 com outros SGBDs.

3.1.9. Alterar a implementação do JPA

Voltemos à arquitetura dos testes anteriores:

Estamos a substituir a implementação JPA/Hibernate por uma implementação JPA/TopLink. Uma vez que o TopLink não utiliza as mesmas bibliotecas que o Hibernate, estamos a utilizar um novo projeto Eclipse:

  • em [1]: o projeto Eclipse. É idêntico ao anterior. As únicas alterações são o ficheiro de configuração [spring-config.xml] [2] e a biblioteca [jpa-toplink], que substitui a biblioteca [jpa-hibernate].
  • em [3]: a pasta de exemplos para este 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"?>
 
<!-- the JVM must be launched with the -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar argument 
    (à 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">
 
    <!-- application layers -->
    <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>
 
    <!-- data source 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>
 
    <!-- transaction manager -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
 
    <!-- translation of exceptions -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
    <!-- persistence -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
</beans>

Basta alterar apenas algumas linhas para mudar do Hibernate para o Toplink:

  • linha 19: a implementação JPA é agora gerida 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 este nome foi explicado na secção 2.1.15.2.

É tudo. Repare como é fácil mudar de SGBD ou de implementações JPA com o Spring.

No entanto, ainda não terminámos. Quando executa [InitDB], por exemplo, obtém 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 solicita que consulte a documentação do Spring. Aí, ficará a saber um pouco mais sobre o papel desempenhado por uma declaração obscura no 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 refere-se a uma classe chamada [InstrumentationLoadTimeWeaver], que pode ser encontrada 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 funcione, a JVM deve ser iniciada com um agente. Este agente é fornecido pelo Spring e chama-se [spring-agent]:

  • o ficheiro [spring-agent.jar] está localizado na pasta <examples>/lib [1]. Está incluído na distribuição do Spring 2.x (ver Secção 5.11).
  • Em [3], crie uma configuração de execução [Run/Run...]
  • Em [4], crie uma configuração de execução Java (existem vários tipos de configurações de execução)
  • Em [5], selecione o separador [Main]
  • Em [6], nomeie a configuração
  • Em [7], nomeie o projeto Eclipse associado a esta configuração (utilize o botão «Browse»)
  • Em [8], nomeie a classe Java que contém o método [main] (utilize o botão «Browse»)
  • Em [9], vá para o separador [Arguments]. Aí, pode especificar dois tipos de argumentos:
    • em [9], aqueles passados para o método [main]
    • em [10], aqueles passados para a JVM que irá executar o código. O agente Spring é definido utilizando o parâmetro da JVM -javaagent:valor. O valor é o caminho para o ficheiro [spring-agent.jar].
  • Em [11]: Guarde a configuração
  • em [12]: a configuração é criada
  • em [13]: executamo-la

Assim que isto estiver feito, o [InitDB] é executado e produz os mesmos resultados que com o Hibernate. Para o [TestNG], proceda da mesma forma:

  • em [1], crie uma configuração de execução [Executar/Executar...]
  • em [2], crie uma configuração de execução do TestNG
  • em [3], selecione o separador [Test]
  • em [4], nomeie a configuração
  • em [5], nomeie o projeto Eclipse associado a esta configuração (use o botão Procurar)
  • Em [6], nomeie a classe de teste (utilize o botão «Browse»)
  • Em [7], vá para o separador [Arguments].
  • Em [8]: Defina o argumento -javaagent para a JVM.
  • Em [9]: Guarde a configuração
  • Em [10]: A configuração foi criada
  • Em [11]: Execute-a

Depois de fazer isto, o [TestNG] é executado e produz os mesmos resultados que com o Hibernate.

3.2. Exemplo 2: E ação do JBoss EJB3/JPA com a entidade Person

Vamos usar o mesmo exemplo de antes, mas executá-lo num contentor EJB3, especificamente o do 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. Iremos descobrir que este fornece serviços semelhantes aos fornecidos pelo Spring. Iremos tentar ver qual destes contentores se revela o mais prático.

A instalação do contentor EJB3 do JBoss é descrita na Secção 5.12.

3.2.1. O projeto Eclipse / JBoss EJB3 / Hibernate

O projeto Eclipse é o seguinte:

  • em [1]: o projeto Eclipse. Pode ser encontrado em [6], nos exemplos do tutorial [5]. Vamos importá-lo.
  • em [2]: o código Java para as camadas apresentadas nos pacotes:
    • [entities]: o pacote de entidades JPA
    • [dao]: a camada de acesso a dados — baseada na camada JPA
    • [service]: uma camada de serviço, em vez de uma camada de negócios. Iremos utilizar o serviço de transações do contentor EJB3.
    • [tests]: contém os programas de teste.
  • em [3]: a biblioteca [jpa-jbossejb3] contém os JARs necessários para o JBoss EJB3 (ver também [7] e [8]).
  • em [4]: a pasta [conf] contém os ficheiros de configuração para cada um dos SGBDs utilizados neste tutorial. Existem dois ficheiros para cada um: [persistence.xml], que configura a camada JPA, e [jboss-config.xml], que configura o contentor EJB3.

3.2.2. Entidades JPA

Existe apenas uma entidade gerida aqui: a entidade Person discutida anteriormente na secção 3.1.2.

3.2.3. A camada [dao]

A camada [DAO] implementa a interface [IDao] descrita anteriormente na secção 3.1.3.

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


package dao;
 
...
@Stateless
public class Dao implements IDao {
 
    @PersistenceContext
    private EntityManager em;
 
    // delete a person via his/her login
    @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);
    }
 
    // get all the people
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }
 
    // get people whose names match a model
    @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();
    }
 
    // find a person via his/her login
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }
 
    // save a person
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }
 
    // update a person
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }
 
}
  • Este código é idêntico em todos os aspetos ao que tínhamos com o Spring. Apenas as anotações Java mudaram, e é isso que estamos a discutir.
  • 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 cujos valores devem ser preservados ao longo do tempo. Um exemplo clássico é uma classe que contém informações relacionadas com o utilizador de uma aplicação web. Uma instância desta classe está associada a um utilizador específico e, quando o segmento de execução do pedido desse utilizador estiver concluído, a instância deve ser mantida para que esteja disponível para o próximo pedido do mesmo cliente. Um EJB @Stateless não tem estado. Utilizando o mesmo exemplo, no final da thread de execução da solicitação de um utilizador, o EJB @Stateless junta-se a um conjunto de EJBs @Stateless e fica disponível para a thread de execução da solicitação de outro utilizador.
  • Para o programador, o conceito de um EJB3 @Stateless é semelhante ao de um singleton do Spring. É utilizado nos mesmos cenários.
  • Linha 7: A anotação @PersistenceContext é a mesma que se encontra na versão Spring da camada [DAO]. Ela designa o campo que irá conter o EntityManager, o que permitirá à camada [DAO] manipular o contexto de persistência.
  • Linha 11: A anotação @TransactionAttribute aplicada a um método é utilizada 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 dentro de uma transação. Se uma transação já tiver sido iniciada, as operações de persistência do método ocorrem dentro dela. Caso contrário, é criada e iniciada uma transação.
    • TransactionAttributeType.REQUIRES_NEW: O método deve ser executado dentro de uma nova transação. Esta transação é criada e iniciada.
    • TransactionAttributeType.MANDATORY: O método deve ser executado dentro de uma transação existente. Se tal transação não existir, é lançada uma exceção.
    • TransactionAttributeType.NEVER: O método nunca é executado dentro de uma 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 [negócio/serviço]

A camada [serviço] implementa a interface [IService] discutida anteriormente na Secção 3.1.4. A implementação [Serviço] da interface [IService] é idêntica à implementação discutida anteriormente na Secção 3.1.4, com três exceções:


 
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {
 
    // layer [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 dentro de uma transação
  • linhas 7–8: uma referência ao EJB na camada [dao] será injetada pelo contentor EJB no campo [IDao dao] na linha 8. A anotação @EJB na linha 7 solicita esta injeção. O objeto injetado deve ser um EJB. Esta é uma diferença fundamental 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] é gerida pelos seguintes ficheiros:

  • [META-INF/persistence.xml] configura a camada JPA
  • [jboss-config.xml] configura o contentor EJB3. Utiliza os ficheiros [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Estes ficheiros estão incluídos no JBoss EJB3 e fornecem uma configuração padrão que normalmente não é alterada. O programador deve preocupar-se apenas com o ficheiro [jboss-config.xml]

Vamos examinar 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">
 
        <!-- the JPA provider is Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
        <!-- the DataSource JTA managed by the Java EE5 environment -->
        <jta-data-source>java:/datasource</jta-data-source>
 
        <properties>
            <!-- search for JBA layer entities -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
 
            <!-- logs SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
 
            <!-- the type of SGBD managed -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
 
            <!-- recreate all tables (drop+create) when the persistence unit is deployed -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
 
        </properties>
    </persistence-unit>
 
</persistence>

Este ficheiro é semelhante aos que já vimos no nosso estudo sobre entidades JPA. Ele configura uma camada Hibernate JPA. As novas funcionalidades são as seguintes:

  • linha 5: a unidade de persistência JPA não possui o atributo transaction-type que sempre tivemos até agora:

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

Se nenhum valor for especificado, o atributo transaction-type assume por padrão o valor "JTA" (para Java Transaction API), indicando que o gestor de transações é fornecido por um contentor EJB 3. 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, pode abrir a transação t1 na ligação c1 no BD 1, a transação t2 na ligação c2 no BD 2 e tratar (t1,t2) como uma única transação na qual ou todas as operações são bem-sucedidas (confirmação) ou nenhuma o é (reversão).

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

  • Linha 11: Declara a fonte de dados que o gestor JTA deve utilizar. Esta é especificada como 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">
 
    <!-- factory of the DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- name JNDI of DataSource -->
        <property name="jndiName">java:/datasource</property>
 
        <!-- managed database -->
        <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>
 
        <!-- properties connection pool -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>
 
        <!-- transaction manager, here JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- hibernate cache manager -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- properties instantiation JNDI ? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>
 
    <!-- the DataSource is requested from a factory -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>
 
</deployment>
  • Linha 3: A tag raiz do ficheiro é <deployment>. Este ficheiro de implementação destina-se principalmente a configurar a fonte de dados java:/datasource que foi declarada no ficheiro persistence.xml.
  • A fonte de dados é definida pelo bean "datasource" na linha 38. Podemos ver que a fonte de dados é obtida (linha 40) a partir de uma "fábrica" definida pelo bean "datasourceFactory" na linha 7. Para obter a fonte de dados da aplicação, o cliente deve chamar o método [getDatasource] da fábrica (linha 39).
  • Linha 7: A fábrica que fornece a fonte de dados é uma classe JBoss.
  • Linha 9: O nome JNDI da fonte de dados. Este deve ser o mesmo nome que o declarado na tag <jta-data-source> no ficheiro persistence.xml. Na verdade, a camada JPA utilizará este nome JNDI para solicitar a fonte de dados.
  • Linhas 12–15: algo mais padrão: as propriedades JDBC para a ligação ao SGBD
  • Linhas 18–21: Configuração do pool de conexões interno do contêiner 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 causará problemas quando quisermos migrar para o TopLink.
  • Linhas 32–34: configuração do serviço JNDI.

Terminámos o ficheiro de configuração JBoss EJB3. É complexo e muitos aspetos permanecem pouco claros. Foi retirado de [ref1]. No entanto, poderemos adaptá-lo 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 agora começar a escrever o primeiro cliente para a arquitetura descrita acima:

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


package tests;
 
...
public class InitDB {
 
    // service layer
    private static IService service;
 
    // manufacturer
    public static void main(String[] args) throws ParseException, NamingException {
        // start the EJB3 JBoss container
        // configuration files ejb3-interceptors-aop.xml and embedded-jboss-beans.xml are used
        EJB3StandaloneBootstrap.boot(null);
 
        // Creating application-specific beans
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
 
        // Deploy all EJBs found on classpath (slow, scans all)
        // EJB3StandaloneBootstrap.scanClasspath();
 
        // deploy all EJB found in the application classpath
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // The JNDI context is initialized. The jndi.properties file is used
        InitialContext initialContext = new InitialContext();
 
        // service layer instantiation
        service = (IService) initialContext.lookup("Service/local");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
        // we stop the Ejb container
        EJB3StandaloneBootstrap.shutdown();
 
    }
 
    // table content display
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }
 
    // table filling
    public static void fill() throws ParseException {
        // creating people
        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);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }
 
    // deleting table items
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • O método para iniciar o contentor JBoss EJB3 foi encontrado 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 Hibernate e o serviço JNDI são configurados.
  • Linha 22: O contentor recebe instruções para analisar a pasta bin do projeto Eclipse para localizar os EJBs. Os EJBs das camadas [service] e [dao] serão encontrados e geridos pelo contentor.
  • Linha 25: É inicializado um contexto JNDI. Iremos utilizá-lo para localizar os EJBs.
  • Linha 28: O EJB correspondente à classe [Service] na camada [service] é solicitado ao serviço JNDI. Um EJB pode ser acedido localmente ou através da rede. Aqui, o nome «Service/local» do EJB que está a ser procurado refere-se à classe [Service] na camada [service] para 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 no código [InitDB] da versão Spring. Encontramos, portanto, o mesmo código em ambas as versões.

public class InitDB {
 
    // service layer
    private static IService service;
 
    // manufacturer
    public static void main(String[] args) throws ParseException {
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
    }
...
  • linha 36 (JBoss EJB3): parar 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]

Os leitores são encorajados a analisar estes registos. Eles contêm informações interessantes sobre o que o contentor EJB3 faz.

3.2.7. Testes unitários [TestNG]

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


package tests;
 
...
public class TestNG {
 
    // service layer
    private IService service = null;
 
    @BeforeClass
    public void init() throws NamingException, ParseException {
        // log
        log("init");
        // start the EJB3 JBoss container
        // configuration files ejb3-interceptors-aop.xml and embedded-jboss-beans.xml are used
        EJB3StandaloneBootstrap.boot(null);
 
        // Creating application-specific beans
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
 
        // Deploy all EJBs found on classpath (slow, scans all)
        // EJB3StandaloneBootstrap.scanClasspath();
 
        // deploy all EJB found in the application classpath
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
 
        // The JNDI context is initialized. The jndi.properties file is used
        InitialContext initialContext = new InitialContext();
 
        // service layer instantiation
        service = (IService) initialContext.lookup("Service/local");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
    }
 
    @AfterClass
    public void terminate() {
        // log
        log("terminate");
        // Shutdown EJB container
        EJB3StandaloneBootstrap.shutdown();
    }
 
    @BeforeMethod
    public void setUp() throws ParseException {
...
    }
 
...
}
  • O método init (linhas 10–37), que configura o ambiente necessário para os testes, utiliza o código explicado anteriormente em [InitDB].
  • O método terminate (linhas 40–45), que é executado no final dos testes (devido à anotação @AfterClass), encerra o contentor EJB3 (linha 44).
  • Tudo o resto é idêntico ao que era na versão Spring.

Os testes são bem-sucedidos:

Image

3.2.8. Alteração do DBMS

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

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">
 
        <!-- the JPA provider is Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
        <!-- the DataSource JTA managed by the Java EE5 environment -->
        <jta-data-source>java:/datasource</jta-data-source>
 
        <properties>
            <!-- search for JBA layer entities -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
 
            <!-- logs SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
 
            <!-- the type of SGBD managed -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
 
            <!-- recreate all tables (drop+create) when the persistence unit is deployed -->
            <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 SQL Server é 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">
 
    <!-- factory of the DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- name JNDI of DataSource -->
        <property name="jndiName">java:/datasource</property>
 
        <!-- managed database -->
        <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>
 
        <!-- properties connection pool -->
    ...
    </bean>
 
</deployment>

Apenas as linhas 12–15 foram alteradas: elas especificam as características da nova ligação JDBC.

Os leitores são encorajados a repetir os testes descritos para o MySQL5 com outros SGBDs.

3.2.9. Alteração da implementação JPA

Como mencionado acima, não encontrámos quaisquer exemplos de utilização do contentor JBoss EJB3 com o TopLink. À data da redação deste artigo (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 «Pessoa». Criámos três arquiteturas para executar 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 utilizam estas três arquiteturas, juntamente com outras entidades abordadas 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 - Endereço - 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 quaisquer novos conceitos arquitetónicos. Aplicam-se simplesmente a um cenário em que há várias entidades para gerir, com relações um-para-muitos ou muitos-para-muitos entre elas — algo que os exemplos que utilizavam a entidade Person não tinham.

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

3.4.1. Visão geral

Aqui, revisitamos 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 conceitos básicos do desenvolvimento Web MVC em Java. Para compreender o exemplo a seguir, o leitor deve estar familiarizado com esses conceitos básicos. A aplicação Web utilizará o servidor Tomcat. A sua instalação e utilização no Eclipse são descritas na Secção 5.3.

A aplicação foi originalmente desenvolvida com uma camada [DAO] baseada na ferramenta iBatis/SQLMap [http://ibatis.apache.org/], que tratava do mapeamento relacional-para-objeto. Iremos simplesmente substituir o iBatis pelo JPA. A arquitetura da aplicação será a seguinte:

A aplicação web que vamos escrever permitirá-nos gerir um grupo de pessoas utilizando quatro operações:

  • lista de pessoas do grupo
  • adicionar uma pessoa ao grupo
  • modificar uma pessoa no grupo
  • remover uma pessoa do grupo

Estas quatro operações básicas são comuns numa tabela de base de dados. As seguintes e as de ecrã mostram as páginas que a aplicação apresenta ao utilizador.

 

3.4.2. O Projeto Eclipse

O projeto Eclipse para a aplicação é o seguinte:

  • em [1]: o projeto web. Trata-se de um projeto Eclipse do tipo [Dynamic Web Project] [2]. Pode ser encontrado em [4], na pasta [3] dos exemplos do tutorial. Vamos importá-lo.
  • em [5]: as fontes e a configuração das camadas [service, dao, jpa]. Mantemos os componentes [dao, entities, service] existentes do projeto Eclipse [hibernate-spring-people-business-dao] discutido na secção 3.1.1. Estamos apenas a desenvolver a camada [web], aqui representada pelo pacote [web]. Além disso, mantemos os ficheiros de configuração [persistence.xml, spring-config.xml] desse projeto, com a exceção de que utilizaremos o SGBD Postgres, o que resulta nas seguintes alterações em [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>
 
    <!-- data source 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 para o Postgres.

  • Em [6]: a pasta [WebContent] contém as páginas JSP do projeto, bem como as bibliotecas necessárias. Estas estão listadas em [8]
  • A aplicação pode ser utilizada com vários SGBDs. Basta modificar o ficheiro [spring-config.xml]. A pasta [conf] [7] contém o ficheiro [spring-config.xml] adaptado para vários SGBDs.

3.4.3. A camada [web]

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

A camada [web] fornecerá ecrãs ao utilizador para que este possa gerir o grupo de pessoas:

  • lista de pessoas no grupo
  • adicionar uma pessoa ao grupo
  • editar uma pessoa no grupo
  • remover uma pessoa do grupo

Para tal, irá recorrer à camada [service], que, por sua vez, irá invocar a camada [DAO]. Já apresentámos os ecrãs geridos pela camada [web] (secção 3.4.1). Para descrever a camada web, iremos apresentar o seguinte, por ordem:

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

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

Vamos dar uma olhada na arquitetura do projeto Eclipse:

 
  • No pacote [web], encontramos o controlador da aplicação web: a classe [Application].
  • As páginas JSP/JSTL da aplicação encontram-se em [WEB-INF/views].
  • A pasta [WEB-INF/lib] contém as bibliotecas de terceiros necessárias para a aplicação. Estas 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>
    <!--  Mapping ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  welcome files -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Unexpected error page -->
    <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 tratadas pelo servlet [people]
  • 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, urlErrors] que identificam as URLs das páginas JSP para as vistas [list, edit, errors].
  • linhas 28-30: a aplicação tem uma página de entrada predefinida [index.jsp] localizada na raiz da pasta da aplicação web.
  • linhas 32–35: A aplicação tem uma página de erro predefinida que é apresentada quando o servidor web encontra uma exceção não tratada pela aplicação.
    • Linha 37: a tag <exception-type> especifica o tipo de exceção tratada pela diretiva <error-page>; aqui, é o tipo [java.lang.Exception] e os seus subtipos, ou seja, todas as exceções.
    • Linha 38: A tag <location> especifica a página JSP a ser exibida quando ocorre uma exceção do tipo definido por <exception-type>. A exceção que ocorreu está disponível nesta página num objeto chamado exception, se a página tiver a diretiva:

<%@ page isErrorPage="true" %>
  • (continuação)
    • Se <exception-type> especificar o tipo T1 e uma exceção do tipo T2 (não derivada de T1) for propagada até ao servidor web, o servidor envia ao cliente uma página de exceção proprietária, que geralmente não é muito intuitiva. Daí a importância da tag <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, ou seja, 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 o URL [/do/list]. Este URL apresenta a lista de pessoas do grupo.

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


A vista [ list.jsp]


É utilizado 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 [people] associado a uma [List] de objetos [Person]: uma lista de pessoas.
    • o elemento opcional [errors] associado a uma [List] de objetos [String]: uma lista de mensagens de erro.
  • Linhas 31–43: Iteramos pela lista ${people} para apresentar uma tabela HTML contendo as pessoas do grupo.
  • linha 40: o URL apontado pelo link [Edit] é definido utilizando o campo [id] da pessoa atual, para que o controlador associado ao URL [/do/edit] saiba qual a pessoa a editar.
  • linha 41: o mesmo é feito para o link [Delete].
  • linha 37: Para exibir a data de nascimento da pessoa no formato DD/MM/AAAA, usamos a tag <dt> da biblioteca de tags [DateTime] do projeto Apache [Jakarta Taglibs]:

Image

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

  • Linha 46: O link [Adicionar] para adicionar uma nova pessoa aponta para a URL [/do/edit], tal como o link [Editar] na linha 40. O valor -1 para o parâmetro [id] indica que se trata de uma adição e não de uma edição.
  • Linhas 10–18: Se o elemento ${errors} estiver presente no modelo, as mensagens de erro nele contidas são apresentadas.

A página [ edit.jsp]


É utilizada para apresentar o formulário para adicionar uma nova pessoa ou modificar uma já existente:

O código para a 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 já existente. A partir de agora, para simplificar o texto, utilizaremos o termo único [atualizar]. O botão [Submit] (linha 73) aciona um pedido POST para o URL [/do/validate] (linha 16). Se o POST falhar, a vista [edit.jsp] é exibida novamente com o(s) erro(s) que ocorreu(ram); caso contrário, a vista [list.jsp] é exibida.

  • A vista [edit.jsp], que é exibida tanto numa solicitação GET como numa solicitação POST com falha, recebe os seguintes elementos no seu modelo:
atributo
GET
POST
id
ID da pessoa que está a ser
atualizada
igual
versão
sua versão
o mesmo
nome próprio
nome
Nome introduzido
apelido
o apelido dele/dela
Apelido introduzido
data de nascimento
data de nascimento dele
data de nascimento introduzida
casado
estado civil
introduziu o estado civil
número de filhos
número de filhos
número de filhos inserido
erroEditar
vazio
uma mensagem de erro indicando que a adição
ou modificação no momento do POST
pelo botão [Submit]. Vazio se não houver erro.
errorFirstName
vazio
indica um nome próprio incorreto – vazio caso contrário
lastNameError
vazio
indica um apelido incorreto – vazio caso contrário
erro na data de nascimento
vazio
indica uma data de nascimento incorreta – vazio caso contrário
errorNumberOfChildren
vazio
indica um número incorreto de filhos – vazio caso contrário
  • linhas 11-15: se o envio do formulário falhar, [errorEdit!=''] será devolvido e será exibida uma mensagem de erro.
  • linha 16: o formulário será enviado para a URL [/do/validate]
  • linha 20: o elemento [id] do modelo é exibido
  • linha 24: o elemento [version] do modelo é exibido
  • linhas 26-32: introdução do nome próprio da pessoa:
    • Quando o formulário é exibido inicialmente (GET), ${firstName} exibe o valor atual do campo [firstName] do objeto [Person] atualizado, e ${firstNameError} está vazio.
    • em caso de erro após o POST, o valor introduzido ${firstName} é exibido novamente, juntamente com qualquer mensagem de erro ${firstNameError}
  • linhas 33-39: introdução do apelido da pessoa
  • linhas 40–46: Introdução da data de nascimento da pessoa
  • Linhas 47–61: introdução do estado civil da pessoa utilizando um botão de opção. O valor do campo [married] do objeto [Person] é utilizado para determinar qual dos dois botões de opção deve ser selecionado.
  • linhas 62-68: introduzir o número de filhos da pessoa
  • linha 71: um campo HTML oculto denominado [id] com um valor igual ao campo [id] da pessoa que está a ser atualizada, -1 para uma adição ou outro valor para uma modificação.
  • linha 72: um campo HTML oculto chamado [version] com um valor igual ao campo [id] da pessoa que está a ser atualizada.
  • Linha 73: O botão [Submit] do formulário
  • linha 74: um link para voltar à lista de pessoas. Está rotulado como [Cancelar] porque permite sair do formulário sem o enviar.

A página [ exception.jsp]


Esta página é utilizada para apresentar uma mensagem indicando que ocorreu uma exceção não tratada pela aplicação e que foi propagada para o servidor web.

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

O código para a 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, a página deve ter definido a tag na linha 3.
  • Linha 6: O código de estado HTTP da resposta é definido como 200. Este é o primeiro cabeçalho HTTP da resposta. O código 200 indica ao cliente que o seu pedido foi atendido. Geralmente, um documento HTML foi incluído na resposta do servidor. É esse o caso aqui. Se o código de estado HTTP da resposta não estiver definido como 200, terá aqui o valor 500, o que significa que ocorreu um erro. De facto, quando o servidor web intercepta uma exceção não tratada, considera isso uma situação anormal e sinaliza-a com um código 500. A resposta a um código HTTP 500 varia consoante o navegador: o Firefox apresenta o documento HTML que pode acompanhar esta resposta, enquanto o IE ignora este documento e apresenta a sua própria página. É por isso que substituímos o código 500 pelo código 200.
  • Linha 16: O texto da exceção é exibido
  • Linha 18: É oferecido ao utilizador um link para regressar à lista de pessoas

A visualização [ -errors .jsp]


É utilizada para apresentar uma página que relata erros de inicialização da aplicação, ou seja, erros detetados durante a execução do método [init] do servlet controlador. Isto pode ser, por exemplo, a ausência de um parâmetro no ficheiro [web.xml], conforme ilustrado no exemplo abaixo:

Image

O código da página [errors.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 um elemento [errors] no seu modelo, que é uma [ArrayList] de objetos [String]; estes são mensagens de erro. São apresentadas pelo ciclo nas linhas 13–15.

3.4.3.3. O controlador da aplicação

O controlador [Application] é definido no pacote [web]:

Image


Estrutura tura e inicialização do controlador


A estrutura do controlador [Application] é a seguinte:


package web;
 
...
 
 
@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // instance parameters
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();
 
    // service
    private IService service = null;
 
    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // retrieve servlet initialization parameters
        ServletConfig config = getServletConfig();
        // other initialization parameters are processed
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // parameter value
            valeur = config.getInitParameter(paramètres[i]);
            // present parameter?
            if (valeur == null) {
                // we note the error
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // parameter value is stored
                params.put(paramètres[i], valeur);
            }
        }
        // the [errors] view url has a special treatment
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
        // empty the base
        clean();
        // fill it
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }
 
    // table filling
    public void fill() throws ParseException {
        // creating people
        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);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }
 
    // deleting table items
    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 {
...
    }
 
    // display list of persons
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // modify / add a person
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // deleting a person
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // display pre-filled form
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }
 
    // post
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // we hand over to GET
        doGet(request, response);
    }
 
}
  • linhas 21–34: Recuperamos os parâmetros especificados no ficheiro [web.xml].
  • linhas 37-39: o parâmetro [urlErrors] deve estar presente, pois especifica a URL da vista [errors] capaz de exibir quaisquer erros de inicialização. Se não existir, a aplicação é encerrada através do lançamento de uma [ServletException] (linha 39). Esta exceção será propagada para o servidor web e tratada pela tag <error-page> no ficheiro [web.xml]. A vista [exception.jsp] é, portanto, exibida:

Image

O link [Voltar à lista] acima está inativo. Ao clicar nele, obtém-se a mesma resposta, desde que a aplicação não tenha sido modificada e recarregada. É útil para outros tipos de exceções, como já vimos.

  • Linhas 40–43: utilize 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 têm uma referência [service] à camada [service] (linha 15) que irão utilizar para executar as ações solicitadas pelo utilizador. Estas serão interceptadas pelo método [doGet], que as fará processar por um método específico do controlador:
Url
Método HTTP
Método do controlador
/do/list
GET
doListPeople
/do/edit
GET
doEditPerson
/do/validate
POST
doValidatePerson
/do/delete
GET
doDeletePerson

O método [doGet]


O objetivo deste método é 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 {
 
        
// check how the servlet was initialized
        if (erreursInitialisation.size() != 0) {
            // we hand over to the error page
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // end
            return;
        }
        // retrieve the request sending method
        String méthode = request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action = request.getPathInfo();
        // action?
        if (action == null) {
            action = "/list";
        }
        // execution action
        if (méthode.equals("get") && action.equals("/list")) {
            // list of persons
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // deleting a person
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // presentation form add / modify a person
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // validation form add / modify a person
            doValidatePersonne(request, response);
            return;
        }
        // other cases
        doListPersonnes(request, response);
    }
  • linhas 7–13: Verificamos se a lista de erros de inicialização está vazia. Se não estiver, exibimos a vista [errors(errors)], que irá reportar o(s) erro(s).
  • linha 15: Recuperamos o método [get] ou [post] que o cliente utilizou para efetuar o pedido.
  • linha 17: recuperamos o valor do parâmetro [action] da solicitação.
  • Linhas 23–27: Processamos a solicitação [GET /do/list], que solicita a lista de pessoas.
  • Linhas 28–32: Processamos a solicitação [GET /do/delete], que solicita a exclusão de uma pessoa.
  • Linhas 33–37: Processamos a solicitação [GET /do/edit], que solicita o formulário para atualizar uma pessoa.
  • linhas 38–42: processar a 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, tratamo-la como se fosse [GET /do/list].

O método [doListPersonnes]


Este método trata a solicitação [GET /do/list], que solicita a lista de pessoas:

Image

O seu código é o seguinte:


    // display list of persons
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // the [list] view model
        request.setAttribute("personnes", service.getAll());
        // list] view display
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • Linha 4: Solicitamos a lista de pessoas do grupo à camada [service] e guardamo-la no modelo sob a chave "people".
  • Linha 6: A vista [list.jsp] descrita na secção 3.4.3.2 é apresentada.

O método [doDeletePerson]


Este método trata do pedido [GET /do/delete?id=XX], que solicita a eliminação da pessoa com id=XX. O URL [/do/delete?id=XX] é o dos links [Delete] na 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>

A linha 12 mostra o URL [/do/delete?id=XX] para o link [Eliminar]. O método [doDeletePerson], que trata deste URL, deve eliminar a pessoa com id=XX e, em seguida, apresentar a lista atualizada de pessoas no grupo. O seu código é o seguinte:


// deleting a person
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // we delete the person
        service.deleteOne(id);
        // redirects to the list of persons
        response.sendRedirect("list");
    }
  • Linha 4: A URL que está a ser processada tem o formato [/do/delete?id=XX]. Recuperamos o valor [XX] do parâmetro [id].
  • linha 6: solicitamos à camada [service] que elimine a pessoa com o ID obtido. Não realizamos qualquer validação. Se a pessoa que estamos a tentar eliminar não existir, a camada [dao] lança uma exceção que é propagada até à camada [service]. Também não a tratamos aqui no controlador. Por conseguinte, propagar-se-á até ao servidor web, que, por configuração, exibirá a página [exception.jsp], descrita na secção 3.4.3.2:

Image

  • Linha 9: Se a eliminação foi bem-sucedida (sem exceção), o cliente é redirecionado para o URL relativo [list]. Uma vez que o URL acabado de processar foi [/do/delete], o URL de redirecionamento será [/do/list]. O navegador irá, portanto, efetuar um pedido [GET /do/list], que irá apresentar a lista de pessoas.

O método [doEditPerson]


Este método trata a solicitação [GET /do/edit?id=XX], que recupera o formulário para atualizar a pessoa com id=XX. A URL [/do/edit?id=XX] é utilizada para os links [Editar] e [Adicionar] na 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 o URL [/do/edit?id=XX] para o link [Editar] e, na linha 17, o URL [/do/edit?id=-1] para o link [Adicionar]. O método [doEditPersonne] deve apresentar o formulário de edição para a pessoa com id=XX ou, se for uma adição, apresentar um formulário vazio.

  • Em [1] acima, o formulário de adição, e em [2], o formulário de edição.

O código para o método [doEditPerson] é o seguinte:


// modify / add a person
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // addition or modification?
        Personne personne = null;
        if (id != -1) {
            // modification - the person to be modified is retrieved
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // add - create an empty person
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // we put the [Person] object in the user's session
        request.getSession().setAttribute("personne", personne);
        // and in the view model [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());
        // view display [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • A solicitação GET tem como destino uma URL no formato [/do/edit?id=XX]. Na linha 4, recuperamos o valor de [id]. Existem então dois casos:
    1. Se id não for igual a -1, trata-se de uma atualização, e precisamos de apresentar um formulário pré-preenchido com as informações da pessoa a ser atualizada. Na linha 9, esta pessoa é solicitada à camada [service].
    2. id é igual a -1. Neste caso, trata-se de uma adição e deve ser apresentado um formulário vazio. Para tal, é criada uma pessoa vazia na linha 14.
    3. Em ambos os casos, os elementos [id, version] do modelo de página [edit.jsp] descrito na secção 3.4.3.2 são inicializados.
  • O objeto [Person] resultante é colocado no modelo de página [edit.jsp]. Este modelo inclui os seguintes elementos: [errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, birthDate, errorBirthDate, married, numberOfChildren, errorNumberOfChildren]. Estes elementos são inicializados nas linhas 19–31, com exceção daqueles cujo valor é a cadeia vazia [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Sabemos que, se estiverem ausentes do modelo, a biblioteca JSTL exibirá uma string vazia como seu valor. Embora o elemento [errorEdit] também tenha uma string vazia como valor, ele é, no entanto, inicializado porque é realizada uma verificação do seu valor na página [edit.jsp].
  • Assim que o modelo estiver pronto, o controlo é passado para a página [edit.jsp], linha 33, que irá gerar a vista [edit].

O método [doValidatePersonne]


Este método trata do pedido [POST /do/validate], que valida o formulário de atualização. Este POST é acionado pelo botão [Validate]:

Image

Vamos rever os elementos de entrada do formulário HTML na 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>

O pedido POST contém os parâmetros [first_name, last_name, date_of_birth, married_to, number_of_children, id] e é enviado para o URL [/do/validate] (linha 1). É processado pelo seguinte método [doValidatePerson]:


// validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // retrieve posted items
        boolean formulaireErroné = false;
        boolean erreur;
        // first name
        String prenom = request.getParameter("prenom").trim();
        // valid first name?
        if (prenom.length() == 0) {
            // we note the error
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // the name
        String nom = request.getParameter("nom").trim();
        // valid first name?
        if (nom.length() == 0) {
            // we note the error
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // date of birth
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // we note the error
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // marital status
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // number of children
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // we note the error
            erreur = true;
        }
        // wrong number of children?
        if (erreur) {
            // we report the error
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // pERSON ID
        int id = Integer.parseInt(request.getParameter("id"));
        // is the form incorrect?
        if (formulaireErroné) {
            // redisplay the form with error messages
            showFormulaire(request, response, "");
            // finish
            return;
        }
        // the form is correct - we update the person who has been placed in the session
        // with information sent by the customer
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // persistence
        try {
            if (id == -1) {
                // creation
                service.saveOne(personne);
            } else {
                // update
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // redisplay the form with the error message
            showFormulaire(request, response, ex.getMessage());
            // finish
            return;
        }
        // redirects to the list of persons
        response.sendRedirect("list");
    }
 
    // display pre-filled form
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // prepare the view model [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());
        // view display [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • linhas 7-13: O parâmetro [firstName] da solicitação POST é recuperado e a sua validade é verificada. Se estiver incorreto, o elemento [firstNameError] é inicializado com uma mensagem de erro e colocado nos atributos da solicitação.
  • linhas 15–21: O mesmo processo é seguido para o parâmetro [lastName]
  • linhas 23–30: O mesmo processo é aplicado ao parâmetro [dateOfBirth]
  • Linha 32: Recuperamos o parâmetro [marie]. Não verificamos a sua validade porque, em princípio, provém do valor de um botão de opção. Dito isto, nada impede que um programa faça uma solicitação [POST /.../do/validate] acompanhada de um parâmetro [marie] fictício. Devemos, portanto, testar a validade deste parâmetro. Aqui, contamos com o nosso tratamento de exceções, que faz com que a página [exception.jsp] seja exibida se o controlador não tratar a exceção por si próprio. Portanto, se a conversão do parâmetro [marie] para um booleano falhar na linha 32, será lançada uma exceção, resultando no envio da página [exception.jsp] para o cliente. Este comportamento funciona para nós.
  • Linhas 34–50: Recuperamos o parâmetro [nbenfants] e verificamos o seu valor.
  • Linha 52: Recuperamos o parâmetro [id] sem verificar o seu valor
  • Linhas 54–59: Se o formulário for inválido, ele é exibido novamente com as mensagens de erro geradas anteriormente
  • Linhas 62–67: Se for válido, criamos um novo objeto [Person] utilizando os campos do formulário
  • linhas 69–82: a pessoa é guardada. A operação de gravação pode falhar. Num ambiente multiutilizador, a pessoa a ser modificada pode ter sido eliminada ou já ter sido modificada por outra pessoa. Neste caso, a camada [dao] lançará uma exceção, que tratamos aqui.
  • linha 84: se não ocorreu nenhuma exceção, o cliente é redirecionado para o URL [/do/list] para exibir o novo estado do grupo.
  • Linha 79: Se ocorreu uma exceção durante o salvamento, solicitamos que o formulário inicial seja exibido novamente, 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] utilizando os valores introduzidos (request.getParameter(" ... ")). Recorde-se que as mensagens de erro já foram colocadas no modelo pelo método [doValidatePersonne]. A página [edit.jsp] é apresentada na linha 99.

3.4.4. Testar a aplicação Web

Foram apresentados vários testes na Secção 3.4.1. Convidamos o leitor a executá-los novamente. Aqui mostramos capturas de ecrã adicionais que ilustram casos de conflitos de acesso a dados num ambiente multiutilizador:

O [Firefox] será o navegador do utilizador U1. O utilizador U1 solicita o URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

Image

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

Image

O utilizador U1 começa a editar o registo da pessoa [p2]:

Image

O utilizador U2 faz o mesmo:

Image

O utilizador U1 faz alterações e envia:

O utilizador U2 faz o mesmo:

O utilizador U2 volta à lista de pessoas utilizando o link [Voltar à lista] no formulário:

Image

Encontra a pessoa [Lemarchand] tal como foi modificada por U1 (casado, 2 filhos). O número de versão de p2 foi alterado. Agora, U2 elimina [p2]:

O U1 ainda tem a sua própria lista e quer editar [p2] novamente:

O U1 usa o link [Voltar à lista] para ver o que se passa:

Image

Ele descobre que [p2] já não faz parte da lista...

3.4.5. Versão 2

Modificamos ligeiramente a versão anterior para utilizar os arquivos das camadas [service, dao, jpa] em vez do seu código-fonte:

  • em [1]: o novo projeto Eclipse. Note que os pacotes [service, dao, entities] desapareceram. Estes foram encapsulados no arquivo [service-dao-jpa-personne.jar] [2] localizado 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 solicitamos a lista de pessoas, recebemos a seguinte resposta:

 

O Hibernate não consegue encontrar a entidade [Person]. Para resolver este problema, temos de declarar explicitamente as entidades geridas no ficheiro [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">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • Linha 7: A entidade Pessoa é declarada.

Depois de fazer isto, a exceção desaparece:

 

3.4.6. Alterar a implementação do JPA

  • em [1]: o novo projeto Eclipse
  • em [2]: as bibliotecas TopLink substituíram as bibliotecas Hibernate
  • A pasta do projeto encontra-se em [4]. Vamos importá-la.

Alterar a implementação do JPA envolve apenas algumas alterações no ficheiro [spring-config.xml]. Nada mais muda. As alterações feitas no ficheiro [spring-config.xml] foram explicadas na secção 3.1.9:


<?xml version="1.0" encoding="UTF-8"?>
 
<!-- the JVM must be launched with the -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar argument 
    (à 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>

Basta alterar algumas linhas para mudar do Hibernate para o Toplink:

  • linha 11: a implementação JPA é agora gerida 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 este nome foi explicado na secção 2.1.15.2.

É tudo. Repare como é fácil mudar de SGBD ou de implementações JPA com o Spring. No entanto, ainda não terminámos. Ao executar a aplicação, ocorre uma exceção:

 

Este é o mesmo problema encontrado e descrito na secção 3.1.9. Resolve-se iniciando a JVM com um agente Spring. Para tal, modifique a configuração de arranque do Tomcat:

  • em [1]: selecionámos a opção [Executar / Executar...] para modificar a configuração do Tomcat
  • em [2]: selecionámos o separador [Argumentos]
  • em [3]: adicionámos o parâmetro -javaagent, conforme descrito na secção 3.1.9.

Depois de fazer isso, podemos 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 discutido na Secção 3.2:

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

Reutilizámos a configuração [jboss-config.xml, persistence.xml] descrita na Secção 3.2 e, em seguida, modificámos o método [init] do controlador [Application.java] da seguinte forma:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // retrieve servlet initialization parameters
            ServletConfig config = getServletConfig();
            // other initialization parameters are processed
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // parameter value
                valeur = config.getInitParameter(paramètres[i]);
                // present parameter?
                if (valeur == null) {
                    // we note the error
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // parameter value is stored
                    params.put(paramètres[i], valeur);
                }
            }
            // the [errors] view url has a special treatment
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // application configuration
            // start the EJB3 JBoss container
            // configuration files ejb3-interceptors-aop.xml and embedded-jboss-beans.xml are used
            EJB3StandaloneBootstrap.boot(null);
 
            // Creating application-specific beans
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
 
            // deploy all EJB found in the application classpath
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();
 
            // The JNDI context is initialized. The jndi.properties file is used
            InitialContext initialContext = new InitialContext();
 
            // service layer instantiation
            service = (IService) initialContext.lookup("Service/local");
            // empty the base
            clean();
            // fill it
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • Linhas 28–38: O contentor EJB3 é iniciado. Isto substitui o contentor Spring.
  • Linha 41: Solicitamos uma referência à camada [service] da aplicação.

À primeira vista, estas são as únicas alterações necessárias. Após a execução, ocorre o seguinte erro:

 

Não consegui determinar exatamente onde residia o problema. A exceção relatada pelo Tomcat parece indicar que o objeto denominado «TransactionManager» foi solicitado ao serviço JNDI, que 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á incorporada neste documento.