Las dependencias circulares en Spring serán entrevistadas por Ali, Tencent y el 90% de ByteDance preguntó

Prefacio

La dependencia circular en Spring siempre ha sido un tema muy importante en Spring. Por un lado, porque el código fuente ha hecho mucho procesamiento para resolver la dependencia circular, por otro lado, es porque durante la entrevista, si haces preguntas de nivel superior en Spring, Entonces la dependencia circular no debe escapar. Si respondes bien, entonces este es tu nirvana. De todos modos, es el nirvana del entrevistador. Esta es también la razón de este título. Por supuesto, el propósito de este artículo es permitirte actuar en todas las entrevistas posteriores. ¡Un nirvana más, especialmente usado para matar al entrevistador!

La idea central de este artículo es,

Cuando el entrevistador pregunta:

"Por favor, hable sobre la dependencia circular en Spring".

¿Cómo debemos responder?

Principalmente dividido en los siguientes puntos

¿Qué es la dependencia circular?

¿En qué circunstancias se pueden manejar las dependencias circulares? ¿Cómo resuelve Spring las dependencias circulares? Al mismo tiempo, este artículo espera corregir varias afirmaciones erróneas sobre dependencias circulares que suelen aparecer en la industria.

Solo en el caso de inyección de setter, la dependencia circular se puede solucionar (incorrecto) El propósito de la caché de tercer nivel es mejorar la eficiencia (incorrecto) OK, la pavimentación se ha hecho, luego comenzamos el texto

¿Qué es la dependencia circular? Entendido literalmente que mientras A depende de B, B también depende de A, como se muestra a continuación

 

 

 

Reflejado en el nivel de código es así

 
  1. @Component

  2. public class A {

  3. // A中注入了B

  4. @Autowired

  5. private B b;

  6. }

  7.  
  8. @Component

  9. public class B {

  10. // B中也注入了A

  11. @Autowired

  12. private A a;

  13. }

Por supuesto, este es el tipo más común de dependencia circular, y hay más especiales.

 
  1. // 自己依赖自己

  2. @Component

  3. public class A {

  4. // A中注入了A

  5. @Autowired

  6. private A a;

  7. }

Aunque las manifestaciones son diferentes, en realidad son el mismo problema -----> dependencia cíclica

¿En qué circunstancias se pueden manejar las dependencias circulares?

Antes de responder a esta pregunta, primero debemos dejar en claro que Spring tiene condiciones previas para resolver dependencias circulares.

Los beans con dependencias circulares deben ser inyección de dependencia singleton, no todas las inyecciones de constructor (muchos blogs dicen que solo puede resolver la dependencia circular del método setter, lo cual es incorrecto) El primer punto debe entenderse bien. El segundo punto: ¿Qué significa no ser todo inyección de constructor? Todavía usamos código para hablar

 
  1. @Component

  2. public class A {

  3. // @Autowired

  4. // private B b;

  5. public A(B b) {

  6.  
  7. }

  8. }

  9.  
  10.  
  11. @Component

  12. public class B {

  13.  
  14. // @Autowired

  15. // private A a;

  16.  
  17. public B(A a){

  18.  
  19. }

  20. }

En el ejemplo anterior, la forma de inyectar B en A es a través del constructor, y la forma de inyectar A en B también es a través del constructor. En este momento, la dependencia circular no se puede resolver. Si tiene dos beans interdependientes en su proyecto , El siguiente error se informará al inicio:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

Para probar la relación entre la solución de la dependencia circular y el método de inyección, realizamos las siguientes cuatro pruebas

 

 

 

El código de prueba específico es simple, así que no lo dejaré pasar. De los resultados de la prueba anterior, podemos ver que la dependencia circular no solo se resuelve en el caso de la inyección del método setter, incluso en el caso de la inyección del constructor, la dependencia circular aún se puede procesar normalmente.

Entonces, ¿por qué exactamente? ¿Cómo trata Spring las dependencias circulares? No te preocupes, miremos hacia abajo

¿Cómo resuelve Spring las dependencias circulares?

La solución a las dependencias circulares debe discutirse en dos casos

Dependencia circular simple (sin AOP)

Combinando la dependencia circular de AOP Dependencia circular simple (sin AOP) Analicemos primero uno de los ejemplos más simples, que es la demostración mencionada anteriormente

 
  1. @Component

  2. public class A {

  3. // A中注入了B

  4. @Autowired

  5. private B b;

  6. }

  7.  
  8. @Component

  9. public class B {

  10. // B中也注入了A

  11. @Autowired

  12. private A a;

  13. }

A través de lo anterior, ya hemos sabido que la dependencia circular en este caso se puede solucionar, entonces ¿cuál es el proceso específico? Analizamos paso a paso

En primer lugar, debemos saber que cuando Spring crea Beans de forma predeterminada, se crean de acuerdo con el orden natural, por lo que el primer paso Spring creará A.

Al mismo tiempo, debemos saber que Spring se divide en tres pasos en el proceso de creación de Bean

Creación de instancias, método correspondiente: método createBeanInstance en AbstractAutowireCapableBeanFactory

Inyección de propiedades, método correspondiente: método PopulateBean de AbstractAutowireCapableBeanFactory

Inicialización, método correspondiente: initializeBean de AbstractAutowireCapableBeanFactory

Estos métodos se han explicado en detalle en los artículos anteriores de análisis de código fuente. Si no ha leído mi artículo antes, solo necesita saber

Creación de instancias, un entendimiento simple es que new tiene una inyección de atributo de objeto, llena la inicialización de atributo para el nuevo objeto en la instanciación, ejecuta los métodos en la interfaz consciente, el método de inicialización y completa el proxy AOP. Con base en el conocimiento anterior, comenzamos a interpretar todo el procesamiento de dependencia circular Todo el proceso debe comenzar con la creación de A. Como se mencionó anteriormente, el primer paso es crear A!

 

 

El proceso de creación de A en realidad está llamando al método getBean, que tiene dos significados

Crear un nuevo Bean. Obtener el objeto creado de la caché. Lo que estamos analizando ahora es el primer significado, ¡porque no hay A en la caché en este momento!

GetSingleton (beanName)

Primero llame al método getSingleton (a), este método llamará a getSingleton (beanName, true), en la figura anterior omití este paso

 
  1. public Object getSingleton(String beanName) {

  2. return getSingleton(beanName, true);

  3. }

El método getSingleton (beanName, true) es en realidad para intentar obtener el Bean del caché. El caché completo se divide en tres niveles

SingletonObjects, la caché de primer nivel, almacena todos los singleton Bean earlySingletonObjects creados, que son instanciados, pero las singletonFactories que aún no han sido inyectadas e inicializadas, son una fábrica de singleton expuesta de antemano. La caché de segundo nivel se almacena El objeto obtenido de esta fábrica se crea por primera vez porque A se crea por primera vez, por lo que no importa qué caché esté allí, ingresará otro método sobrecargado getSingleton (beanName, singletonFactory) de getSingleton.

Obtener Singleton (beanName, singletonFactory)

Este método se utiliza para crear Bean, y su código fuente es el siguiente:

 
  1. public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {

  2. Assert.notNull(beanName, "Bean name must not be null");

  3. synchronized (this.singletonObjects) {

  4. Object singletonObject = this.singletonObjects.get(beanName);

  5. if (singletonObject == null) {

  6.  
  7. // ....

  8. // 省略异常处理及日志

  9. // ....

  10.  
  11. // 在单例对象创建前先做一个标记

  12. // 将beanName放入到singletonsCurrentlyInCreation这个集合中

  13. // 标志着这个单例Bean正在创建

  14. // 如果同一个单例Bean多次被创建,这里会抛出异常

  15. beforeSingletonCreation(beanName);

  16. boolean newSingleton = false;

  17. boolean recordSuppressedExceptions = (this.suppressedExceptions == null);

  18. if (recordSuppressedExceptions) {

  19. this.suppressedExceptions = new LinkedHashSet<>();

  20. }

  21. try {

  22. // 上游传入的lambda在这里会被执行,调用createBean方法创建一个Bean后返回

  23. singletonObject = singletonFactory.getObject();

  24. newSingleton = true;

  25. }

  26. // ...

  27. // 省略catch异常处理

  28. // ...

  29. finally {

  30. if (recordSuppressedExceptions) {

  31. this.suppressedExceptions = null;

  32. }

  33. // 创建完成后将对应的beanName从singletonsCurrentlyInCreation移除

  34. afterSingletonCreation(beanName);

  35. }

  36. if (newSingleton) {

  37. // 添加到一级缓存singletonObjects中

  38. addSingleton(beanName, singletonObject);

  39. }

  40. }

  41. return singletonObject;

  42. }

  43. }

En el código anterior, captamos principalmente un punto: el Bean devuelto por el método createBean finalmente se coloca en la caché de primer nivel, que es el grupo de singleton.

Entonces aquí podemos sacar una conclusión: lo que se almacena en la caché de primer nivel es un bean singleton que ha sido completamente creado

Llamar al método addSingletonFactory

Como se muestra abajo:

 

 

Después de instanciar el Bean, Spring envuelve el Bean en una fábrica y lo agrega a la caché de tres niveles antes de la inyección de propiedad. El código fuente correspondiente es el siguiente:

 
  1. // 这里传入的参数也是一个lambda表达式,() -> getEarlyBeanReference(beanName, mbd, bean)

  2. protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {

  3. Assert.notNull(singletonFactory, "Singleton factory must not be null");

  4. synchronized (this.singletonObjects) {

  5. if (!this.singletonObjects.containsKey(beanName)) {

  6. // 添加到三级缓存中

  7. this.singletonFactories.put(beanName, singletonFactory);

  8. this.earlySingletonObjects.remove(beanName);

  9. this.registeredSingletons.add(beanName);

  10. }

  11. }

  12. }

Aquí solo se agrega una fábrica, se puede obtener un objeto a través del método getObject de esta fábrica (ObjectFactory), y este objeto en realidad se crea a través del método getEarlyBeanReference. Entonces, ¿cuándo se llamará al método getObject de esta fábrica? Esta vez está a punto de crear el proceso de B.

Cuando se crea una instancia de A y se agrega a la caché de tres niveles, es necesario iniciar la inyección de propiedades para A. Cuando se inyecta, se encuentra que A depende de B. Entonces Spring irá a getBean (b) nuevamente en este momento, y luego el setter de llamadas de reflexión El método completa la inyección de atributos.

 

 

Como B necesita inyectarse en A, cuando se crea B, se volverá a llamar a getBean (a). En este momento, volverá al proceso anterior, pero la diferencia es que el getBean anterior es para crear el Bean. Llamar a getBean no es para crearlo, sino para obtenerlo del caché, porque A lo puso en el caché de tercer nivel singletonFactories después de la instanciación, por lo que el proceso de getBean (a) es así en este momento

 

 

Desde aquí podemos ver que A inyectado en B es un objeto expuesto de antemano a través del método getEarlyBeanReference, no un Bean completo, entonces, ¿qué hace getEarlyBeanReference? Veamos su código fuente

 
  1. protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {

  2. Object exposedObject = bean;

  3. if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {

  4. for (BeanPostProcessor bp : getBeanPostProcessors()) {

  5. if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {

  6. SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;

  7. exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);

  8. }

  9. }

  10. }

  11. return exposedObject;

  12. }

En realidad, llama a getEarlyBeanReference del postprocesador, y solo hay un postprocesador que realmente implementa este método, que es el AnnotationAwareAspectJAutoProxyCreator importado a través de la anotación @EnableAspectJAutoProxy. En otras palabras, si no considera AOP, el código anterior es equivalente a:

 
  1. protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {

  2. Object exposedObject = bean;

  3. return exposedObject;

  4. }

Es decir, esta fábrica no tiene nada que hacer y devuelve directamente los objetos creados en la fase de instanciación. Entonces, ¿es útil la caché de tres niveles sin considerar AOP? Para ser razonable, es realmente inútil. ¿No está bien que coloque este objeto en la caché de segundo nivel? Si dice que mejora la eficiencia, entonces dígame dónde está la eficiencia mejorada.

Entonces, ¿qué hace exactamente la caché de tercer nivel? No se preocupe, primero terminemos todo el proceso y podrá darse cuenta del papel del caché de tercer nivel cuando analice la dependencia circular con AOP a continuación.

En este punto, no sé si mis amigos tendrán alguna pregunta. ¿Será un problema inyectar un objeto de tipo A no inicializado en B por adelantado?

Respuesta: no

En este momento, debemos completar el proceso de creación del Bean A, como se muestra en la siguiente figura:

 

 

 

De la figura anterior, podemos ver que aunque B se inyecta con un objeto A no inicializado por adelantado cuando se crea B, la referencia al objeto A inyectado en B siempre se usa en el proceso de creación de A, y luego A se inicializará en función de esta referencia, por lo que no hay ningún problema.

Combinado con la dependencia circular de AOP

Como hemos dicho antes, en el caso de dependencias circulares ordinarias, la caché de tres niveles no tiene ningún efecto. La caché de tres niveles está realmente relacionada con AOP en Spring. Echemos un vistazo al código de getEarlyBeanReference:

 
  1. protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {

  2. Object exposedObject = bean;

  3. if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {

  4. for (BeanPostProcessor bp : getBeanPostProcessors()) {

  5. if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {

  6. SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;

  7. exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);

  8. }

  9. }

  10. }

  11. return exposedObject;

  12. }

Si AOP está habilitado, entonces se llama al método getEarlyBeanReference de AnnotationAwareAspectJAutoProxyCreator, y el código fuente correspondiente es el siguiente:

 
  1. public Object getEarlyBeanReference(Object bean, String beanName) {

  2. Object cacheKey = getCacheKey(bean.getClass(), beanName);

  3. this.earlyProxyReferences.put(cacheKey, bean);

  4. // 如果需要代理,返回一个代理对象,不需要代理,直接返回当前传入的这个bean对象

  5. return wrapIfNecessary(bean, beanName, cacheKey);

  6. }

Volviendo al ejemplo anterior, si realizamos AOP proxy en A, getEarlyBeanReference devolverá un objeto proxy en lugar del objeto creado en la fase de creación de instancias, lo que significa que el A inyectado en B será un objeto proxy En lugar del objeto creado en la fase de instanciación de A.

 

 

Al ver esta imagen, es posible que tenga las siguientes preguntas

¿Por qué inyectar un objeto proxy al inyectar B? Respuesta: Cuando realizamos un proxy AOP para A, significa que queremos obtener el objeto proxy de A del contenedor en lugar de A en sí, por lo que cuando inyectamos A como una dependencia, también debemos inyectar su objeto proxy.

Obviamente, es el objeto A cuando se inicializa, entonces, ¿dónde coloca Spring el objeto proxy en el contenedor?

 

 

Después de completar la inicialización, Spring vuelve a llamar al método getSingleton. Esta vez los parámetros pasados ​​son diferentes. False puede entenderse como deshabilitar la caché de tres niveles. Como se mencionó en la figura anterior, se ha inyectado en B. La fábrica en la caché de tercer nivel se elimina y un objeto obtenido de la fábrica se coloca en la caché de segundo nivel, por lo que el tiempo para el método getSingleton aquí es obtener el objeto proxy A de la caché de segundo nivel. expuestoObjeto == bean se puede considerar verdadero, a menos que tenga que reemplazar el Bean en el proceso normal en el postprocesador de la fase de inicialización, por ejemplo, agregue un postprocesador:

 
  1. @Component

  2. public class MyPostProcessor implements BeanPostProcessor {

  3. @Override

  4. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

  5. if (beanName.equals("a")) {

  6. return new A();

  7. }

  8. return bean;

  9. }

  10. }

Sin embargo, no realice este tipo de operación de espectáculo, ¡ya que aumentará sus problemas!

Al inicializar, el objeto A en sí mismo se inicializa, y el contenedor y se inyecta en B son todos objetos proxy. ¿Será esto un problema? Respuesta: No, esto se debe a que si se trata de una clase de proxy generada por un proxy cglib o un proxy dinámico jdk, hay una referencia a la clase de destino en el interior. Cuando se llama al método del objeto de proxy, se llama al método del objeto de destino y A está completo La inicialización es equivalente a la inicialización del propio objeto proxy

¿Por qué la caché de tercer nivel usa fábricas en lugar de referencias directas? En otras palabras, ¿por qué necesita este caché de tercer nivel? ¿No puede exponer directamente una referencia a través del caché de segundo nivel? Respuesta: El propósito de esta fábrica es retrasar el proxy de los objetos generados en la etapa de instanciación. Solo cuando ocurra la dependencia circular, los objetos proxy se generarán por adelantado, de lo contrario solo se creará una fábrica y se colocará en el caché de tercer nivel. Pero en realidad no creará objetos a través de esta fábrica.

Consideremos una situación simple. Tomemos la creación de A por separado como ejemplo. Supongamos que no hay dependencia entre AB, pero A es proxy. En este momento, cuando se crea una instancia de A, ingresará el siguiente código:

 
  1. // A是单例的,mbd.isSingleton()条件满足

  2. // allowCircularReferences:这个变量代表是否允许循环依赖,默认是开启的,条件也满足

  3. // isSingletonCurrentlyInCreation:正在在创建A,也满足

  4. // 所以earlySingletonExposure=true

  5. boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&

  6. isSingletonCurrentlyInCreation(beanName));

  7. // 还是会进入到这段代码中

  8. if (earlySingletonExposure) {

  9. // 还是会通过三级缓存提前暴露一个工厂对象

  10. addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

  11. }

Como puede ver, incluso si no hay dependencia circular, se agregará a la caché de tercer nivel y debe agregarse a la caché de tercer nivel, porque hasta ahora Spring no puede determinar si este bean tiene una dependencia circular con otros beans. .

Suponiendo que usamos directamente el caché de segundo nivel aquí, significa que todos los Beans deben completar el proxy AOP en este paso. ¿Es esto necesario?

¡No solo es innecesario, sino que también viola el diseño de Spring de combinar el ciclo de vida de AOP y Bean! El ciclo de vida de la combinación de Spring de AOP y Bean se completa con el postprocesador de AnnotationAwareAspectJAutoProxyCreator, y el proxy AOP se completa para el Bean inicializado en el método postProcessAfterInitialization de este posprocesamiento. Si hay una dependencia circular, no hay forma, pero crear un proxy para el Bean primero, pero cuando no hay dependencia circular, el diseño es dejar que el Bean complete el proxy en el último paso del ciclo de vida en lugar de completar el proxy inmediatamente después de la instanciación. .

¿La caché de tres niveles realmente mejora la eficiencia?

Ahora ya conocemos el papel real de la caché de tercer nivel, pero esta respuesta puede que no te convenza, así que resumiremos y analizaremos la última ola, ¿la caché de tercer nivel realmente mejora la eficiencia? La discusión se divide en dos puntos:

La dependencia circular entre beans sin AOP se puede ver en el análisis anterior, en este caso, ¡la caché de tres niveles es inútil en absoluto! Así que no habrá ningún reclamo para mejorar la eficiencia

La dependencia circular entre beans AOP toma nuestro A y B como ejemplo, donde A es representado por AOP, primero analizamos el proceso de creación de A y B cuando se usa el caché de tres niveles

 

 

 

Suponiendo que no se utiliza la caché de tercer nivel, directamente en la caché de segundo nivel

 

 

 

La única diferencia entre los dos procesos anteriores es que el tiempo para crear un proxy para el objeto A es diferente. Cuando se usa el caché de tercer nivel, el momento para crear el proxy para A es cuando A debe inyectarse en B y si no se usa el caché de tercer nivel Una vez que se crea una instancia de A, debe crear un agente para A inmediatamente y colocarlo en la caché de segundo nivel. Para todo el proceso de creación de A y B, el tiempo consumido es el mismo

En resumen, no importa cuál sea la situación, ¡la afirmación de que la caché de tercer nivel mejora la eficiencia es incorrecta!

para resumir

Entrevistador: "¿Cómo resuelve Spring las dependencias circulares?"

Respuesta: Spring resuelve la dependencia circular a través de la caché de tres niveles. La caché de primer nivel es la agrupación de objetos singleton (singletonObjects), la caché de segundo nivel es earlySingletonObjects y la caché de tercer nivel es la fábrica de objetos de exposición temprana (singletonFactories). Cuando se produce una referencia circular entre A y B, después de que se crea una instancia de A, el objeto instanciado se usa para crear una fábrica de objetos y se agrega a la caché de tercer nivel. Si A es proxy de AOP, entonces a través de esta fábrica Lo que se obtiene es el objeto después del proxy A. Si A no es proxy de A, entonces lo que obtiene la fábrica es el objeto instanciado por A. Cuando A realiza la inyección de atributos, se crea B y B depende de A, por lo que cuando se crea B, se llamará a getBean (a) para obtener las dependencias requeridas. En este momento, getBean (a) se obtendrá de la caché. , El primer paso es obtener la fábrica en la caché de tercer nivel; el segundo paso es llamar al método getObject de la fábrica de objetos para obtener el objeto correspondiente y luego inyectarlo en B después de obtener este objeto. Luego, B finalizará su proceso de ciclo de vida, incluida la inicialización, el posprocesador, etc. Cuando se crea B, B se inyectará en A y A completará todo su ciclo de vida. En este punto, ¡la dependencia circular ha terminado!

Entrevistador: "¿Por qué utilizar una caché de tercer nivel? ¿Puede la caché de segundo nivel resolver dependencias circulares?"

Respuesta: Si desea utilizar el caché de segundo nivel para resolver la dependencia circular, significa que todos los Beans deben completar el proxy AOP después de la instanciación, lo que viola el principio del diseño de Spring. Al comienzo del diseño, Spring usó el AnnotationAwareAspectJAutoProxyCreator para realizar el Bean El último paso del ciclo de vida es completar el proxy AOP, en lugar de realizar el proxy AOP inmediatamente después de la instanciación.

Una pregunta

¿Por qué se puede resolver la dependencia circular del tercer caso en la siguiente tabla, pero no se puede resolver el cuarto caso?

Consejo: Spring crea Beans de forma predeterminada según el orden natural, por lo que A se creará antes que B

 

Supongo que te gusta

Origin blog.csdn.net/yuandengta/article/details/109258929
Recomendado
Clasificación