DEV Community

Cover image for Desvendando os Beans do Spring usando Kotlin
Fabiano Góes • e-Programar
Fabiano Góes • e-Programar

Posted on • Edited on

Desvendando os Beans do Spring usando Kotlin

Tenho visto pessoas usando o Spring Framework de forma totalmente mecânica, sem saber O QUE e O POR QUÊ decoram as classes com as Annotations: @Controller, @RestController, @Service, @Repository, @Component e etc.
Hoje vamos entender um pouco sobre o que acontece por traz dessas Annotations.

O que acontece na prática, é que decoramos que devemos estruturar nosso projeto com os seguintes packages: controller, service, repository.
image

E que toda classe no package controller deve ser anotada com @Controller ou @RestController.
Toda classe no package service deve ser anotada com @Service.
Toda classe no package repository deve ser anotada com @Repository.

E as classes de modelo que ficam no package model não precisam ser anotadas

Então, quando precisamos usar algumas dessas classes anotadas, usamos o mágico @Autowired.
Com isso em mente, usamos uma IDE ou o site Spring Initializr para criar nossos projetos, aplicamos a ideia acima de packages e Annotations e voilà, tudo funciona magicamente, bola pra frente, check no LinkedIn que manjo tudo de Spring Framework ;)
image

E de verdade, não vejo problema em começar assim, acho legal começar simplesmente codando e vendo as coisas funcionarem, mas acho que na sequencia, é preciso se esforçar um pouquinho para entender o que aconteceu e o que exatamente o Framework resolveu para nós.

Component Scan

Bom, dado o problema, vamos tentar esclarecer um pouco as coisas.
Quando criamos um projeto usando Spring como Framework Web, durante o Boot da aplicação existem alguns processos que o Spring executa para preparar esse ambiente, e um desses processo se chama "Component Scan".
Na prática, o Spring Escaneia nossos pacotes em busca de classes Anotadas com Estereótipos: @Component e suas especializações como @Controller, @RestController, @Service, @Repository.

Podemos usar a Annotation @ComponentScan para customizar onde queremos que o Spring procure classes Beans.

@ComponentScan("com.example.demo")
class MyCustomScan {}
Enter fullscreen mode Exit fullscreen mode

Cada classe que o Spring encontrar, ele vai registrar em um Container chamado "ApplicationContext", a partir daí essa classe virou um Bean.
O que ele faz é tentar instanciar essas classes e adicioná-las em uma lista de Bean (Spring-managed).
Então ele passa por todos os @Autowired e procura em sua lista de Beans algum Bean que equivalente.
Assim sua aplicação sobe com as classes magicamente instanciadas e Injetadas.

The IoC container

O Spring Framework aplica o princípio de Inversão de Controle IoC (Inversion Of Control). IoC também é conhecido como injeção de dependência (DI). É um processo pelo qual os objetos definem suas dependências, ou seja, os outros objetos com os quais trabalham, apenas por meio de argumentos do construtor, argumentos para um método de fábrica ou propriedades que são definidas na instância do objeto após serem construídas ou retornadas a partir de um método de fábrica. O contêiner injeta essas dependências quando criar o Bean. Esse processo é fundamentalmente o inverso, daí o nome Inversion of Control (IoC), do próprio Bean que controla a instanciação ou localização de suas dependências, usando a construção direta de classes ou um mecanismo como o padrão Service Locator.

Aqui vale uma observação importante: é uma boa prática programar para Interface e não para Implementações, assim mantendo suas classes com baixo acoplamento.
Então, onde queremos Injetar nossas classes com @Autowired criamos as variáveis ou parâmetros do tipo da Interface e não da Implementação, ai o Spring tem o trabalho de encontrar algum Bean que implemente aquela Interface para então injetar.


Exemplo prático

interface PersonService {
    fun findAll()
}
@Service
class PersonServiceImpl : PersonService {
    override fun findAll() {
        println("Returning all people")
    }
}
@RestController
@RequestMapping("/people")
class PersonController {

    @Autowired
    lateinit var personService: PersonService

    @GetMapping
    fun findAll() = personService.findAll()
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo criamos uma interface PersonInterface e implementamos essa interface através da classe PersonServiceImpl e anotamos a classe com @Service.
Então no controller PersonController pedimos para o Spring Injetar uma PersonService por meio do @Autowired.
Perceba que criamos a variável do tipo da Interface e não da classe e deixamos a critério do Spring encontrar em sua lista de Beans uma implementação equivalente.
Uma outra maneira de fazer essa Injeção de Dependência é usando o construtor invés de um atributo da classe, vamos ver como ficaria:

@RestControllerSpring-managed
@RequestMapping("/people")
class PersonController(val personService: PersonService) {

    @GetMapping
    fun findAll() = personService.findAll()
}
Enter fullscreen mode Exit fullscreen mode

O QUE aconteceu até aqui ?

Quando executarmos nossa aplicação o Spring começará a fazer sua magia.
Passando pelo processo de Component Scan, ele perceberá que temos nossa classe PersonService anotada com @Service, irá instanciar essa classe e registrá-la em seu ApplicationContext, tornando nossa classe um Bean Spring-managed.

Então ele continua o processo e encontra na classe PersonController anotada com @RestController, logo ele sabe que precisa Instanciar essa classe e registrá-la no ApplicationContext. Porém, essa classe tem uma dependência em seu Construtor e ele percebe que essa dependência é uma Interface, então ele percorre sua lista de Beans, em busca de alguém que implemente essa Interface. Encontra nossa implementação PersonServiceImpl e aplica a IoC | DI.

Perceba que não precisamos usar o @Autowired no construtor, e não teria problema em usar, funcionaria do mesmo jeito, mas o Spring é inteligente o suficiente para saber que para instanciar o PersonController ele precisa Injetar o PersonService que é uma dependência do construtor, até porque usamos val que irá criar um atributo da classe somente leitura e imutável, então é obrigatório a passagem do valor neste momento.
A partir daí o Contexto do Spring é carregado com nossas classes instanciadas e quem está controlando o ciclo de vida desses objetos é o próprio Spring.
Se você está atento a tudo o que aconteceu você deve ter se perguntado:
Mas e se eu tiver duas implementações da Interface PersonService, como o Spring saberia qual Injetar ???? :(

Vamos criar mais uma implementação e testar esse comportamento do Spring:

@Service
class CustomPersonServiceImpl : PersonService {
    override fun findAll() {
        println("CustomPersonServiceImpl - Returning all people")
    }
}
Enter fullscreen mode Exit fullscreen mode

Se executarmos nossa aplicação, veja o comportamento do Spring:
image

Veja que foi lançado um Erro ao iniciar o ApplicationContext, ele encontrou nossas duas implementações e não soube o que fazer. Neste caso precisamos dizer para ele o que queremos, e perceba que ele até sugeriu uma solução que é anotar uma de nossas implementações com @Primary. Vamos fazer isso anotando nossa primeira implementação com @Primary.

@Primary
@Service
class PersonServiceImpl : PersonService {
    override fun findAll() {
        println("Returning all people")
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora se executarmos novamente nossa aplicação tudo estará funcionando.
Mas e quando precisarmos que ele Injete nossa segunda implementação??

@RestController
@RequestMapping("/custom-people")
class CustomPersonController(
        @Qualifier("customPersonServiceImpl") val personService: PersonService
) {
    @GetMapping
    fun findAll() = personService.findAll()
}
Enter fullscreen mode Exit fullscreen mode

Criei um novo controller CustomPersonController e nele quero que seja injetado nossa segunda implementação de PersonService - CustomPersonServiceImpl.
Para isso usei a Annotation @Qualifier, passando como value o nome da implementação começando com LowerCase @Qualifier(“customPersonServiceImpl”).
Isso é possível porque quando o Spring está registrando os Beans no ApplicationContext, ele usa o próprio nome da classe começando com LowerCase, uma prática muito comum utilizado por qualquer programador que vem de Java.

Se quisermos customizar o nome de nossos Beans podemos passar como value da Annotation @Service(“myCustomPersonServiceImpl”) e depois usamos este mesmo nome no @Qualifier(“myCustomPersonServiceImpl”).

Poderíamos ir um pouco além do convencional, imagine que vc tem classes de domínio que você gostaria que o Spring usasse como um Bean Spring-managed, porém, você não quer acoplar seu domínio a nenhum Framework, então não quer anotar nenhum de suas classes de domínio com @Service ou @Component.

Vamos resolver isso:

interface PersonService {
    fun findAll()
}
class PersonServiceImpl : PersonService {
    override fun findAll() {
        println("PersonServiceImpl Returning all people")
    }
}

class CustomPersonServiceImpl : PersonService {
    override fun findAll() {
        println("CustomPersonServiceImpl - Returning all people")
    }
}
Enter fullscreen mode Exit fullscreen mode

Tiramos as Annotations @Service e agora nossas classes de Serviço/Domínio não estão acopladas ao Spring.
Mas queremos que em algum momento, o Spring registre elas no ApplicationContext para ser injetada em algum lugar fora da nossa camada de Domínio, por exemplo na camada de Controller.

@Configuration
class PersonConfiguration {

    @Primary
    @Bean
    fun personServiceImpl(): PersonService = PersonServiceImpl()

    @Bean
    fun customPersonServiceImpl(): PersonService =    CustomPersonServiceImpl()
}
Enter fullscreen mode Exit fullscreen mode

Criamos uma classe de configuração anotando com @Configuration, onde usamos a Annotation @Bean sobre uma function que tem como tipo de retorno nossa Interface.
Só que em cada um retornamos uma instância diferente e, por convenção, usamos o nome da function como o nome da classe começando com LowerCase, usando a mesma regra do Spring quando está registrando os Beans em seu ApplicationContext.

Então, agora quando acontecer o "Component Scan" ele perceberá que tem uma classe @Configuration que está solicitando para o Spring register alguns Beans no ApplicationContext.
A partir daí tudo volta a funcionar, mas resolvemos o problema de acoplamento do Framework em nossas classes de domínio e perceba que usamos novamente a Annotation @Primary para direcionar o Spring que quando ele encontrar uma Injection da nossa Interface PersonService sem um @Qualifier ele deve priorizar o Bean personServiceImpl.

Para finalizar, quero só explicar que as Annotations @Controller, @RestController, @Service, @Repository são todas especializações de @Component:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
  ...
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
  ...
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
   ...
}
Enter fullscreen mode Exit fullscreen mode

O que são apenas stereotype para deixar mais explícito o que as classes anotadas com elas quer tratar:

  • Service = Business
  • Repository = Data
  • Controller = Web

E em alguns casos, por exemplo, @Repository o Spring vai tratar diferente as Exceptions.
Uma vantagem de usar esta anotação é que ela possui a tradução automática de exceção de persistência ativada.
E ao usar uma estrutura de persistência como o Hibernate, as exceções ativas lançadas usarão classes anotadas com @Repository para lançar automaticamente convertidas em subclasses do DataAccessExeption do Spring.


O POR QUÊ devemos usar tudo isso ?

Isso é importante para podermos tirar um maior proveito do Framework que estamos utilizando.
Assim, usamos o Framework para resolver problemas de implementação de alguns Patterns, por exemplo MVC, IoC/DI, DAO/Repository, e ainda deixamos a cargo do Framework a responsabilidade de gerenciar o Ciclo de Vida dos nossos Objetos/Beans.

Com isso conseguimos deixar nosso código mais coeso e menos acoplado melhorando o Design do Software e ainda facilitando a implementação de Tests Automatizados.
Se entrarmos mais a fundo na questão do Spring Boot em volta de tudo isso temos muito mais problemas resolvidos tornando nosso dia-a-dia como Desenvolvedores muito mais produtivo.

Bom, se você chegou até aqui você foi muito guerreiro porque foi um Post bem teórico, mas como mencionei lá no começo, alguns momentos é preciso entender O QUE estamos fazendo e O POR QUÊ estamos fazendo determinadas coisas em nossos sistemas.

Espero que este Post seja útil para melhorar seu entendimento sobre Spring Framework.

Um abraço e até a próxima.


Referencias

java #kotlin #spring #springframework #springboot #jvm #beans

Top comments (2)

Collapse
 
thiagojacinto profile image
Thiago Jacinto

Obrigado por compartilhar, Fabiano!

Collapse
 
fabianogoes profile image
Fabiano Góes • e-Programar

👊🏼🤘🏼