08 dez 2010

Spring Integration

Uma arquitetura desacoplada com o Spring Integration


A cada dia crescem mais as aplicações que disponibilizam serviços Web para que qualquer cliente possa consultá-los. E a cada dia que passa nossas aplicações dependem mais e mais desses serviços para obter informações de usuários, armazenar dados remotamente, consultar emails, postar entradas no twitter dentre outros. Por isso logo surge um problema o qual é bem difícil de lidar: dependência da API dos serviços.

Depender de uma determinada API para algum serviço é um fator de risco bem alto se considerarmos que as APIs podem mudar e nossa aplicação pode parar de funcionar do dia para a noite. E pior, dependendo da mudança é necessário uma reestruturação muito grande em nosso código para se adequar a nova especificação. Pensando nisso que programamos seguindo padrões de design de código e boas práticas de programação. Ainda assim pode não ser suficiente se pensarmos de uma maneira um pouco mais macro em nossa aplicação, pois em algum ponto do nosso sistema ainda existirá uma “ponte” de integração com algum determinado serviço (entenda serviço como qualquer WebService, classe ou biblioteca que consultamos).

A solução é desacoplar o máximo possível nossa aplicação de tais serviços, e deixarmos nossa aplicações executar independente de como o serviço será consultado. Para obter esse nível de abstração é necessário programar e planejar o sistema pronto para se adequar as mudanças, e a melhor maneira de se programar para mudança é não programar. Parece um pensamento meio estranho, mas se analisarmos com cuidado veremos que existe uma certa razão para isso, pois quanto menos código tivermos, menos precisará ser mudado.

Spring Integration permite que criemos serviços de maneira totalmente desacoplada entre si e outros serviços Web, e disponibiliza as conexões em sua própria Bean Factory, ou seja, nosso sistema é livre de qualquer consulta a serviços ou API externa a aplicação. Vamos entender um pouco da arquitetura do Spring Integration.


Entendendo o que está acontecendo


A idéia por trás do spring integration é bem simples na verdade: todo serviço só distribui informação e recebe informação através de channels (canais). Canais são uma forma de acoplar os serviços, sem que estes dependam da API ou da maneira com que as chamadas são feitas.
Toda canal trafega mensagens. Mensagens possuem um cabeçalho e um conteúdo qualquer:


E um canal nada mais é do que o responsável por receber e disponibilizar tais mensagens a quem estiver interessado. Para ser mais direto, qualquer bean definido no contexto do spring tem acesso a tais canais.


Fica claro aqui que podemos ter vários tipos diferentes de canais com várias configurações e comportamentos diferentes. De fato o spring disponibiliza tais configurações e comportamentos out of the box. Veremos mais pra frente.

Exemplo prático


Vamos criar um exemplo para entender melhor os conceitos.
A primeira coisa a se fazer é baixar os jars do spring integration. Feito isso adicione-os ao classpath do seu projeto que deve ser um spring application padrão. Se você está usando o maven, basta adicionar as seguintes dependências:

1
2
3
4
5
6
7
8
9
10
11
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>3.0.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-core</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>


Estou assumindo que as dependências para o contexto do spring já estejam corretamente configuradas.

Agora em nosso arquivo de beans do spring, basta adicionarmos o seguinte namespace e schema location:

1
2
3
4
5
6
7
8
9
10
11
12
<?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:amq="http://activemq.apache.org/schema/core"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:si="http://www.springframework.org/schema/integration"
    xsi:schemaLocation="http://activemq.apache.org/schema/core http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/integration
        http://www.springframework.org/schema/integration/spring-integration-2.0.xsd"
>
</beans>


Podemos começar a configurar nossos serviços e canais agora.

Crie um serviço bem simples que recebe e guarda uma String, e disponibiliza-a em um getter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package br.com..services;

@Service("simpleService")
public class SimpleService {

    private String s;

    public void setString(String s) {
        this.s = s;
    }

    public String getS() {
        return s;
    }
}


Repare que estou usando a criação de beans por anotação, por isso tenho que colocar em meu arquivo de beans que o pacote de tal serviço deve ser “escaneado”:

1
    <context:component-scan base-package="br.com..services" />


Crie um teste unitário para nosso serviço, e certifique-se que a injeção dos beans está sendo feita corretamente e que o spring está corretamente configurado até aqui. Não é o foco deste artigo mostrar como configurar tal teste, por isso vou supor que as configurações já estão corretas e vou colocar o código do teste diretamente (estou usando JUnit 4):

1
2
3
4
5
6
    private SimpleService simpleService;

    @Test
    public void testChannels() {
        Assert.assertNotNull(this.simpleService);
    }


Vamos criar agora um canal de entrada e um canal de saída em nossos beans. Para isso basta adicionar o seguinte código em nosso xml de beans:

1
2
3
4
5
    <si:channel id="input" />

    <si:channel id="output">
        <si:queue capacity="10" />
    </si:channel>


Estou setando uma capacidade para nosso canal de saída para ilustrar uma das possíveis configurações do canal.

Até aqui nenhum segredo. Vamos agora disparar nosso serviço quando uma determinada mensagem for adicionada ao canal de entrada. É aqui que o spring integration se mostra muito flexível e poderoso. Não necessário nenhuma mudança no código para que essa conexão do serviço com o canal seja feita, basta adicionarmos um service-activator para fazer o trabalho. Para isso basta adicionar o seguinte bean ao xml:

1
2
3
    <si:service-activator input-channel="input"
        output-channel="output" ref="simpleService" method="setString">
    </si:service-activator>


Estamos dizendo que uma mensagem que chega no canal input, deve ter seu conteúdo enviado para o método setString no bean simpleService, e que o resultado deve ser adicionado no canal output.
Simples não? Vamos testar agora:

1
2
3
4
5
6
7
8
9
10
    private MessageChannel input;

    @Test
    public void testChannels() {
        Assert.assertNotNull(this.simpleService);

        input.send(new GenericMessage<string>("hello!"));

        Assert.assertEquals("hello!", this.simpleService.getS());
    }


Vamos mudar nosso serviço para devolver o valor que foi setado no métodosetString, com isso nosso service activator irá pegar o resultado e enviá-lo para o canal de saída:

1
2
3
4
    public String setString(String s) {
        this.s = s;
        return this.s;
    }


E agora conseguimos fazer o seguinte teste:

1
2
3
4
5
6
7
8
9
10
11
12
13
    private MessageChannel output;

    @Test
    public void testChannels() {
        Assert.assertNotNull(this.simpleService);

        input.send(new GenericMessage<string>("hello!"));

        Assert.assertEquals("hello!", this.simpleService.getS());

        Message<string> msg = (Message<string>) this.output.receive();
        Assert.assertEquals("hello!", msg.getPayload());
    }


Rode o teste e verifique que ele passa.
Vamos recapitular então o que vimos até agora. Criamos um serviço que é totalmente livre de qualquer API que apenas guarda uma String que é setada através do método setString. Criamos um canal de entrada (input) e um de saída (output) que guarda até 10 mensagens. Conectamos os canais com um service activator, que quando recebe uma mensagem no canal de entrada, invoca o método setString em nosso serviço e envia uma mensagem para o canal de saída com o valor que o serviço devolveu. Fizemos um teste que envia uma String para o canal de entrada, verifica que chegou até o serviço, e verifica que estava disponível no canal de saída.

Um fato interessante é que o método receive do canal de saída “trava” até que chegue uma nova mensagem, portanto estamos falando de uma arquitetura passiva baseadas em eventos que são disparados com o recebimento de mensagens. Muito bom!

Gateways


O conceito de Gateway existe para facilitar o envio de mensagens aos canais, estabelecendo um interface comum para que possam ser usados de maneira natural no código.
Vamos criar um gateway para invocar nosso serviço que guarda a String:

1
2
3
4
5
package br.com..services;

public interface SimpleGateway {
    void call(String s);
}


Pronto! Basta agora adicionarmos o gateway em nosso arquivo de beans:

1
2
    <si:gateway id="simpleGateway" default-request-channel="input"
        service-interface="br.com..services.SimpleGateway" />


Estamos dizendo aqui que nosso gateway irá despachar as mensagens para o canal input, e como o gateway recebe uma String, o Spring sabe que o conteúdo da mensagem que deve ser enviada é do tipo String. Vamos ao teste:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    private SimpleGateway simpleGateway;

    @Test
    public void testChannels() {
        Assert.assertNotNull(this.simpleService);

        input.send(new GenericMessage<string>("hello!"));

        Assert.assertEquals("hello!", this.simpleService.getS());

        Message<string> msg = (Message<string>) this.output.receive();
        Assert.assertEquals("hello!", msg.getPayload());

        this.simpleGateway.call("uma string");

        Assert.assertEquals("uma string", this.simpleService.getS());
    }


Próximos passos


Spring Integration é uma ferramenta muito poderosa, que permite criarmos aplicações de qualquer porte com uma arquitetura muito desacoplada facilitando a escalabilidade e reação a mudanças externas. Repare que sua aplicação ganha um aspecto de arquitetura voltada a eventos que são disparados através de mensagens, e que não há limitações de tipos e quantidades de conexões. Por exemplo, nosso canal de saída poderia sem problemas ser o canal de entrada para um outro serviço, ou até mesmo para um outro método do mesmo serviço. Também existem várias regras de encaminhamento de mensagens disponíveis no spring integration, como por exemplo um roteador de mensagens, um replicador de mensagens, um agregador, etc. Veremos alguns exemplos no futuro.

Existem muitos outros pacotes disponíveis como por exemplo o de integração com o twitter, o de integração com arquivos, o de integração com jms, dentre outros. Veremos tais integrações mais para frente.

Por @Gust4v0_H4xx0r

As imagens foram tiradas do próprio site do Spring Integration.

Comments

  • Mario Junior
    dezembro 9, 2010 Responder

    Muito bom!
    Esses dias estava lendo sobre isso mas ainda nao tinha “metido a mao na massa”.
    Fiquei com uma dúvida, e como to com preguiça de testar num ambiente, vou perguntar para vc 🙂 (hehe)

    Seria facilmente possível usar o Spring Integration para comunicação de Modules dentro de um ambiente OSGi (usando o Spring Modules)? Tipo, um modulo (A) comunicar-se com outro (B) atraves de um terceiro módulo (q atuasse como um middleware) … ou achas q nem precisaria tanto pra isso???

    Bom post, acho q é o primeiro em pt-BR q aborda o assunto.
    Abraço!

  • gustavo.moreira
    dezembro 10, 2010 Responder

    Bom dia Mario, obrigado pelo comentário!
    Seguinte, é possível sim fazer tal comunicação entre módulos seguindo uma arquitetura do spring-dm de maneira bem simples, se ao invés de usarmos interfaces dos serviços diretamente como osgi:services, usarmos um ‘gateway’ so pring integration que vimos no post. Dessa forma teríamos acesso aos canais do módulo que serviria de middleware e conseguimos seguir na execução normal. Na saída de nosso módulo intermediário podemos utilizar a mesma lógica e ativar o serviço osgi com um ‘service activator’, a partir de um canal de saída. Repare que não é necessário alterar nem o módulo (A) nem o módulo (B) para adicionar o módulo intermediário, essa é a maior vantagem do spring integration.

    Agora minha opinião sobre isso ser exagero ou não: depende. Pode ser que seja necessário adicionar essa lógica a mais dado que queremos desencadear tarefas adicionais entre as chamadas dos serviços, mas depende da complexidade do sistema.

    Espero ter ajudado, abraço!

Leave a Comment