Skip to content

2. Artigo 1 - Spring IoC

Objetivos do documento:

  • descobrir as possibilidades de configuração e integração do framework Spring (http://www.springframework.org)
  • definir e utilizar o conceito de IoC (Inversão de Controlo), também designado por injeção de dependências (Dependency Injection)

2.1. Configurar uma aplicação de 3 camadas com o Spring

Consideremos uma aplicação clássica de 3 camadas:

Partiremos do princípio de que o acesso às camadas de negócio e DAO é controlado por interfaces Java:

  1. a interface [IArticlesDao] para a camada de acesso aos dados
  1. a interface [IArticlesManager] para a camada de negócios

Na camada de acesso aos dados, ou camada DAO (Data Access Object), é frequente trabalhar com um SGBD e, por conseguinte, com um controlador JDBC. Consideremos o esboço de uma classe que acede a uma tabela de artigos num SGBD:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // ligação à fonte de dados
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // solicita-se a lista de artigos
        try {
             // carregar o controlador JDBC
            Class.forName(driverClassName);
            // criação de uma ligação à BD
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

Para realizar uma operação no SGBD, qualquer método necessita de um objeto [Connection] que represente a ligação à base de dados, através da qual transitarão as trocas de dados entre esta e o código Java. Para construir este objeto, são necessárias quatro informações:

String driverClassName
o nome da classe do controlador JDBC do SGBD
String url
a URL JDBC da base de dados a utilizar
String user
a identidade com a qual se estabelece a ligação
String pwd
a palavra-passe dessa identidade

Como é que a nossa classe anterior [ArticlesDaoPlainJdbc] pode obter estas informações? Existem várias possibilidades:

solução 1 — as informações estão codificadas de forma estática na classe:

1
2
3
4
5
6
7
8
public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // ligação à fonte de dados
    private final String driverClassName = "org.firebirdsql.jdbc.FBDriver";
    private String url = "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb";
    private String user = "someone";
    private String pwd = "somepassword";
 ....

A desvantagem desta solução é que é necessário alterar o código Java sempre que se verifique uma alteração nestas informações, por exemplo, a alteração da palavra-passe.

Solução 2 — as informações são transmitidas ao objeto durante a sua criação:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // ligação à fonte de dados
    private final String driverClassName;
    private String url;
    private String user;
    private String pwd;
 ....
    public ArticlesDaoPlainJdbc(String driverClassName,String url,String user,String pwd) {
      this.driverClassName=driverClassName;
    this.url=url;
    this.user=user;
    this.pwd=pwd;
    ...
    }

Aqui, o objeto recebe, aquando da sua construção, as informações de que necessita para funcionar. O problema passa então para o código que lhe transmitiu as quatro informações. Como é que as obteve? A seguinte classe [ArticlesManagerWithDataBase] da camada de negócio poderia construir um objeto [ArticlesDaoPlainJdbc] da camada de acesso aos dados:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // uma instância de acesso aos dados
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // criação do serviço de acesso aos dados
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

Vemos que, mais uma vez, as informações necessárias para a construção do objeto [ArticlesDaoPlainJdbc] são fornecidas ao construtor do objeto [ArticlesManagerWithDataBase]. Podemos imaginar que estas lhe são transmitidas por uma camada superior, como a camada de interface com o utilizador. Chega-se assim, passo a passo, à camada mais elevada da aplicação. Devido à sua posição, esta não é chamada por uma camada que lhe possa transmitir as informações de configuração de que necessita. É, portanto, necessário encontrar uma solução alternativa à configuração por construtor. A solução habitual para configurar uma aplicação ao nível da sua camada mais elevada consiste na utilização de um ficheiro onde se encontram todas as informações suscetíveis de sofrer alterações ao longo do tempo. Podem existir vários ficheiros deste tipo. No arranque da aplicação, uma camada de inicialização irá então criar a totalidade ou parte dos objetos necessários às diferentes camadas da aplicação.

Existe uma grande variedade de ficheiros de configuração. A tendência atual é utilizar ficheiros XML. Esta é a opção adotada pelo Spring. O ficheiro que configura um objeto [ArticlesDaoPlainJdbc] poderia ser o seguinte:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- a classe de acesso aos dados -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
</beans>

Uma aplicação é um conjunto de objetos que o Spring denomina «beans», porque seguem a norma JavaBean de nomenclatura dos acessadores e inicializadores (getters/setters) dos campos privados de um objeto. Os objetos que, numa aplicação, têm como função prestar um serviço são frequentemente criados numa única instância. Chamam-se singletons. Assim, no nosso exemplo de aplicação multicamadas aqui analisado, o acesso à base de dados de artigos será assegurado por uma única instância da classe [ArticlesDaoPlainJdbc]. Numa aplicação web, estes objetos de serviço atendem vários clientes ao mesmo tempo. Não se cria um objeto de serviço por cliente.

O ficheiro de configuração do Spring acima permite criar um único objeto de serviço do tipo [ArticlesDaoPlainJdbc] num pacote denominado [istia.st.articles.dao]. As quatro informações necessárias ao construtor deste objeto estão definidas dentro de uma baliza <bean>...</bean>. Haverá tantas balizas <bean> quantos os singletons a construir.

Em que momento ocorre a criação dos objetos definidos no ficheiro Spring? A inicialização de uma aplicação pode ser atribuída ao método main dessa mesma aplicação, caso esta o possua. No caso de uma aplicação web, pode ser o método [init] do servlet principal. Em qualquer aplicação, existe um método que é garantidamente o primeiro a ser executado. É geralmente nesse método que ocorre a criação dos singletons.

Vejamos um exemplo. Suponhamos que se pretenda testar a classe [ArticlesDaoPlainJdbc] acima referida, utilizando um teste JUnit. Uma classe de teste JUnit possui um método [setUp] que é executado antes de qualquer outro método. É aí que se criará o singleton [ArticlesDaoPlainJdbc].

Se seguirmos a solução de passar as informações de configuração através do construtor, teremos a seguinte classe de teste:

public class TestArticlesPlainJdbc extends TestCase {
    // testa a classe de acesso aos artigos ArticlesDaoPlainJdbc
     // a fonte de dados está definida no sprintest

     // uma instância da classe testada
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // recupera uma instância de acesso aos dados
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

A classe de chamada [TestArticlesPlainJdbc] deve conhecer as quatro informações necessárias para a inicialização do singleton [ArticlesDaoPlainJdbc] a ser construído.

Se seguirmos a solução de passagem das informações de configuração por ficheiro de configuração, poderíamos ter a seguinte classe de teste utilizando o ficheiro Spring descrito acima.

public class TestSpringArticlesPlainJdbc extends TestCase {
    // testa a classe de acesso aos artigos ArticlesDaoJdbc
     // a fonte de dados está definida no sprintest

     // uma instância da classe testada
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // recupera uma instância de acesso aos dados
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

Aqui, a classe de chamada [TestSpringArticlesPlainJdbc] não precisa de conhecer as informações necessárias para a inicialização do singleton a ser criado. Precisa apenas de conhecer:

  1. [springArticlesPlainJdbc.xml]: o nome do ficheiro de configuração Spring descrito acima
  2. [articlesDao]: o nome do singleton a criar

Uma alteração no ficheiro de configuração, fora destas duas entidades, não tem qualquer impacto no código Java. Este método de configuração dos objetos de uma aplicação é muito flexível. Para se configurar, a aplicação só precisa de saber duas coisas:

  • o nome do ficheiro Spring que contém a definição dos singletons a construir
  • os nomes desses singletons, que servem ao código Java para obter uma referência aos objetos aos quais foram associados através do ficheiro de configuração

2.2. Injeção de dependências e inversão de controlo

Vamos agora introduzir o conceito de injeção de dependências (Dependency Injection) utilizado pelo Spring para configurar as aplicações. Também se utiliza o termo inversão de controlo (IoC, Inversion of Control). Consideremos a criação do singleton [ArticlesManagerWithDataBase] da camada de negócio da nossa aplicação:

Para aceder aos dados do SGBD, a camada de negócio deve utilizar os serviços de um objeto que implemente a interface [IArticlesDao], por exemplo, um objeto do tipo [ArticlesDaoPlainJdbc]. O código da classe [ArticlesManagerWithDataBase] poderia ser semelhante ao seguinte:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // uma instância de acesso aos dados
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // criação do serviço de acesso aos dados
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

A classe [ArticlesDaoPlainJdbc] deve, neste caso, implementar uma interface [IArticlesDao]:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

Para criar o singleton do tipo [IArticlesDao] necessário ao funcionamento da classe, o construtor desta utiliza explicitamente o nome da classe de implementação da interface [IArticlesDao]:

articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);

Temos, portanto, uma dependência rígida no código em relação ao nome da classe. Se a classe de implementação da interface [IArticlesDao] viesse a mudar, o código do construtor anterior teria de ser alterado. Temos as seguintes relações entre os objetos:

A classe [ArticlesManagerWithDataBase] toma ela própria a iniciativa de criar o objeto [ArticlesDaoPlainJdbc] de que necessita. Voltando ao termo «inversão de controlo», dir-se-á que é ela que tem o «controlo» para criar o objeto de que necessita.

Se tivéssemos de escrever uma classe de teste JUnit para a classe [ArticlesManagerWithDataBase], poderíamos ter algo semelhante ao seguinte:

public class TestArticlesManagerWithDataBase extends TestCase {
    // uma instância da classe de negócio testada
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // cria uma instância da classe de negócio testada
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

A classe de teste cria uma instância da classe de negócio [ArticlesManagerWithDataBase], que, por sua vez, cria, no seu construtor, uma instância da classe de acesso aos dados [ArticlesDaoPlainJdbc].

A solução com o Spring eliminará a necessidade de a classe de negócio [ArticlesManagerWithDataBase] conhecer o nome [ArticlesDaoPlainJdbc] da classe de acesso aos dados de que necessita. Isto permitirá alterá-la sem alterar o código Java da classe de negócio. O Spring permitirá criar simultaneamente os dois singletons: o da camada de acesso aos dados e o da camada de negócios. O ficheiro de configuração do Spring definirá um novo bean:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- a classe de acesso aos dados -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
</beans>

A novidade reside no bean que define o singleton da classe de negócio a criar:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. A classe que implementa o bean [articlesManager] é definida: [ArticlesManagerWithDataBase]
  2. o campo [articlesDao] do bean recebe um valor através da baliza <property name="articlesDao">. Trata-se do campo definido na classe [ArticlesManagerWithDataBase]:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // interface de acesso aos dados
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

Para que o campo [articlesDao] possa ser inicializado pelo Spring e pela sua baliza <property>, é necessário que o campo siga a norma JavaBean e que exista um método [setArticlesDao] para inicializar o campo [articlesDao]. Deve-se notar que o nome do método é derivado de forma muito precisa do nome do campo. Paralelamente, existe frequentemente um método [get...] para obter o valor do campo. Neste caso, trata-se do método [getArticlesDao]. Nesta nova versão, a classe [ArticlesManagerWithDataBase] já não possui um construtor. Já não precisa dele.

  • O valor que será atribuído ao campo [articlesDao] pelo Spring é o do bean [articlesDao] definido no seu ficheiro de configuração:
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
    .............
    </bean>
  • quando o Spring criar o singleton [ArticlesManagerWithDataBase], terá de criar também o singleton [ArticlesDaoPlainJdbc]:
    • O Spring irá estabelecer um gráfico de dependências dos beans e verificará que o bean [articlesManager] depende do bean [articlesDao]
    • irá criar o bean [articlesDao], ou seja, um objeto do tipo [ArticlesDaoPlainJdbc]
    • depois, criará o bean [articlesManager] do tipo [ArticlesManagerWithDataBase]

Imaginemos agora um teste JUnit para a classe [ArticlesManagerWithDataBase]. Poderia ser semelhante ao seguinte:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // testa a classe de negócio [ArticlesManagerWithDataBase]

    // uma instância da classe de negócio testada
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // recupera uma instância de acesso aos dados
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

Vamos acompanhar o processo de criação dos dois singletons definidos no ficheiro Spring denominado [springArticlesManagerWithDataBase.xml].

  • O método [setUp] acima solicita uma referência ao bean denominado [articlesManager]
  • O Spring consulta o seu ficheiro de configuração e encontra o bean [articlesManager]. Se este já estiver criado, limita-se a devolver uma referência ao objeto (singleton); caso contrário, cria-o.
  • O Spring deteta a dependência do bean [articlesManager] em relação ao bean [articlesDao]. Assim, cria o singleton [articlesDao] do tipo [ArticlesDaoPlainJdbc], caso este ainda não tenha sido criado (singleton).
  • Cria o singleton [articlesManager] do tipo [ArticlesManagerWithDataBase]

Este mecanismo poderia ser esquematizado da seguinte forma:

Recorde-se a estrutura da classe [ArticlesManagerWithDataBase]:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // interface de acesso aos dados
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

No final da construção dos singletons pelo Spring, temos um objeto do tipo [ArticlesManagerWithDataBase] cujo campo [articlesDao] foi inicializado sem que ele saiba como. Diz-se que foi injetada uma dependência no objeto [ArticlesManagerWithDataBase]. Diz-se também que se inverteu o controlo: já não é o objeto [ArticlesManagerWithDataBase] que toma a iniciativa de criar ele próprio o objeto que implementa a interface [IArticlesDao] de que necessita, mas sim a aplicação ao nível mais elevado (quando se inicializa) que se encarrega de criar todos os objetos de que necessita, gerindo as dependências entre eles.

A principal vantagem da configuração do singleton [ArticlesManagerWithDataBase] através de um ficheiro Spring é que, agora, é possível alterar a classe de implementação correspondente ao campo [articlesDao] da classe [ArticlesManagerWithDataBase] sem que o código desta última seja alterado. Basta alterar o nome da classe na definição do bean [articlesDao] no ficheiro Spring:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
...
    </bean>

passará a ser, por exemplo:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap">
...
    </bean>

O bean [ArticlesManagerWithDataBase] funcionará com esta nova classe de acesso aos dados, sem sequer se aperceber disso.

2.3. Spring IoC na prática

2.3.1. Exemplo 1

Consideremos a seguinte classe:

package istia.st.springioc.domain;

public class Personne {
  private String nom;
  private int age;

   // visualização de Pessoa
  public String toString() {
    return "nom=[" + this.nom + "], age=[" + this.age + "]";
  }

   // inicialização e encerramento
  public void init() {
    System.out.println("init personne [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy personne [" + this.toString() + "]");
  }

   // getters-setters
  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }
}

A classe apresenta:

  • dois campos privados «nome» e «idade»
  • os métodos de leitura (get) e de gravação (set) destes dois campos
  • um método toString para recuperar o valor do objeto [Personne] na forma de uma cadeia de caracteres
  • um método init que será chamado pelo Spring aquando da criação do objeto, e um método close que será chamado aquando da destruição do objeto

Para criar objetos do tipo [Personne], utilizaremos o seguinte ficheiro Spring:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

Este ficheiro terá o nome config.xml.

  • Define dois beans com as chaves «pessoa1» e «pessoa2», respetivamente, do tipo [Personne]
  • Inicializa os campos [nom, age] de cada pessoa
  • define os métodos a serem chamados durante a construção inicial do objeto [init-method] e durante a destruição do objeto [destroy-method]

Para os nossos testes, utilizaremos uma única classe de teste JUnit, à qual iremos adicionar métodos sucessivamente. A primeira versão desta classe será a seguinte:

package istia.st.springioc.tests;

import istia.st.springioc.domain.Personne;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

public class Tests extends TestCase {

   // fábrica de beans
  private ListableBeanFactory bf;

   // inicialização de testes
  public void setUp() {
    bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
  }

  public void test1() {
    // recuperação dos beans [Personne] do ficheiro Spring através da sua chave
    Personne personne1 = (Personne) bf.getBean("personne1");
    System.out.println("personne1=" + personne1.toString());
    Personne personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
  }
}

Comentários:

  • para obter os beans definidos no ficheiro [config.xml], utilizamos um objeto do tipo [ListableBeanFactory]. Existem outros tipos de objetos que permitem aceder aos beans. O objeto [ListableBeanFactory] é obtido no método [setUp] da classe de teste e armazenado numa variável privada. Desta forma, estará disponível para todos os métodos de teste.
  • O ficheiro [config.xml] será colocado no [ClassPath] da aplicação, c.a.d, num dos diretórios explorados pela máquina virtual Java quando esta procura uma classe referenciada pela aplicação. O objeto [ClassPathResource] serve para procurar um recurso no [ClassPath] de uma aplicação, neste caso o ficheiro [config.xml].
  • O Spring pode utilizar ficheiros de configuração com vários formatos. O objeto [XmlBeanFactory] permite analisar um ficheiro de configuração no formato XML.
  • A análise de um ficheiro Spring resulta num objeto do tipo [ListableBeanFactory], neste caso o objeto bf. Com este objeto, um bean identificado pela chave C é obtido através de bf.getBean(C).
  • O método [test1] solicita e apresenta o valor dos beans com as chaves «pessoa1» e «pessoa2».

A estrutura do projeto Eclipse da nossa aplicação é a seguinte:

Image

Comentários:

  • A pasta [src] contém os códigos-fonte. Os códigos compilados serão colocados numa pasta [bin], aqui não representada.
  • O ficheiro [config.xml] encontra-se na raiz da pasta [src]. A compilação do projeto copia-o automaticamente para a pasta [bin], que faz parte da pasta [ClassPath] da aplicação. É aí que é procurado pelo objeto [ClassPathResource].
  • A pasta [lib] contém três bibliotecas Java necessárias para a aplicação:
      • commons-logging.jar e spring-core.jar para as classes Spring
      • junit.jar para as classes JUnit
  • a pasta [lib] também faz parte do [ClassPath] da aplicação

A execução do método [test1] do teste JUnit produz os seguintes resultados:

18 sept. 2004 11:28:53 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
personne1=nom=[Simon], age=[40]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
personne2=nom=[Brigitte], age=[20]
personne2=nom=[Brigitte], age=[20]

Comentários:

  • O Spring regista uma série de eventos através da biblioteca [commons-logging.jar]. Estes registos permitem-nos compreender melhor o funcionamento do Spring.
  • O ficheiro [config.xml] foi carregado e, em seguida, utilizado
  • a operação*
Personne personne1 = (Personne) bf.getBean("personne1");

forçou a criação do bean [personne1]. Podemos ver o registo do Spring a este respeito. Como na definição do bean [personne1] tínhamos escrito [init-method="init"], o método [init] do objeto [Personne] criado foi executado. A mensagem correspondente é apresentada.

  • A operação
System.out.println("personne1=" + personne1.toString());

fez com que fosse exibido o valor do objeto [Personne] criado.

  • O mesmo fenómeno repete-se para o bean-chave [personne2].
  • A última operação
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

não provocou a criação de um novo objeto do tipo [Personne]. Se assim fosse, teríamos assistido à execução do método [init], o que não acontece neste caso. Este é o princípio do singleton. Por predefinição, o Spring cria apenas uma única instância dos beans do seu ficheiro de configuração. Trata-se de um serviço de referências a objetos. Se lhe for solicitada a referência de um objeto ainda não criado, o Spring cria-o e devolve uma referência ao mesmo. Se o objeto já tiver sido criado, o Spring limita-se a fornecer uma referência ao mesmo.

  • Pode-se observar que não há qualquer vestígio do método [close] do objeto [Personne], embora o tenhamos escrito na definição dos beans [destroy-method=close]. É possível que este método só seja executado quando a memória ocupada pelo objeto for recuperada pelo coletor de lixo (garbage collector). Nesse momento, a aplicação já terminou e a escrita no ecrã não tem qualquer efeito. A verificar.

Agora que já dominamos os fundamentos de uma configuração Spring, as nossas explicações serão um pouco mais rápidas.

2.3.2. Exemplo 2

Consideremos a seguinte nova classe [Voiture]:

package istia.st.springioc.domain;

public class Voiture {
  private String marque;
  private String type;
  private Personne propriétaire;

   // construtores

  public Voiture() {
  }

  public Voiture(String marque, String type, Personne propriétaire) {
    this.marque = marque;
    this.type = type;
    this.propriétaire = propriétaire;
  }

   // toString
  public String toString() {
    return "Voiture : marque=[" + this.marque + "] type=[" + this.type
        + "] propriétaire=[" + this.propriétaire + "]";
  }

     // getters-setters
  public String getMarque() {
    return marque;
  }

  public void setMarque(String marque) {
    this.marque = marque;
  }

  public Personne getPropriétaire() {
    return propriétaire;
  }

  public void setPropriétaire(Personne propriétaire) {
    this.propriétaire = propriétaire;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

   // inicialização e encerramento
  public void init() {
    System.out.println("init voiture [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy voiture [" + this.toString() + "]");
  }

}

A classe apresenta:

  • três campos privados: tipo, marca e proprietário. Estes campos podem ser inicializados e lidos através dos métodos públicos get e set dos beans. Podem também ser inicializados utilizando o construtor Carro(String, String, Pessoa). A classe possui ainda um construtor sem argumentos, de modo a cumprir a norma JavaBean.
  • um método toString para recuperar o valor do objeto [Voiture] na forma de uma cadeia de caracteres
  • um método init que será chamado pelo Spring imediatamente após a criação do objeto, e um método close que será chamado aquando da destruição do objeto

Para criar objetos do tipo [Voiture], utilizaremos o seguinte ficheiro Spring [config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <constructor-arg index="0">
            <value>Peugeot</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>307</value>
        </constructor-arg>
        <constructor-arg index="2">
            <ref bean="personne2"></ref>
        </constructor-arg>
    </bean>
</beans>

Este ficheiro adiciona às definições anteriores um bean com a chave «voiture1» do tipo [Voiture]. Para inicializar este bean, poderíamos ter escrito:

    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <property name="marque">
            <value>Peugeot</value>
        </property>
        <property name="type">
            <value>307</value>
        </property>
        <property name="propriétaire">
            <ref bean="personne2"/>
        </property>
    </bean>

Em vez de optar por este método já apresentado, optámos aqui por utilizar o construtor Carro(String, String, Pessoa) da classe. Além disso, o bean [voiture1] define o método a ser chamado durante a construção inicial do objeto [init-method] e aquele a ser chamado durante a destruição do objeto [destroy-method].

Para os nossos testes, utilizaremos a classe de teste JUnit já apresentada, adicionando-lhe o seguinte método [test2]:

1
2
3
4
5
  public void test2() {
     // recuperação do bean [voiture1]
    Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
    System.out.println("Voiture1=" + Voiture1.toString());
  }

O método [test2] recupera o bean [voiture1] e apresenta-o.

A estrutura do projeto Eclipse mantém-se igual à do teste anterior. A execução do método [test2] do teste JUnit produz os seguintes resultados:

18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'voiture1'
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture(java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]

Comentários:

  1. o método [test2] solicita uma referência ao bean [voiture1]
  2. linha 4: o Spring inicia a criação do bean [voiture1], uma vez que este bean ainda não foi criado (singleton)
  3. linha 6: como o bean [voiture1] faz referência ao bean [personne2], este último bean é, por sua vez, criado
  4. linha 7: o bean [personne2] foi criado. O seu método [init] é então executado.
  5. linha 9: o Spring indica que vai utilizar um construtor para criar o bean [voiture1]
  6. linha 10: o bean [voiture1] foi criado. O seu método [init] é então executado.
  7. linha 11: o método [test2] exibe o valor do bean [voiture1]

2.3.3. Exemplo 3

Introduzimos a seguinte nova classe [GroupePersonnes]:

package istia.st.springioc.domain;

import java.util.Map;

public class GroupePersonnes {
  private Personne[] membres;
  private Map groupesDeTravail;

   // getters - setters
  public Personne[] getMembres() {
    return membres;
  }

  public void setMembres(Personne[] membres) {
    this.membres = membres;
  }

  public Map getGroupesDeTravail() {
    return groupesDeTravail;
  }

  public void setGroupesDeTravail(Map groupesDeTravail) {
    this.groupesDeTravail = groupesDeTravail;
  }

   // exibição
  public String toString() {
    String liste = "membres : ";
    for (int i = 0; i < this.membres.length; i++) {
      liste += "[" + this.membres[i].toString() + "]";
    }
    return liste + ", groupes de travail = " + this.groupesDeTravail.toString();
  }

   // inicialização-encerramento
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy GroupePersonnes [" + this.toString() + "]");
  }
}

Os seus dois membros privados são:

membros: um array de pessoas que fazem parte do grupo

groupesDeTravail: um dicionário que atribui uma pessoa a um grupo de trabalho

Note-se aqui que a classe [GroupePersonnes] não define um construtor sem argumentos, em conformidade com a norma JavaBean. Recorde-se que, na ausência de qualquer construtor, existe um construtor «padrão», que é o construtor sem argumentos e que não faz nada.

O objetivo aqui é mostrar como o Spring permite inicializar objetos complexos, tais como objetos que possuem campos do tipo matriz ou dicionário. Adicionamos um novo bean ao ficheiro Spring [config.xml] anterior:

    <bean id="groupe1" class="istia.st.springioc.domain.GroupePersonnes" 
        init-method="init" destroy-method="close">
        <property name="membres">
            <list>
                <ref bean="personne1"/>
                <ref bean="personne2"/>
            </list>
        </property>
        <property name="groupesDeTravail">
            <map>
                <entry key="Brigitte">
                    <value>Marketing</value>
                </entry>
                <entry key="Simon">
                    <value>Ressources humaines</value>
                </entry>
            </map>
        </property>
    </bean>
  1. A baliza <list> permite inicializar um campo do tipo matriz ou que implemente a interface List com diferentes valores.
  2. A baliza <map> permite fazer o mesmo com um campo que implemente a interface Map

Para os nossos testes, utilizaremos a classe de teste JUnit já apresentada, adicionando-lhe o seguinte método [test3]:

1
2
3
4
5
  public void test3() {
    // recuperação do bean [groupe1]
    GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
    System.out.println("groupe1=" + groupe1.toString());
  }

O método [test3] recupera o bean [groupe1] e apresenta-o.

A estrutura do projeto Eclipse mantém-se igual à do teste anterior. A execução do método [test3] do teste JUnit produz os seguintes resultados:

18 sept. 2004 15:51:45 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'groupe1'
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
init GroupePersonnes [membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}]
groupe1=membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}

Comentários:

  • o método [test3] solicita uma referência ao bean [groupe1]
  • linha 4: o Spring inicia a criação deste bean
  • uma vez que o bean [groupe1] faz referência aos beans [personne1] e [personne2], estes dois beans são criados (linhas 6 e 9) e o seu método init é executado (linhas 7 e 10)
  • linha 11: o bean [groupe1] foi criado. O seu método [init] é agora executado.
  • linha 12: exibição solicitada pelo método [test3].

2.4. Spring para configurar aplicações web de três camadas

2.4.1. Arquitetura geral da aplicação

Pretende-se construir uma aplicação de três camadas com a seguinte estrutura:

  • as três camadas serão tornadas independentes através da utilização de interfaces Java
  • A integração das três camadas será realizada pelo Spring
  • Serão criados pacotes separados para cada uma das três camadas, que serão denominados Control, Domain e Dao. Um pacote adicional conterá as aplicações de teste.

A estrutura da aplicação no Eclipse poderá ser a seguinte:

Image

2.4.2. A camada DAO de acesso aos dados

A camada DAO implementará a seguinte interface:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • Escrever duas classes, Dao1Impl1 e Dao1Impl2, que implementem a interface IDao1. O método Dao1Impl1. doSomethingInDaoLayer irá devolver a+b e o método Dao1Impl2. doSomethingInDaoLayer irá devolver a-b.
  • escrever uma classe de teste JUnit que teste as duas classes anteriores

2.4.3. A camada de negócio

A camada de negócio irá implementar a seguinte interface:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • Escrever duas classes, Domain1Impl1 e Domain1Impl2, que implementem a interface IDomain1. Estas classes terão um construtor que receba como parâmetro um objeto do tipo IDao1. O método Domain1Impl1.doSomethingInDomainLayer incrementará a e b em uma unidade e, em seguida, passará esses dois parâmetros para o método doSomethingInDaoLayer do objeto do tipo IDao1 recebido. O método Domain1Impl2.doSomethingInDomainLayer, por sua vez, diminuirá a e b em uma unidade antes de fazer o mesmo.
  • Escrever uma classe de teste JUnit para testar as duas classes anteriores

2.4.4. A camada de interface do utilizador

A camada de interface do utilizador implementará a seguinte interface:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • Escrever duas classes, Control1Impl1 e Control1Impl2, que implementem a interface IControl1. Estas classes terão um construtor que recebe um parâmetro do tipo IDomain1. O método Control1Impl1.doSomethingInControlLayer incrementará a e b em uma unidade e, em seguida, passará esses dois parâmetros para o método doSomethingInDomainLayer do objeto do tipo IDomain1 recebido. Já o método Control11Impl2.doSomethingInControlLayer irá decrementar a e b em uma unidade antes de fazer o mesmo.
  • Escrever uma classe de teste JUnit para testar as duas classes anteriores

2.4.5. Integração com o Spring

  • Escrever um ficheiro de configuração do Spring que determinará quais as classes que cada uma das três camadas anteriores deverá utilizar
  • Escrever uma classe de teste JUnit utilizando diferentes configurações do Spring, de modo a destacar a flexibilidade da aplicação criada
  • Escrever uma aplicação autónoma (método main) que passe dois parâmetros à interface IControl1 e exiba o resultado gerado pela interface.

2.4.6. Uma solução

2.4.6.1. O projeto Eclipse

Image

Os arquivos da pasta [lib] foram adicionados ao [ClassPath] do projeto.

2.4.6.2. O pacote [istia.st.demo.dao]

A interface:

1
2
3
4
5
6
7
8
9
package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}

Uma primeira classe de implementação:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public class Dao1Impl1 implements IDao1 {

     // faz-se algo na camada [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

Uma segunda classe de implementação:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *
 */
public class Dao1Impl2 implements IDao1 {

     // está a ser feito algo na camada [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. O pacote [istia.st.demo.domain]

A interface:

package istia.st.demo.domain;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDomain1 {

     // faz-se algo na camada [domain]
  public int doSomethingInDomainLayer(int a, int b);
}

Uma primeira classe de implementação:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl1 implements IDomain1 {

     // o serviço de acesso à camada [dao]
  private IDao1 dao1;

  public Domain1Impl1() {
     // construtor sem argumentos
  }

     // armazena o serviço de acesso à camada [dao]
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // realiza-se alguma ação na camada [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

Uma segunda classe de implementação:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl2 implements IDomain1 {

     // o serviço de acesso à camada [dao]
  private IDao1 dao1;

  public Domain1Impl2() {
     // construtor sem argumentos
  }

     // armazena o serviço de acesso à camada [dao]
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // realiza-se alguma ação na camada [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. O pacote [istia.st.demo.control]

A interface

1
2
3
4
5
6
7
8
9
package istia.st.demo.control;

/**
 * @author ST-ISTIA
 *  
 */
public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}

Uma primeira classe de implementação:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // classe de negócio na camada [domain]
    private IDomain1 domain1;

  public Control1Impl1() {
     // construtor sem argumentos
  }

     // memorização do serviço de acesso à camada [domain]
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // faz-se alguma coisa
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

Uma segunda classe de implementação:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl2 implements IControl1 {

     // a classe de acesso à camada [domain]
    private IDomain1 domain1;

  public Control1Impl2() {
     // construtor sem argumentos
  }

     // armazena a classe de acesso à camada [domain]
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // faz-se alguma coisa
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. Os ficheiros de configuração [Spring]

Um primeiro [springMainTest1.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- a classe DAO -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
    </bean>
     <!-- a classe de negócio -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- a classe de controlo -->
    <bean id="control" class="istia.st.demo.control.Control1Impl1">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

Um segundo [springMainTest2.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- a classe DAO -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
    </bean>
     <!-- a classe de negócio -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- a classe de controlo -->
    <bean id="control" class="istia.st.demo.control.Control1Impl2">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

2.4.6.6. O pacote de testes [istia.st.demo.tests]

Um teste do tipo [main]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest1 {
  public static void main(String[] arguments) {
     // recupera-se uma implementação da interface IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // utiliza-se a classe
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // exibe-se o resultado
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Os resultados obtidos na consola do Eclipse:

11 mars 2005 11:25:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest1.xml]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl1(istia.st.demo.dao.IDao1)]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl1(istia.st.demo.domain.IDomain1)]
control(10,20)=34

Outro teste utilizando o segundo ficheiro de configuração [Spring]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest2 {
  public static void main(String[] arguments) {
     // recupera-se uma implementação da interface IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // utiliza-se a classe
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // exibe-se o resultado
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Resultados obtidos na consola do Eclipse:

11 mars 2005 11:28:52 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest2.xml]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl2(istia.st.demo.dao.IDao1)]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl2(istia.st.demo.domain.IDomain1)]
control(10,20)=-10

Por fim, um teste Junit:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

/**
 * @author ST-ISTIA
 *  
 */
public class JunitTest2Control1 extends TestCase {
  public void testControl1() {
     // recupera-se uma implementação da interface IControl1
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // utiliza-se a classe
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // recupera-se outra implementação da interface IControl1
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // utiliza-se a classe
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusion

O framework Spring permite uma verdadeira flexibilidade, tanto na arquitetura das aplicações como na sua configuração. Utilizámos o conceito IoC, um dos dois pilares do Spring. O outro pilar é o AOP (Programação Orientada a Aspectos), que não apresentámos. Permite adicionar, através da configuração, «comportamento» a um método de uma classe sem alterar o código da mesma. De forma esquemática, o AOP permite filtrar as chamadas a determinados métodos:

  • o filtro pode ser executado antes ou depois do método M alvo, ou em ambos os casos.
  • O método M ignora a existência destes filtros. Estes são definidos no ficheiro de configuração do Spring.
  • O código do método M não é alterado. Os filtros são classes Java a serem criadas. O Spring fornece filtros predefinidos, nomeadamente para gerir as transações do SGBD.
  • Os filtros são beans e, como tal, são definidos no ficheiro de configuração do Spring como beans.

Um filtro comum é o filtro transacional. Consideremos um método M da camada de negócio que realiza duas operações indissociáveis sobre dados (unidade de trabalho). Este método recorre a dois métodos, M1 e M2, da camada DAO para realizar essas duas operações.

Por se encontrar na camada de negócio, o método M ignora o suporte destes dados. Não tem, por exemplo, de partir do princípio de que os dados se encontram num SGBD e de que precisa de colocar as duas chamadas aos métodos M1 e M2 no âmbito de uma transação de SGBD. Cabe à camada DAO tratar desses detalhes. Uma solução para o problema anterior consiste, então, em criar um método na camada DAO que, por sua vez, chamasse os métodos M1 e M2, chamadas que incluiria numa transação de SGBD.

A solução de filtragem AOP é mais flexível. Permite definir um filtro que, antes da chamada de M, iniciará uma transação e, após a chamada, executará um commit ou um rollback, conforme o caso.

Esta abordagem apresenta várias vantagens:

  • uma vez definido o filtro, este pode ser aplicado a vários métodos, por exemplo, todos aqueles que necessitam de uma transação
  • os métodos assim filtrados não precisam de ser reescritos
  • os filtros a utilizar são definidos por configuração, pelo que podem ser alterados

Para além dos conceitos IoC e AOP, o Spring disponibiliza várias classes de suporte para aplicações de três camadas:

  • para JDBC, SqlMap (iBatis), Hibernate, JDO (Java Data Object) na camada DAO
  • para o modelo MVC na camada de Interface do Utilizador

Para mais informações: http://www.springframework.org.