Mês: outubro 2009

Spring.NET – Parte 4 – Programação Orientada a Aspectos

Objetivo

O objetivo desta parte do tutorial é mostrar a poderosa implementação de AOP no Spring. AOP (aspect-oriented programming) ou programação orientada a aspectos é uma idéia para facilitar a implementação de requisitos não-funcionais da aplicação.

Neste artigo estaremos abordando alguns exemplos de requisitos não-funcionais, a idéia central da AOP e como implementar um recurso usando essas funcionalidades no Spring.Net

Requisitos Funcionais e Não-Funcionais

Dentro da disciplina de levantamento de requisitos (ver Levantamento de Requisitos), temos os requisitos funcionais da aplicação, ou seja, as “telas” em que o usuário vai interagir no sistema, interfaces e tudo o que descreva o comportamento dos sistemas.

Para esse tipo de requisito, a abordagem da programação orientada a objetos, principalmente a idéia do encapsulamento é fundamental para que seja possível implementar estes requisitos de forma reusável e de fácil entendimento. A programação orientada a aspectos não vem ajudar nem substituir essa idéia. Ela vem para complementá-la.

Dentro da idéia de requisitos não-funcionais temos “características” da aplicação. Confiabilidade, performance, segurança, etc.

Alguns desses requisitos que o usuário nos solicita complicam muito nossa vida em tempo de implementação. Alguns exemplos:

  • Todos as operações da minha aplicação precisam ser auditáveis. Em outras palavras, preciso de log de tudo.
  • Todos os web services devem ser seguros.
  • As operações da minha aplicação devem ser transacionais
  • Deve ser possível monitorar a performance de todas as operações do sistema.

Como vemos, além de bem subjetivos, esses requisitos não são de fácil implementação. Usando uma abordagem “tradicional” de programação (mesmo que orientada a objetos), precisariamos nos preocupar com esses aspectos em cada um dos requisitos funcionais.

Ex.: Numa aplicação que controla pedidos, precisamos sempre, no momento de fazer um pedido lembrar de:

  • Gravar registros nas tabelas de log no momento da gravação, alteração e exclusão do pedido.
  • Em cada método do web service, impedir que um usuário não autenticado não veja as informações e ainda impedir que quando autenticado veja informações de outros usuários
  • Lembrar de abrir e concluir ou dar rollback na transação em cada uma das operações.
  • Lembrar de calcular o tempo de início e fim de cada operação e persistir esses tempos.

Mesmo seguindo a idéia de encapsulamento, ou seja, cada classe tendo a sua responsabilidade bem definida e criando uma “capa” bem definidas para esconder comportamentos reusáveis e complexos, fica praticamente impossível de implementar idéias como essas cada uma no seu lugar. Como eu vou implementar transação, log, profiling (cálculo dos tempos de execução) e segurança de uma forma centralizada se em cada classe que implementa requisitos funcionais (no exemplo acima, Pedido), esses métodos precisam ser chamados?

É para suprir essa complexidade que temos a idéia da programação orientada a aspectos.

Implementando um Aspecto no Spring.Net

Podemos conceituar como Aspecto, uma dessas funcionalidades “espalhadas” por toda a aplicação. Para simplicar o nosso exemplo, vamos fazer um aspecto de Profiling (cálculo dos tempos de execução das operações).

Para simplificar nossa implementação, vamos utilizar como “base” o nosso tutorial parte 1 do Spring: Spring.Net – Parte 3. Suporte ADO e Transação.

Aqui vamos começar a entender como todo o custo de criar o container e suas implementações vai se pagar. Cada centavo.

Tabela ExecucaoOperacao

Vamos precisar criar uma nova tabela na base de dados para persistir os tempos de execução das operações. Poderia ser qualquer outro meio de persistência. Estamos criando no banco apenas para fins didáticos.

A estrutura da tabela é a seguinte:

create table ExecucaoOperacao(
  ExecucaoOperacaoID int identity(1,1) not null,
  NomeOperacao varchar(200) not null,
  Segundos int not null,
  ExecucaoOperacaoPaiID int
  constraint PK_ExecucaoOperacao primary key (ExecucaoOperacaoID)
  constraint FK_ExecucaoOperacao_ExecucaoOperacaoPai foreign key (ExecucaoOperacaoPaiID) references 
	  ExecucaoOperacao(ExecucaoOperacaoID)
)

A idéia do auto-relacionamento é justamente para criar todo o encadeamento das operações, ou seja: Para executar a operação “inserirPedido” na camada de negócio, ela executa várias vezes inserirPedido e inserirItemPedido da camada DAL. teremos um registro para cada operação, sendo que os da camada DAL vão possuir o registro da camada BO como “pai”.

Assim podemos ter um nível de detalhe maior de quais são os métodos que demoram mais para serem executados.

Criando model para ExecucaoOperacao

Vamos criar um VO para representar nossa classe de log. O problema aqui é que existe o encadeamento pai/filho, ou seja, uma operação pode disparar outras operações e assim sucessivamente.

Para representar essa estrutura, vamos usar a seguinte classe, criada no assembly Model, com o nome ExecucaoOperacao:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Model {
  public class ExecucaoOperacao {
    public int ExecucaoOperacaoID{
      get; set;
    }
    
    public string NomeOperacao{
      get; set;
    }
    
    public int Segundos{
      get; set;
    }
    
    public int? ExecucaoOperacaoPaiID{
      get; set;
    }
    
    private List<ExecucaoOperacao> _execucoesFilhas = new List<ExecucaoOperacao>();
    public List<ExecucaoOperacao> execucoesFilhas{
      get { return _execucoesFilhas;}
      set { _execucoesFilhas = value;}
    }
  }
}

Como vemos acima, temos dessa classe uma collection que conterá “n” vezes ela mesma, criando uma estrutura recursiva.

Criando a camada DAL para ExecucaoOperacao

Vamos construir a classe para persistência de ExecucaoOperacao seguindo a mesma idéia da parte 3 do nosso tutorial, usando o mesmo conceito de transação e de persistência das demais classes.

Para isso, vamos no assembly DAL.Interface e vamos criar a interface IExecucaoOperacaoDAO, com o código a seguir:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Model;

namespace DAL.Interface {
  public interface IExecucaoOperacaoDAO {
    void inserirExecucaoOperacao(ExecucaoOperacao e);
  }
}

A idéia aqui é criar apenas um método para “gravar no log” um registro de ExecucaoOperacao.

Vamos criar também uma implementação para essa interface, na classe ExecucaoOperacaoDAO, no assembly DAO.ADO.SqlServer, com o código a seguir:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DAL.Interface;
using Spring.Data.Generic;
using Spring.Data.Common;
using System.Data;

namespace DAL.ADO.SQLServer {
  public class ExecucaoOperacaoDAO : AdoDaoSupport, IExecucaoOperacaoDAO {

    public void inserirExecucaoOperacao(Model.ExecucaoOperacao e) {
      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("NomeOperacao").Type(DbType.String).Value(e.NomeOperacao);
      builder.Create().Name("Segundos").Type(DbType.Int32).Value(e.Segundos);
      builder.Create().Name("ExecucaoOperacaoPaiID").Type(DbType.Int32).Value(e.ExecucaoOperacaoPaiID);

      string sql =
        "insert into ExecucaoOperacao (NomeOperacao, Segundos, ExecucaoOperacaoPaiID) " +
        "values (@NomeOperacao, @Segundos, @ExecucaoOperacaoPaiID) \n" +
        "select SCOPE_IDENTITY()";

      decimal d = (decimal)AdoTemplate.ExecuteScalar(CommandType.Text, sql, builder.GetParameters());
      e.ExecucaoOperacaoID = Convert.ToInt32(d);
    }
   
  }
}

Criando BO para ExecucaoOperacao

Aqui vamos criar a classe que será “exposta” como serviço para gravação de logs. Seguindo a mesma estrutura do Spring, temos a interface separada da implementação.

A interface IExecucaoOperacaoBO deve estar no assembly BLL.Interface e terá o seguinte código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Model;

namespace BLL.Interface {
  public interface IExecucaoOperacaoBO {
    void inserirExecucaoOperacao(ExecucaoOperacao e);
  }
}

A sua implementação, ExecucaoOperacaoBO estará no assembly BLL.Implementation e terá o seguinte código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BLL.Interface;
using Model;
using DAL.Interface;
using Spring.Transaction.Interceptor;

namespace BLL.Implementation {
  public class ExecucaoOperacaoBO : IExecucaoOperacaoBO {
    #region Injected Stuff
    
    public IExecucaoOperacaoDAO execucaoOperacaoDAO{
      get; set;
    }
    
    #endregion
  
    #region IExecucaoOperacaoBO Members

    [Transaction]
    public void inserirExecucaoOperacao(ExecucaoOperacao e) {
      execucaoOperacaoDAO.inserirExecucaoOperacao(e);
      foreach(ExecucaoOperacao filha in e.execucoesFilhas){
        //Recursivamente insere as filhas...
        filha.ExecucaoOperacaoPaiID = e.ExecucaoOperacaoID;
        inserirExecucaoOperacao(filha); 
      }
    }

    #endregion
  }
}

A idéia aqui é simples. O método inserirExecucaoOperacao recebe um vo ExecucaoOperacao. Ela faz a gravação do “cabeçalho” (primeiro registro do log). Este por sua vez vai ser um método do DAO que vai inserir UM registro e vai gerar o seu identity.

Fazemos então um loop nas operações filhas e para cada uma delas chamamos recursivamente o método inserirExecucaoOperacao da camada BO, que por sua vez fará o mesmo procedimento nas filhas, passando os ID’s gerados pelo identity corretamente.

Vale lembrar que graças à simplicidade do conceito de transação do Spring, a anotação [Transaction] garante que “ou tudo acontece, ou nada acontece”. Sem precisar ficar tratando commits e rollbacks de acordo com situações específicas na aplicação.

Criando o ProfilingAdvice

Vamos começar criando um novo Solution Folder chamado “Aspects” na nossa solution. Aqui vamos colocar nossas implementações relacionadas aos aspectos da aplicação.

O Advice (não consegui arrumar uma boa tradução para isso. Ao pé-da-letra seria “conselho”) representa a idéia do código que vai ser “espalhado” por diversos objetos para implementar um aspecto.

Para criar um advice, vamos primeiro criar uma nova class library “Aspects” dentro do nosso solution folder “Aspects”. Essa class library precisa referenciar o assembly Spring.Aop, obtido no “pacote” baixado do springframework.net.

Vamos então criar uma nova classe chamada “ProfilingAdvice” dentro dessa class library. Essa classe deve implementar a interface AopAlliance.Intercept.IMethodInterceptor. Essa interface está definida no assembly Spring.Aop.

Existem outros tipos de advice. Mais detalhes podem ser obtidos na própria documentação do Spring.

Nessa interface, está definido o método “Invoke”, que será definido conforme abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AopAlliance.Intercept;
using Model;
using BLL.Interface;

namespace Aspects {
  public class ProfilingAdvice : IMethodInterceptor {
    #region Injected Stuff
    
    public IExecucaoOperacaoBO execucaoOperacaoBO{
      get; set;
    }
    
    #endregion 
  
  
    #region IMethodInterceptor Members
    
    private Stack<ExecucaoOperacao> _stack = new Stack<ExecucaoOperacao>();

    public object Invoke(IMethodInvocation invocation) {
      ExecucaoOperacao prev = null;
      if (_stack.Count > 0) {
        prev = _stack.Pop();        
        _stack.Push(prev);
      }                    
    
      ExecucaoOperacao exec = new ExecucaoOperacao();
      exec.NomeOperacao = invocation.Method.Name;      
      if(prev != null)
        prev.execucoesFilhas.Add(exec);
      _stack.Push(exec);
      
      DateTime start = DateTime.Now;
      object result = invocation.Proceed();
      DateTime end = DateTime.Now;     
       
      exec.Segundos = end.Subtract(start).Seconds;                  
            
      exec = _stack.Pop();
      if (_stack.Count <= 0){
        execucaoOperacaoBO.inserirExecucaoOperacao(exec);
      }        
      
      return result;
    }

    #endregion
  }
}

Essa implementação apesar de extremamente simples, é muito interessante. Usamos um stack para controlar a “pilha” de execução dos métodos. Sempre que algum método for invocado e passar neste advice, vamos verificar se já existe um objeto na pilha. Caso já exista, inserimos o novo “ExecucaoOperacao” como “filho” do anterior.

Após a execução do método, desempilhamos um ExecucaoOperacao. Caso não exista mais nada na pilha, disparamos a gravação dele, usando o nosso ExecucaoOperacaoBO. Isso vai disparar recursivamente a gravação de todos os filhos.

Configuração e Testes

Configurando a aplicação Web e o Web.config.

Primeiro vamos adicionar as referências necessárias na Web.config. Como sabemos, é necessário em tempo de desenvolvimento apenas conhecer as interfaces, mas vamos adicionar também as implementações para garantir que os assemblys necessários estejam no diretório bin em run-time.

Para isso, adicionamos referências no Web.Config para o assembly Aspects, e vamos começar a configuração da Web.config.

Primeiro vamos adicionar dentro da seção /, o seguinte nó:

<object id="profilingAdvice" type="Aspects.ProfilingAdvice, Aspects">
  <property name="execucaoOperacaoBO" ref="ExecucaoOperacaoBO" />
</object>
<object id="ExecucaoOperacaoDAO" type="DAL.ADO.SqlServer.ExecucaoOperacaoDAO, DAL.ADO.SqlServer" >
  <property name="AdoTemplate" ref="adoTemplate" />
</object>
<object id="ExecucaoOperacaoBO" type="BLL.Implementation.ExecucaoOperacaoBO, BLL.Implementation" autowire="byType" />        

Com isso, estaremos criando uma instância do nosso “ProfilingAdvice”, assim como criamos de qualquer objeto no nosso contexto. Não existe nada de especial aqui. O mesmo para o ExecucaoOperacaoDAO

Em seguida, vamos criar o seguinte nó, dentro da mesma seção:

<object id="advicedPedidoBO" type="Spring.Aop.Framework.ProxyFactoryObject" lazy-init="true" >
  <property name="target" ref="PedidoBO" />
  <property name="interceptorNames">
    <list>
      <value>profilingAdvice</value>
    </list>
  </property>
</object>   

Aqui entra um pouco de magia negra. Vamos criar no contexto um objeto do tipo ProxyFactoryObject. Esse objeto terá como “target” o nosso PedidoBO tradicional (criado na parte 3 do tutorial) e terá como “interceptor” o nosso “profilingAdvice”.

Na prática, teremos instanciado no contexto um objeto de nome “advicedPedidoBO”, que será um “mix” do nosso PedidoBO + profilingAdvice. Posteriormente vamos tentar descrever melhor como esse proxy funciona.

Para continuar nossa configuração, vamos ter que fazer ainda uma alteração na declaração do nosso ~/Default.aspx. Invés de injetar a instância “pura” do PedidoBO, vamos usar o advicedPedidoBO (PedidoBO + profilingAdvice):

<object type="~/Default.aspx" >
  <property name="pedidoBO" ref="advicedPedidoBO" />
</object>

Primeiro teste

Para simplificar o entendimento aqui, vamos colocar breakpoints nos métodos inserirPedido do PedidoBO, método Invoke do nosso ProfilingAdvice e no método inserirPedido do default.aspx.cs.

Agora vamos executar a aplicação e clicar em Salvar Pedido.

No nosso primeiro breakpoint, em default.aspx.cs, verificamos no método inserirPedido que a instância que recebemos de _pedidoBO é de um tipo “esquisito”:

Esse tipo foi magicamente “fabricado” em tempo de execução pelo Spring.Net, sendo um proxy de composição que garante a execução do código que está no Advice e pelo código que está no Service. Magia negra, não?

Agora se continuarmos a execução, verificamos que o próximo breakpoint é o do profilingAdvice. Nosso código cria a instância do ExecucaoOperacao, faz o encadeamento pai/filho qdo necessário, e calcula o início do tempo de execução.

No ponto onde o código cai em “Invocation.Proceed”, verificamos que o código cai no nosso inserirPedido do PedidoBO. Que é quem faz o insert na tabela pedido.

Na seqüência, volta para o Advice, calcula o tempo total que o método terminou de executar e persiste na tabela ExecucaoOperacao.

Legal, né?

Configurando o Advice na camada DAO

No exemplo acima verificamos que para a execução de um método, sem encadeamento, tudo funciona normalmente. Agora para complicar um pouquinho, vamos inserir o mesmo advice em outro objeto no nosso contexto.

Para isso, vamos inserir mais dois objetos no contexto, com a seguinte definição:

<object id="advicedPedidoDAO" type="Spring.Aop.Framework.ProxyFactoryObject" lazy-init="true" >
  <property name="target" ref="PedidoDAO">
  </property>
  <property name="interceptorNames">
    <list>
     <value>profilingAdvice</value>
    </list>
  </property>
</object>
<object id="advicedItemPedidoDAO" type="Spring.Aop.Framework.ProxyFactoryObject" lazy-init="true" >
  <property name="target" ref="ItemPedidoDAO">
  </property>
  <property name="interceptorNames">
    <list>
      <value>profilingAdvice</value>
    </list>
  </property>
</object>

E também será necessário alterar a definição do nosso PedidoBO, removendo o autowire. Com essa configuração, teríamos 2 objetos no contexto com o mesmo tipo e uma exceção “Unsatisfied dependency expressed through object property ‘pedidoDAO’: There are 2 objects of Type [DAL.Interface.IPedidoDAO] for autowire by type, when there should have been just 1 to be able to autowire property ‘pedidoDAO’ of object ‘PedidoBO’.”.

Mais a frente veremos uma forma mais “elegante” de fazer essa configuração no Spring.

A definição do PedidoBO fica da seguinte forma:

<object id="PedidoBO" type="BLL.Implementation.PedidoBO, BLL.Implementation">
  <property name="pedidoDAO" ref="advicedPedidoDAO" />
  <property name="itemPedidoDAO" ref="advicedItemPedidoDAO" />
</object>

Agora mais uma vez se executarmos a aplicação e clicarmos em Salvar Pedido, vamos observar que para cada execução de inserirPedido no BO, inserirPedido no DAO e inserirItemPedido, passamos pelo advice, criando corretamente o encadeamento e o Stack de execução, compondo no método principal a soma do tempo de execução dos demais.

Legal, não?

Configuração Avançada

Configurando

O conceito é muito bacana, mas ficar configurando isso tudo na mão cansa um pouco, né?

O Spring tem algumas alternativas para simplificar esse processo. Aí entram alguns novos conceitos que precisamos compreender.

  • Advisor: A tradução ao pé da letra seria “conselheiro”. Na prática é mais ou menos isso… ele sai dando “conselhos” para todos os objetos do contexto, ou seja, aplica os advices nos objetos do contexto.
  • Pointcut: “ponto-de-corte” (traduções literais são péssimas!). A idéia é uma “regra” para sair distribuindo conselhos por aí.

Não conseguiu entender as metáforas? Tudo bem… eu também não entendi direito. Vamos fazer na prática que fica mais fácil.

Primeiro precisamos configurar a seção “aop” no Spring. Vamos adicionar dentro do nó /:

<parser type="Spring.Aop.Config.AopNamespaceParser, Spring.Aop"/>      

Vamos alterar também no nó o atributo, associando o namespace aop ao parser adicionado na configuração acima:

    <objects 
      xmlns="http://www.springframework.net"
      xmlns:tx="http://www.springframework.net/tx"
      xmlns:db="http://www.springframework.net/database"
      xmlns:aop="http://www.springframework.net/aop"
      >

Agora vamos remover toda a configuração manual que fizemos dos advices, removendo os seguintes nós:

      <object id="advicedPedidoDAO" type="Spring.Aop.Framework.ProxyFactoryObject" lazy-init="true" >
        <property name="target" ref="PedidoDAO">
        </property>
        <property name="interceptorNames">
          <list>
            <value>profilingAdvice</value>
          </list>
        </property>
      </object>
      <object id="advicedItemPedidoDAO" type="Spring.Aop.Framework.ProxyFactoryObject" lazy-init="true" >
        <property name="target" ref="ItemPedidoDAO">
        </property>
        <property name="interceptorNames">
          <list>
            <value>profilingAdvice</value>
          </list>
        </property>
      </object>
      <object id="advicedPedidoBO" type="Spring.Aop.Framework.ProxyFactoryObject" lazy-init="true" >
        <property name="target" ref="PedidoBO">
        </property>
        <property name="interceptorNames">
          <list>
            <value>profilingAdvice</value>
          </list>
        </property>
      </object>

Vamos alterar também a configuração dos objetos abaixo para não mais usarem o advicedPedidoBO:

<object id="PedidoBO" type="BLL.Implementation.PedidoBO, BLL.Implementation" autowire="byType" />        
<object type="~/Default.aspx" >
  <property name="pedidoBO" ref="PedidoBO" />
</object>

Se nesse momento executarmos a aplicação, vamos perceber que não mais são gerados registros em ExecucaoOperacao.

Agora vamos adicionar a seguinte configuração, dentro do nó /:

      <object id="TrueMethodMatcher" type="Spring.Aop.TrueMethodMatcher" />

      <object id="allTypesExceptExecucaOperacaoBOPointcut" type="Spring.Aop.Support.ComposablePointcut">
        <constructor-arg name="typeFilter" ref="allTypesExceptExecucaoOperacaoBOTypeFilter" />
        <constructor-arg name="methodMatcher" ref="TrueMethodMatcher"  />
      </object>
      
      <object id="allTypesExceptExecucaoOperacaoBOTypeFilter" type="Spring.Aop.Support.TypeNameTypeFilter, Spring.Aop">
        <constructor-arg name="patterns">
          <list>
            <value>DAL.ADO.SQLServer.P*</value>
            <value>DAL.ADO.SQLServer.I*</value>
            <value>BLL.Implementation.PedidoBO</value>
          </list>
        </constructor-arg>
      </object>
      <aop:config>
        <aop:advisor id="loggingAdvisor" advice-ref="profilingAdvice" pointcut-ref="allTypesExceptExecucaOperacaoBOPointcut" />
      </aop:config>

Testando e Entendendo

Reinicie a aplicação e clique em Salvar Pedido. Você vai reparar que o profilingAdvice será executado uma vez para cada um dos métodos do PedidoBO, PedidoDAO e ItemPedidoDAO.

O que aconteceu então?

A configuração aop-advisor, instancia um “conselheiro”, que vai sair aplicando “conselhos” do tipo “profilingAdvice” em todos os objetos de contexto baseados nos critérios definidos pelo pointcut “allTypesExceptExecucaOperacaoBOPointcut”.

O PointCut é definido por duas interfaces:

  • ITypeFilter: Permite a definição de um critério para definir quais tipos terão o advice aplicado.
  • IMethodMatcher: Permite a definição de um critério para definir quais tipos terão o advice aplicado.

Nosso objeto “allTypesExceptExecucaOperacaoBOPointcut”, é uma instância de ComposablePointcut. Essa classe vem na framework do Spring prontinha. Você passa para ela um IMethodMatcher e um ITypeFilter como parâmetro no seu construtor para definir o critério.

Como parâmetro para o MethodMatcher, mandamos o TrueMethodMatcher, que é uma classe da framework do Spring que simplesmente considera que todo método é valido para o pointcut, por isso “true” method matcher.

Como parâmetro para o TypeFilter, mandamos o allTypesExceptExecucaoOperacaoBOTypeFilter, que é uma instância de TypeNameTypeFilter. Como o próprio nome diz, é uma implementação de uma classe que filtra por nome de tipo, baseado nos critérios que informamos no parâmetro “patterns” passado como construturo.

Em outras palavras, nosso pointcut define que todo objeto instanciado no contexto que atendam as expressões “DAL.ADO.SQLServer.P*”, “DAL.ADO.SQLServer.I*” e “BLL.Implementation.PedidoBO” serão atendidas pelo pointcut.

Conclusão

Como podemos observar a framework de AOP nos ajuda a economizar toneladas de código repetitivo. No exemplo acima, temos uma única entidade “Pedido”. Imagine um sistema com centenas de entidades e todo o seu código para logging, transação, profiling centralizado num único lugar. É muita economia de código.

Além do tanto de código que podemos economizar, ganhamos ainda toda a flexibilidade de tratar esses aspectos como “configuração” na aplicação, seguindo a idéia de injeção de dependência apresentada na parte 1 do tutorial.

É através dessa mesma abordagem que o Spring implementa os recursos que vimos na parte 2 e 3 do tutorial, ou seja, toda a parte de transação e exportação de Web Services.

Por aí, podemos ter uma grande idéia do quanto essa ferramenta pode nos ajudar a explorar novos horizontes. Espero que aproveitem bastante o conceito.

Código fonte da solução

O código fonte pode ser baixado aqui: http://ericlemes.wikidot.com/local–files/dotnet-spring-pt4/SpringParte4.zip.

Anúncios