Spring Container: O que eu considero primordial quando se trata de entender o funcionamento do Spring Framework
Pedro Nandi
Posted on July 4, 2023
Você estuda ou trabalha com Java? Usa o Spring Framework? Você entende como o Spring funciona e como ele faz para conectar tudo por trás dos panos? Caso você entenda, talvez esse artigo não te traga nenhuma novidade. Mesmo assim, você pode usá-lo para relembrar algumas coisas e até para me corrigir, caso eu tenha escrito alguma bobagem.
Decidi escrever esse artigo porque notei que muita gente utiliza o Spring e não entende muito bem como ele funciona. Ou até entende, porém aprendeu lendo aqui e ali e levou tempo até que as coisas fizessem sentido. É difícil achar algo mais compilado que agrupe explicações e conceitos que se relacionam e sejam tão importantes.
Pontuado isso, seguimos! O que você lembra ou sabe sobre orientação à objetos em Java? Se você sabe diferenciar uma classe de um objeto, entende o que é e como funciona herança, polimorfismo e interfaces, ótimo! Sensacional! Se você não sabe, sugiro que procure entender cada uma dessas coisas antes de continuar essa leitura. Porém, decidi começar esse artigo com uma breve revisão sobre o que é e como funciona o polimorfismo. Pois acho um conceito fundamental para entendermos tudo que vem pela frente.
O que é o polimorfismo dentro da orientação à objetos? Vamos logo com exemplos: Imagine que temos uma aplicação qualquer que cadastra um novo cliente, inicialmente inativo. Após o cadastro é possível ativá-lo e essa ativação dispara uma notificação via e-mail. Uma implementação básica para isso seria a seguinte:
Funcionalidade entrege, bacana. Agora, o cenário mudou e o cliente precisa ser notificado via SMS, após a ativação. O que você faria? Refatoraria o código trocando a NotificacaoEmail por uma NotificacaoSMS? Ok. Mas e se depois tiver que mudar novamente? Ficaremos reféns disso? Complicado, concorda?
Aí entra o poderoso uso do polimorfismo através das interfaces. Explico melhor... Imagine que criamos uma interface chamada Notificacao com a declaração de um método notificar(). Toda classe que implementar essa interface será obrigada a fornecer uma implementação deste método. Dito isso, imagine que nossas classes NotificacaoEmail e NotificacaoSMS passem a implementar essa interface. Isso ficaria assim:
Agora, nossa classe Cliente não precisa mais ter de forma imperativa qual o tipo de notificação a ser utilizada. Cliente pode, simplesmente, possuir um atributo do tipo Notificacao que vai chamar o método notificar():
Dessa forma, Cliente passa a ser agnóstica quanto ao tipo de notificação a ser utilizada. Quem instancia um objeto cliente poderá decidir qual o tipo de notificação lhe convém:
Cliente pedro = new Cliente('Pedro', '123', false, notificacaoSMS);
Cliente joao = new Cliente('João', '456', false, notificacaoEmail);
Através desse exemplo simples, eu consigo trazer aqui 3 conceitos básicos: Polimorfismo, injeção de dependência e inversão de controle. Vamos com calma, um por um:
Polimorfismo: Quando uma ou mais classes implementam uma mesma interface, a interface passa a representar a classe. Quando um método tem como parâmetro de entrada uma interface, qualquer objeto de classe que implementa a mesma pode ser passado como sendo esse parâmetro. No exemplo, como declaramos um atributo Notificacao na classe Cliente, o método construtor do Cliente passa a receber uma Notificacao como parâmetro. Ou seja, nos permitindo informar tanto NotificacaoSMS quanto NotificacaoEmail;
Injeção de dependência: No exemplo, Cliente passa a ter uma Notificacao como dependência. Ou seja, se na construção de um objeto Cliente não for informada uma forma de notificação, Cliente não poderá ser instanciado. Como essa dependência está vindo via construtor, dizemos que ela está sendo injetada. Ou seja, está ocorrendo uma injeção da dependência;
Inversão de controle (IoC): Notificacao é uma dependência do Cliente. Nos nossos primeiros exemplos, Cliente instanciava um NotificacaoEmail. Ou seja, Cliente tinha controle sobre qual o tipo de notificação estava usando. Era via e-mail. Quando passamos a usar o polimorfismo através da interface Notificacao, observamos a classe Cliente e vemos que não tem mais como saber qual o tipo de notificação a mesma utiliza. Agora, depende de quem instancia um cliente. Ou seja, Cliente perdeu o controle sobre o tipo de notificação. Esse controle agora pertence a quem instancia. Ou seja, houve uma inversão de controle da dependência.
Note que esses 3 conceitos são amarrados. Não tem como falar de um sem falar dos outros dois. Para que exista inversão de controle, tem que haver injeção de dependência e isso só ocorre, de forma efetiva, via polimorfismo. Como diz um caro colega: Não é magia, é tecnologia.
E que diabos o Spring tem a ver com isso??
O coração do Spring funciona através de inversão de controle e injeção de dependências, meu caro. Quando subimos uma aplicação Spring, uma das primeiras etapas executadas pelo framework é a chamada Component Scanning.
O Spring busca todos os beans que deve gerenciar a partir da classe anotada com @SpringBootApplication. Pera, o que significa tudo isso?
Quando o Spring inicia, ele instancia uma penca de objetos dentro da aplicação. Objetos de todo o tipo que vão auxiliar o spring a ter acesso a informações que ele precisa pra funcionar e se localizar. Esses objetos são chamados de beans. Esses beans podem ser de todas as formas e existem várias anotações dentro do Spring que evidenciam se uma classe vai gerar um bean. Exemplos: @Bean, @Component, @Controller, @Service, @Repository, @Entity, @Configuration... Tudo exemplo de bean.
Geralmente, a classe que inicia a aplicação, aquela que possui o método main(), é anotada com @SpringBootApplication. Ou seja, quando essa classe executa, o Spring incia o processo de component scanning, buscando todas as classes que possuem anotações que representam beans. Ele instancia objetos de todas essas classes antes de começar a executar a aplicação propriamente dita.
Aqueles beans que não possuem dependências, ou seja, que não possuem parâmetros no construtor, o Spring sobe com tranquilidade. Quando um bean que precisa subir depende de receber parâmetros via construtor, é necessário que seja implementada uma classe com @Configuration:
No exemplo acima, o Spring entende que NotificacaoSMS será instanciada a partir do método notificacaoSMS da NotificacaoSMSConfig. Sendo assim, a classe NotificacaoSMS não precisará mais de @Component.
Quando uma classe possui mais de um método construtor, usamos o @Autowired para informar ao Spring qual é o construtor que deve ser utilizado para instanciar a classe. Curiosidade: @Autowired(required = false) torna a dependência (a ser injetada), opcional. Ou seja, se ela não for instanciada, não ocorrerá NullPointerException.
O objetivo desse texto foi tentar mostrar que o Spring só consegue performar o component scanning por conta do uso daqueles três conceitos: Polimorfismo, injeção de dependência e inversão de controle. É comum ler em vários lugares que o Spring utiliza muito IoC, mas não é tão comum ler como isso funciona na prática. O component scanning é uma das primeiras etapas executadas pelo Spring ao ser iniciado e é a base para que o Spring tenha controle e noção do que precisa fazer e de onde cada parte da aplicação está localizada e como se relaciona. É parte importante do famoso "por trás dos panos".
Espero ter ajudado com essa minha explicação fajuta e pretendo voltar com mais textos continuando esse assunto. Podemos falar de ambiguidade de beans, nesse processo, quem sabe. Valeu!
Posted on July 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.