1. ¿Qué es una dependencia circular?
Ocurre cuando un bean A depende de otro bean B, y el bean B depende también del bean A:
Bean A → Bean B → Bean A
Por supuesto, podríamos tener más beans implícitos:
Bean A → Bean B → Bean C → Bean D → Bean E → Bean A
2. Qué sucede en primavera
Cuando el contexto de Spring está cargando todos los beans, intenta crear beans en el orden necesario para que funcionen por completo. Por ejemplo, si no tuviéramos una dependencia circular, como el siguiente caso:
Bean A → Bean B → Bean C
Spring creará el frijol C, luego creará el frijol B (e inyectará el frijol C en él), luego creará el frijol A (e inyectará el frijol B en él).
Pero, al tener una dependencia circular, Spring no puede decidir cuál de los beans debe crearse primero, ya que dependen unos de otros. En estos casos, Spring generará una BeanCurrentlyInCreationException mientras carga el contexto.
Puede suceder en Spring cuando se usa la inyección del constructor ; Si usa otros tipos de inyecciones, no debería encontrar este problema, ya que las dependencias se inyectarán cuando sean necesarias y no en la carga de contexto.
3. Un ejemplo rápido
Definamos dos beans que dependen uno del otro (mediante inyección de constructor):
@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; } }
Ahora podemos escribir una clase de configuración para las pruebas, llamémosla TestConfig , que especifica el paquete base para buscar componentes. Supongamos que nuestros beans están definidos en el paquete " com.baeldung.circulardependency ":
@Configuration @ComponentScan(basePackages = { "com.baeldung.circulardependency" }) public class TestConfig { }
Y finalmente podemos escribir una prueba JUnit para verificar la dependencia circular. La prueba puede estar vacía, ya que la dependencia circular se detectará durante la carga del 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 } }
Si intenta ejecutar esta prueba, obtendrá la siguiente excepción:
BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA': Requested bean is currently in creation: Is there an unresolvable circular reference?
4. Las soluciones
Mostraremos algunas de las formas más populares de tratar este problema.
4.1. Rediseñar
Cuando tiene una dependencia circular, es probable que tenga un problema de diseño y las responsabilidades no estén bien separadas. Debe intentar rediseñar los componentes correctamente para que su jerarquía esté bien diseñada y no haya necesidad de dependencias circulares.
Si no puede rediseñar los componentes (puede haber muchas razones posibles para eso: código heredado, código que ya ha sido probado y no se puede modificar, tiempo o recursos insuficientes para un rediseño completo ...), hay algunas soluciones para probar.
4.2. Utilice @Lazy
Una forma sencilla de romper el ciclo es decir que Spring inicialice uno de los beans de forma perezosa. Es decir: en lugar de inicializar completamente el bean, creará un proxy para inyectarlo en el otro bean. El bean inyectado solo se creará por completo cuando se necesite por primera vez.
Para probar esto con nuestro código, puede cambiar CircularDependencyA a lo siguiente:
@Component public class CircularDependencyA { private CircularDependencyB circB; @Autowired public CircularDependencyA(@Lazy CircularDependencyB circB) { this.circB = circB; } }
Si ejecuta la prueba ahora, verá que el error no ocurre esta vez.
4.3. Use Setter / Inyección de campo
Una de las soluciones más populares, y también lo que propone la documentación de Spring, es usar la inyección de setter.
En pocas palabras, si cambia las formas en que se conectan sus beans para usar la inyección de setter (o inyección de campo) en lugar de la inyección de constructor, eso soluciona el problema. De esta forma, Spring crea los beans, pero las dependencias no se inyectan hasta que se necesitan.
Hagamos eso: cambiemos nuestras clases para usar inyecciones de setter y agregaremos otro campo ( mensaje ) a CircularDependencyB para que podamos hacer una prueba unitaria adecuada:
@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; } }
Ahora tenemos que hacer algunos cambios en nuestra prueba unitaria:
@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()); } }
A continuación, se explican las anotaciones que se ven arriba:
@Bean : Para decirle al framework Spring que estos métodos deben usarse para recuperar una implementación de los beans para inyectar.
@Test : La prueba obtendrá el bean CircularDependencyA del contexto y afirmará que su CircularDependencyB se ha inyectado correctamente, verificando el valor de su propiedad de mensaje .
4.4. Utilice @PostConstruct
Otra forma de romper el ciclo es inyectar una dependencia usando @Autowired en uno de los beans y luego usar un método anotado con @PostConstruct para establecer la otra dependencia.
Nuestros frijoles podrían tener el siguiente 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; } }
Y podemos ejecutar la misma prueba que teníamos anteriormente, por lo que verificamos que la excepción de dependencia circular todavía no se esté lanzando y que las dependencias se hayan inyectado correctamente.
4.5. Implementar ApplicationContextAware e InitializingBean
Si uno de los beans implementa ApplicationContextAware , el bean tiene acceso al contexto Spring y puede extraer el otro bean de allí. Implementando InitializingBean indicamos que este bean tiene que realizar algunas acciones después de que se hayan establecido todas sus propiedades; en este caso, queremos configurar manualmente nuestra dependencia.
El código de nuestros frijoles sería:
@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; } }
Nuevamente, podemos ejecutar la prueba anterior y ver que no se lanza la excepción y que la prueba funciona como se esperaba.
5. En conclusión
Hay muchas formas de lidiar con las dependencias circulares en Spring. Lo primero a considerar es rediseñar sus beans para que no haya necesidad de dependencias circulares: suelen ser un síntoma de un diseño que se puede mejorar.
Pero si es absolutamente necesario tener dependencias circulares en su proyecto, puede seguir algunas de las soluciones sugeridas aquí.
El método preferido es usar inyecciones de incubadora. Pero hay otras alternativas, generalmente basadas en evitar que Spring administre la inicialización e inyección de los beans, y hacerlo usted mismo usando una estrategia u otra.
Los ejemplos se pueden encontrar en el proyecto GitHub.