Uso correto dos Beans no Spring Boot

Comecei a estudar Spring Boot recentemente e percebi que em muitos tutoriais,
inclusive em alguns exemplos oficiais, é mostrado o uso de Bean da
seguinte forma:

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class MyApp {
  public static void main(String... args) {
    SpringApplication.run(MyApp.class, args);
  }

  @Bean
  public String message() {
    return "Hello!!!";
  }

  @Bean
  public CommandLineRunner print() {
    return args -> System.out.println(message());
  }
}

O Bean message é definido e usado dentro da mesma classe e até aí tudo bem.

Mas percebeu que ele é explícitamente invocado em print?

Da forma como entendo isso é errado, pois o propósito de um Bean é ser gerenciado
pelo container, logo, eu não deveria invocá-lo “manualmente” como no exemplo acima.

A forma correta, na minha opinião, é injetá-lo como uma dependência, seguem 2 exemplos:

Exemplo 1: Injetando por parametro no método print

@SpringBootApplication
public class MyApp {
  public static void main(String... args) {
    SpringApplication.run(MyApp.class, args);
  }

  @Bean
  public String message() {
    return "Hello!!!";
  }

  @Bean
  public CommandLineRunner print(final String message) {
    return args -> System.out.println(message);
  }
}

Exemplo 2: Usando @Autowired

@SpringBootApplication
public class MyApp {
  public static void main(String... args) {
    SpringApplication.run(MyApp.class, args);
  }

  @Autowired
  private String message;

  @Bean
  public String message() {
    return "Hello!!!";
  }

  @Bean
  public CommandLineRunner print() {
    return args -> System.out.println(this.message);
  }
}

Aí eu te pergunto:

  • Qual é sua opinião sobre a invocação explícita de Beans?
  • Fazer isto é, de alguma forma, prejudicial ou tanto faz?
  • Vc também já isso acontecer?
  • Há casos de uso em que esta prática é justificavél?

Também acho bem errado fazer dessa forma, mas nesse caso, acredito que não seja problema por se tratar de uma String.

O exemplo é ruim por não ser um caso real. Mas no geral não adicione complexidade sem necessidade. Que pode ser esse caso, em que o foco do exemplo nao é esse.

Ah sim, @Lucas_Camara, é que eu usei uma String pra simplificar o exemplo, mas poderia ser qualquer outro tipo de dado.

Há algum tipo de dados que vc considera particularmente problemático?

@javaflex eu não expliquei bem, mas eu espera que o exemplo fosse usado como base pra considerarmos casos reais na discussão.

Como eu não tenho conhecimento prático, é mais dificil imaginar possiveis casos reais, por isso decidi trazer a discussão até vcs.

Ainda não é um caso real, mas vejam o exemplo abaixo que é um pouco mais complexo:

https://github.com/spring-projects/spring-security/blob/5.3.0.RELEASE/samples/boot/oauth2authorizationserver/src/main/java/sample/AuthorizationServerConfiguration.java

Na linha 133 é definido o Bean tokenStore que é invocado explicitamente na linha 123. Este exemplo faz mais sentido?

É um comportamento um tanto quanto estranho, porém deve ter suas justificativas apesar de serem desconhecidas por mim!

Eu costumo criar os meus @Bean dentro de classes @Configuration, mantendo tudo bem separado e organizado, não gosto muito da abordagem utilizada no exemplo, embora seja funcional e boa parte da comunidade utilize.

@Jonathan_Medeiros essa era outra coisa que eu queria saber mesmo!

Se eu deveria colocar os Beans numa classe separada. Interessante saber que vc faz isso.

No caso é vc que prefere fazer isso ou essa é uma recomendação geral da comunidade?

Gente, outro exemplo.

No exemplo a pessoa criou uma variável de instancia chamada jwtAccessTokenConverter (linha 36) e um Bean com o mesmo nome (linha 69). E na linha 52 o Bean é invocado explícitamente.

E além disso, vejam a implementação do Bean:

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
  if (jwtAccessTokenConverter != null) {
    return jwtAccessTokenConverter;
  }
  
  SecurityProperties.JwtProperties jwtProperties = securityProperties.getJwt();
  KeyPair keyPair = keyPair(jwtProperties, keyStoreKeyFactory(jwtProperties));
  
  jwtAccessTokenConverter = new JwtAccessTokenConverter();
  jwtAccessTokenConverter.setKeyPair(keyPair);
  return jwtAccessTokenConverter;
}

Ele faz uma checagem pra ver se a variavel de instancia é nula ou não, mas o container já não faz isso por nós?

Eu aprendi muito seguindo o tutorial desse cara, pra quem tiver interesse, o artigo é esse:

Mas essa essa questão ficou na minha cabeça, por isso trouxe pra cá.

Tem gente que prega que isso é só perfumaria aplicada no código (talvez seja afinal a nível funcional as duas formas atendem), porém nos materiais que tenho acompanhado, tanto da Pivotal como também da Algaworks desde que comecei a trabalhar com Spring Boot, eles dizem ser uma boa prática fazer essa separação em diversas classes conforme necessidade, por questões de organização mesmo, isso melhora significativamente a legibilidade do código.

Nada contra com quem faz da forma como está no exemplo, mas prefiro da forma como citei acima :slightly_smiling_face:

Depende muito do contexto também, talvez gere mais complexidade criar uma classe @Configuration para definir um @Bean e depois injetá-lo onde será usado, do que defini-lo na própria classe, já que só será utilizado ali naquele ponto, como é a questão dos exemplo que tu enviou acima de um AuthorizationServer.

O Spring é uma framework que permite plugar outras bibliotecas/frameworks, e isso é feito utilizando as @Configurations. Em vez de soh adicionar a dependencia no seu projeto e sair dando new nas classes, vc pode utilizar elas através do contexto adicionando essas instancias por meio dessas classes de configuração.

Outra coisa é poder definir pré configurações nos bean no momento em que o mesmo é criado, deixando centralizado. E até mesmo sobrescrever atributos de beans existentes e fornecendo-os com outros nomes.

Tenho um projeto no github que é para tratar o conceito de HATEOAS em projetos spring. Toda a lógica eu faço independente do Spring (ainda estou desenvolvendo, não está tão maduro) e, para plugar, eu tenho uma Configuration para fazer esse trabalho.

Acho que é bem por aí mesmo então, @Jonathan_Medeiros. Se vc sabe que o @Bean vai ser usado só em um lugar, não há por quê criar um @Configuration separado.

Então @Lucas_Camara, eu entendo o propósito dos @Bean, dos @Configuration e outros @Component, a questão é focada na forma como algumas pessoas usam os @Bean invocando-os explicitamente e checando seus valores explicitamente como mostrei nos exemplos.

No caso, os código funcionam perfeitamente, mas será que podem ocorrer situações negativas ao fazer esse tipo de coisa?

Neste caso do jwtToken concordo em usar @Autowired. Pra aquele exemplo da String seria firula. E não tendo esse recurso simples do @Autowired, eu instancia na mão mesmo dentro própria classe, independente de “boas práticas” que complicariam mais a programação.

1 curtida

Por padrão, os beans spring são singleton. Se vc expoe um bean:

@Bean
public MeuBean meuBean() {
    return new MeuBean();
}

E ao injetar assim:

@Autowired
private MeuBean meuBean; 

E chamar o método diretamente dentro da configuration:

@Bean
public OutroBean outroBean() {
    return new OutroBean(meuBean());
}

Se vierem instancias diferentes de MeuBean, ai eu entendo que é um erro. Não sei dizer o resultado desse cenário, pois nunca fiz dessa forma. Sempre que um método de uma configuration precisa de um outro bean da mesma configuration, eu coloco esse outro bean como parâmetro e ficava tudo certo.

Acabei de fazer este teste, @Lucas_Camara.

Primeiro eu pesquisei rapidamente e encontrei esta resposta:

https://stackoverflow.com/a/27990468/3334365

Que explica que as chamadas a Beans normais passam pela CGLIB e nisso, se uma instancia for encontrada, ela é que será usada, nenhuma nova instancia será criada.

Só que isso só vale quando o escopo do Bean é SCOPE_SINGLETON que é o padrão, se mudarmos para SCOPE_PROTOTYPE novas instancias serão criadas.

Há outros escopos, mas não testei porque minha dúvida já foi respondida com o teste que fiz. Eis o código que usei pra testar:

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

class Test {
  private final String type;
  private static final Map<String, Integer> counter = new HashMap<>();

  Test(final String type) {

    if (!counter.containsKey(type)) {
      counter.put(type, 0);
    }

    final int count = counter.get(type) + 1;
    counter.put(type, count);

    this.type = type + " " + count;

    System.out.printf("\n\033[32mCriando Test [%s] (%d)\033[0m\n", type, count);
  }

  @Override
  public String toString() {
    return super.toString() + " " + this.type;
  }
}

@SpringBootApplication
public class AppTestApplication {
  public static void main(String... args) {
    SpringApplication.run(AppTestApplication.class, args);
  }

  @Bean // O escopo é ConfigurableBeanFactory.SCOPE_SINGLETON por padrão
  public Test aSimpleBean() {
    return new Test("aSimpleBean");
  }

  @Bean
  @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  public Test aPrototypeScopedBean() {
    return new Test("aPrototypeScopedBean");
  }

  @Bean
  public static Test aStaticBean() {
    return new Test("aStaticBean");
  }

  @Bean
  public CommandLineRunner print1(final Test aSimpleBean, final Test aPrototypeScopedBean, final Test aStaticBean) {
    return args -> {
      System.out.printf("Test 1: %s\n", aSimpleBean);
      System.out.printf("Test 2: %s\n", aPrototypeScopedBean);
      System.out.printf("Test 3: %s\n", aStaticBean);
    };
  }

  @Bean
  public CommandLineRunner print2(final Test aSimpleBean, final Test aPrototypeScopedBean, final Test aStaticBean) {
    return args -> {
      System.out.printf("Test 1: %s\n", aSimpleBean);
      System.out.printf("Test 2: %s\n", aPrototypeScopedBean);
      System.out.printf("Test 3: %s\n", aStaticBean);
    };
  }

  @Bean
  public CommandLineRunner print3(final Test aSimpleBean, final Test aPrototypeScopedBean, final Test aStaticBean) {
    return args -> {
      System.out.printf("Test 1: %s %b\n", aSimpleBean, aSimpleBean == aSimpleBean());
      System.out.printf("Test 2: %s %b\n", aPrototypeScopedBean, aPrototypeScopedBean == aPrototypeScopedBean());
      System.out.printf("Test 3: %s %b\n", aStaticBean, aStaticBean == aStaticBean());
    };
  }
}

Com este teste constatei que:

  1. Quando o @Bean é comum (SCOPE_SINGLETON) não há mal em misturar injeções (por parametro ou com @Autowired) e chamadas explicitas, pois a mesma instancia será usada sempre.
  2. Quando for um Bean estático a mesma instancia será usada nos lugares em que vc o injetar, porém, se vc invocar o Bean explicitamente, uma nova instancia será criada.
  3. Quando o Bean for SCOPE_PROTOTYPE uma nova instancia será criada cada vez que for injetado ou chamdo explicitamente, nenhuma instancia é reaproveitada.
1 curtida