Entendendo Spring Framework

Injeção de Dependência e Inversão de Controle

Esse artigo é baseado no livro: Pro Spring 5 da Iuliana Cosmina editora apress.

Esse artigo vai explicar como o Spring Framework funciona. Note que não se trata de Spring Boot, nem Spring MVC e nenhum outro projeto do ecossistema do Spring, mas sim do Spring Framework Core!

Vamos começar nossa jornada com um simples "Hello, World".

package com.willams.nttdata;

public class App  {
    public static void main( String[] args ) {
        System.out.println( "Hello World!" );
    }
}

O que você faria se precisasse mudar a mensagem? O que você faria se quisesse mostrar a mensagem de forma diferente, enviar para um arquivo em vez da tela ou encapsular a mensagem em tags HTML em vez de texto puro? E se você quisesse enviar para o a saída padrão de erro (stderr) em vez da saída padrão atual (stdout)?

O código acima não é extensível. Embora as mudanças citadas exija apenas poucas mudanças no código, a aplicação teria que ser recompilada sempre que algo for modificado. Para aplicações grandes, recompilar o código inteiro toda vez que for mudar uma simples mensagem é inapropriado.

Vamos tentar corrigir todos os problemas que podemos encontrar nesse "hello world" de tal forma que ele fique extensível e flexível e com menos acoplamento entre suas dependências. A primeira mudança mais simples de fazer é receber a mensagem a ser impressa em tempo de execução via argumentos:

package com.willams.nttdata;

public class App  {
    public static void main( String[] args ) {
         if(args.length > 0) {
            System.out.println(args[0]);
        } else {
            System.out.println("Hello World!");
        }
    }
}

O código funciona, mas ainda existem algumas melhorias a serem feitas. Note que o código responsável por obter a mensagem também é responsável por imprimir a mensagem. Então vamos mudar nosso código extraindo dele duas classes, uma responsável por fornecer a mensagem e outra responsável por mostrar a mensagem.

Primeiro, uma interface que define o comportamento de obter uma mensagem:

package com.willams.nttdata.message;

public interface MessageProvider {
    String getMessage();
}

Agora criaremos uma classe que "decidiu" ter o comportamento de obter uma mensagem (i.e. ela implementa a interface MessageProvider):

public class HelloWorldMessageProvider implements MessageProvider {
    private static final String HELLO_WORLD = "Hello, World!";

    @Override
    public String getMessage() {
        return HELLO_WORLD;
    }
}

Agora precisamos de uma classe que renderiza a mensagem. Seguiremos com a interface que declara esse comportamento e então uma classe que decide ter esse comportamento. Lembrando que, uma vez definido um comportamento (i.e. criado uma interface), podemos decidir ter esse comportamento em qualquer classe que quisermos.

Essa é uma boa maneira de entender interfaces: interfaces são comportamentos que uma classe pode decidir possuir! Pois não?

public interface MessageRenderer {
    void render();

    void setMessageProvider(MessageProvider messageProvider);
    MessageProvider getMessageProvider();

}

e

public class StandardOutputMessageRenderer implements MessageRenderer {

    private MessageProvider messageProvider;

    @Override
    public void render() {
        if(messageProvider == null) {
            throw new RuntimeException("Você deve setar a propriedade messageProvider");
        }

        System.out.println(messageProvider.getMessage());
    }

    @Override
    public void setMessageProvider(MessageProvider messageProvider) {
        this.messageProvider = messageProvider;
    }

    @Override
    public MessageProvider getMessageProvider() {
        return this.messageProvider;
    }
}

Note que poderíamos ter uma classe que 'renderiza' a mensagem para um arquivo, p.ex.:

public class FileOutputMessageRenderer implements MessageRenderer {
    ...
}

Agora nossa classe principal ficaria assim:

public class App  {
    public static void main( String[] args ) {
        MessageRenderer messageRenderer = new StandardOutputMessageRenderer();
        MessageProvider messageProvider = new HelloWorldMessageProvider();

        messageRenderer.setMessageProvider(messageProvider);
        messageRenderer.render();
    }
}

Esse código funciona bem, mas ainda temos um problema: sempre que mudarmos a implementação do renderer ou do provider isso implica em mudar o código. Tem uma maneira de resolver isso criando uma fábrica que lê o nome da implementação de um arquivo de propriedades e só então instancia na aplicação. P.ex.:

public class MessageSupportFactory {
    private static MessageSupportFactory instance;

    private Properties properties;
    private MessageRenderer renderer;
    private MessageProvider provider;

    static {
        instance = new MessageSupportFactory();
    }

    public MessageSupportFactory() {
        this.properties = new Properties();

        try {
            properties.load(this.getClass().getResourceAsStream("/message-factory.properties"));

            String rendererClass = properties.getProperty("message.renderer.class.name");
            String providerClass = properties.getProperty("message.provider.class.name");

            renderer = (MessageRenderer) Class.forName(rendererClass).newInstance();
            provider = (MessageProvider) Class.forName(providerClass).newInstance();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    public static MessageSupportFactory getInstance() {
        return instance;
    }

    public MessageProvider getMessageProvider() {
        return provider;
    }

    public MessageRenderer getMessageRenderer() {
        return renderer;
    }
}

Apesar da quantidade de código (java é bastante verboso), é um código simples. Essa classe permite obter instâncias de renderer e de provider cujo nome da implementação é obtido a partir de um arquivo .properties. Assim, caso você crie uma nova implementação (i.e. cria uma nova classe que decidiu ter o comportamento da interface de MessageProvider ou MessageRenderer) basta gerar um .class dessa implementação (i.e. compilando a mesma) e setar o nome dela na propriedade.

A aplicação usará a classe MessageSupportFactory assim:

public class App  {
    public static void main( String[] args ) {
        MessageRenderer messageRenderer = MessageSupportFactory.getInstance().getMessageRenderer();
        MessageProvider messageProvider = MessageSupportFactory.getInstance().getMessageProvider();

        messageRenderer.setMessageProvider(messageProvider);
        messageRenderer.render();
    }
}

Note que a única mudança é que agora não estamos obtendo as instâncias com new e sim via um factory.

Entender o que fizemos acima é fundamental para entender o Spring Framework. O Spring Framework tem como principal característica o suporte para injeção de dependências e inversão de controle. O problema que enfrentamos em nosso exemplo é solucionado pelo Spring de forma parecida com a nossa solução: O Spring fornece todo o suporte necessário para que nosso código não precise instanciar diretamente os objetos. Com a ajuda do Spring podemos facilmente injetar as dependências (cujo nome pode ser obtido a partir de um arquivo .xml) necessárias. Nossa aplicação, assim, quando precisa de uma dependência, pede para o spring injetar pra gente, nossa aplicação cede o controle ao Spring, daí o nome "inversão de controle". O próximo exemplo vai reescrever o código acima, mas dessa vez com o Spring Framework.