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.