Dependências circulares na primavera

Dependências circulares na primavera

1. O que é uma dependência circular?

Isso acontece quando um feijão A depende de outro feijão B e o feijão B também depende do feijão A:

Feijão A → Feijão B → Feijão A

Obviamente, poderíamos ter mais beans implícitos:

Feijão A → Feijão B → Feijão C → Feijão D → Feijão E → Feijão A

2. O que acontece na primavera

Quando o contexto do Spring está carregando todos os beans, ele tenta criar beans na ordem necessária para que funcionem completamente. Por exemplo, se não tivéssemos uma dependência circular, como no seguinte caso:

Feijão A → Feijão B → Feijão C

O Spring criará o feijão C, depois criará o feijão B (e injetará o feijão C), depois criará o feijão A (e injetará o feijão B).

Porém, ao ter uma dependência circular, o Spring não pode decidir qual dos beans deve ser criado primeiro, pois eles dependem um do outro. Nesses casos, o Spring aumentará aBeanCurrentlyInCreationException enquanto carrega o contexto.

Isso pode acontecer na primavera ao usarconstructor injection; se você usar outros tipos de injeções, não deverá encontrar esse problema, pois as dependências serão injetadas quando forem necessárias e não no carregamento do contexto.

3. Um Exemplo Rápido

Vamos definir dois beans que dependem um do outro (via injeção do construtor):

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(CircularDependencyB circB) {
        this.circB = circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    @Autowired
    public CircularDependencyB(CircularDependencyA circA) {
        this.circA = circA;
    }
}

Agora podemos escrever uma classe de configuração para os testes, vamos chamá-la deTestConfig, que especifica o pacote base para fazer a varredura de componentes. Vamos supor que nossos beans sejam definidos no pacote “com.example.circulardependency”:

@Configuration
@ComponentScan(basePackages = { "com.example.circulardependency" })
public class TestConfig {
}

E, finalmente, podemos escrever um teste JUnit para verificar a dependência circular. O teste pode estar vazio, pois a dependência circular será detectada durante o carregamento do contexto.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {

    @Test
    public void givenCircularDependency_whenConstructorInjection_thenItFails() {
        // Empty test; we just want the context to load
    }
}

Se você tentar executar este teste, receberá a seguinte exceção:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?

4. As soluções alternativas

Mostraremos algumas das maneiras mais populares de lidar com esse problema.

4.1. Redesenhar

Quando você tem uma dependência circular, é provável que você tenha um problema de design e as responsabilidades não estejam bem separadas. Você deve tentar redesenhar os componentes adequadamente para que sua hierarquia seja bem projetada e não haja necessidade de dependências circulares.

Se você não conseguir reprojetar os componentes (pode haver várias razões possíveis para isso: código herdado, código que já foi testado e não pode ser modificado, tempo ou recursos suficientes para um reprojeto completo ...), existem algumas soluções alternativas a serem tentadas.

4.2. Use@Lazy

Uma maneira simples de interromper o ciclo é dizer que o Spring inicializa um dos grãos preguiçosamente. Ou seja: em vez de inicializar completamente o bean, ele criará um proxy para injetá-lo no outro bean. O feijão injetado será totalmente criado quando for necessário.

Para tentar isso com nosso código, você pode alterar o CircularDependencyA para o seguinte:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

Se você executar o teste agora, verá que o erro não ocorre desta vez.

4.3. Use Setter / Injeção de Campo

Uma das soluções alternativas mais populares, e tambémSpring documentation proposes, é usar a injeção de setter.

Simplificando, se você alterar a maneira como seus grãos são conectados para usar a injeção de setter (ou injeção de campo) em vez da injeção de construtor - isso resolve o problema. Dessa forma, o Spring cria os beans, mas as dependências não são injetadas até que sejam necessárias.

Vamos fazer isso - vamos mudar nossas classes para usar injeções de setter e adicionar outro campo (message) aCircularDependencyB para que possamos fazer um teste de unidade adequado:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

Agora temos que fazer algumas alterações no nosso teste de unidade:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {

    @Autowired
    ApplicationContext context;

    @Bean
    public CircularDependencyA getCircularDependencyA() {
        return new CircularDependencyA();
    }

    @Bean
    public CircularDependencyB getCircularDependencyB() {
        return new CircularDependencyB();
    }

    @Test
    public void givenCircularDependency_whenSetterInjection_thenItWorks() {
        CircularDependencyA circA = context.getBean(CircularDependencyA.class);

        Assert.assertEquals("Hi!", circA.getCircB().getMessage());
    }
}

O seguinte explica as anotações vistas acima:

@Bean: Para dizer ao framework Spring que esses métodos devem ser usados ​​para recuperar uma implementação dos beans a serem injetados.

@Test: o teste obterá o bean CircularDependencyA do contexto e afirmará que seu CircularDependencyB foi injetado corretamente, verificando o valor de sua propriedademessage.

4.4. Use@PostConstruct

Outra maneira de quebrar o ciclo é injetar uma dependência usando@Autowired em um dos beans e, em seguida, usar um método anotado com@PostConstruct para definir a outra dependência.

Nossos beans podem ter o seguinte código:

@Component
public class CircularDependencyA {

    @Autowired
    private CircularDependencyB circB;

    @PostConstruct
    public void init() {
        circB.setCircA(this);
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

E podemos executar o mesmo teste que fizemos anteriormente, para verificar se a exceção de dependência circular ainda não está sendo lançada e se as dependências foram injetadas corretamente.

4.5. ImplementeApplicationContextAware eInitializingBean

Se um dos beans implementaApplicationContextAware, o bean tem acesso ao contexto do Spring e pode extrair o outro bean de lá. ImplementandoInitializingBean, indicamos que este bean tem que fazer algumas ações após todas as suas propriedades terem sido definidas; neste caso, queremos definir manualmente nossa dependência.

O código dos nossos beans seria:

@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {

    private CircularDependencyB circB;

    private ApplicationContext context;

    public CircularDependencyB getCircB() {
        return circB;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        circB = context.getBean(CircularDependencyB.class);
    }

    @Override
    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
        context = ctx;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

Novamente, podemos executar o teste anterior e ver que a exceção não é lançada e que o teste está funcionando conforme o esperado.

5. Em conclusão

Há muitas maneiras de lidar com dependências circulares no Spring. A primeira coisa a considerar é redesenhar seus beans para que não haja necessidade de dependências circulares: elas geralmente são um sintoma de um design que pode ser aprimorado.

Mas se você absolutamente precisar de dependências circulares em seu projeto, poderá seguir algumas das soluções alternativas sugeridas aqui.

O método preferido é usar injeções de incubação. Mas existem outras alternativas, geralmente baseadas em impedir que o Spring gerencie a inicialização e a injeção dos beans e faça isso você mesmo usando uma estratégia ou outra.

Os exemplos podem ser encontrados emGitHub project.