天天看點

Spring(SpringBoot)--解決@Configuration的循環依賴問題

簡介

問題來源

        前幾天遇到一個循環依賴問題,是RedissonClient這個bean引起的。RedissonClient是由一個配置類(@Configuration注解的類)提供的,這配置類在初始化時(@PostConstruct注解的方法中)去擷取RedissonClient這個bean。我在自動注入(@Autowired)RedissonClient時發現報循環依賴異常,應用無法啟動。

        為什麼配置類要在執行個體化時擷取自己管理的bean呢?因為代碼的作者是要把這個配置類作為工具類,這個工具類要用到這個bean。這個工具類是作為Redis分布式鎖用的,用到RedissonClient鎖相關方法,而我想注入RedissonClient,使用它讀寫Redis的方法。

        本文介紹這個問題是如何引起的,以及如何解決。

題外話

        剛遇到這個問題時,我其實有點竊喜的。原因是:這個問題我知道快速解決的方案,也想通過這個問題來深入了解循環依賴的原理。

        快速解決的方案是:不進行自動注入,使用時再使用ApplicationContext來擷取​

        我之前寫過關于循環依賴的部落格,對循環依賴也算是比較熟悉了,我感覺我能以比較優雅的方式來解決這個問題。​

        這也說明了寫部落格的重要性,有問題直接找就行,自己實測過的代碼,用起來絕對放心。

問題複現

本處為了簡單起見不引用RedisClient依賴,自己寫一個簡單的類進行示例。

代碼

RedissonClient配置類(工具類)

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        RedisLockUtil.redissonClient = applicationContext.getBean(RedissonClient.class);
        // System.out.println("This is AUtil#init");
    }

    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

RedissonClient類

package com.example.entity;

public class RedissonClient {
    public void getLock(String string) {
        System.out.println("RedissonClient#getLock:" + string);
    }

    public void getMap() {
        System.out.println("RedissonClient#getMap");
    }
}      

我這裡為了示例 ,直接列印參數,傳回值也直接寫為void。

啟動類

package com.example;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    @Autowired
    private RedissonClient redissonClient;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}      

        如果在其他地方注入RedissonClient,有可能不會循環依賴,有可能會循環依賴,這是與bean的掃描順序有關系的。本處為了百分百複現,直接在啟動類注入。

啟動時的報錯列印

        這個列印太長了,但都比較重要。是以我将它單獨作為一個二級标題,可以快速在其他内容與這個錯誤列印之間跳轉。

Connected to the target VM, address: '127.0.0.1:54749', transport: 'socket'

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2021-09-24 23:30:07.025  INFO 13560 --- [           main] com.example.DemoApplication              : Starting DemoApplication on DESKTOP-QI6B9ME with PID 13560 (E:\work\Idea_proj\demo_JAVA\demo_SpringBoot\target\classes started by Liu in E:\work\Idea_proj\demo_JAVA\demo_SpringBoot)
2021-09-24 23:30:07.027  INFO 13560 --- [           main] com.example.DemoApplication              : No active profile set, falling back to default profiles: default
2021-09-24 23:30:08.312  INFO 13560 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-09-24 23:30:08.321  INFO 13560 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-09-24 23:30:08.321  INFO 13560 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.35]
2021-09-24 23:30:08.430  INFO 13560 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-09-24 23:30:08.430  INFO 13560 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1365 ms
2021-09-24 23:30:08.481  WARN 13560 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoApplication': Unsatisfied dependency expressed through field 'redissonClient'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisLockUtil': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getRedissonClient': Requested bean is currently in creation: Is there an unresolvable circular reference?
2021-09-24 23:30:08.483  INFO 13560 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2021-09-24 23:30:08.493  INFO 13560 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-09-24 23:30:08.506 ERROR 13560 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoApplication': Unsatisfied dependency expressed through field 'redissonClient'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisLockUtil': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getRedissonClient': Requested bean is currently in creation: Is there an unresolvable circular reference?
  at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:643) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:130) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1422) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:895) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at com.example.DemoApplication.main(DemoApplication.java:14) [classes/:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisLockUtil': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getRedissonClient': Requested bean is currently in creation: Is there an unresolvable circular reference?
  at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:160) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:416) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:595) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:409) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1338) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1306) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  ... 20 common frames omitted
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getRedissonClient': Requested bean is currently in creation: Is there an unresolvable circular reference?
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:347) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:219) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:227) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1174) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:422) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:352) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:345) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1126) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at com.example.config.RedisLockUtil.init(RedisLockUtil.java:21) ~[classes/:na]
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201]
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201]
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201]
  at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201]
  at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:389) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:333) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:157) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  ... 41 common frames omitted

2021-09-24 23:30:08.510  WARN 13560 --- [           main] o.s.boot.SpringApplication               : Unable to close ApplicationContext

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springApplicationAdminRegistrar' defined in class path resource [org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfiguration.class]: Unsatisfied dependency expressed through method 'springApplicationAdminRegistrar' parameter 1; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
  at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:798) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:539) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1338) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.event.AbstractApplicationEventMulticaster.retrieveApplicationListeners(AbstractApplicationEventMulticaster.java:245) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.event.AbstractApplicationEventMulticaster.getApplicationListeners(AbstractApplicationEventMulticaster.java:197) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:134) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.boot.availability.AvailabilityChangeEvent.publish(AvailabilityChangeEvent.java:81) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.availability.AvailabilityChangeEvent.publish(AvailabilityChangeEvent.java:67) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.doClose(ServletWebServerApplicationContext.java:167) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:978) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.boot.SpringApplication.handleRunFailure(SpringApplication.java:814) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:325) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
  at com.example.DemoApplication.main(DemoApplication.java:14) [classes/:na]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1716) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1272) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:885) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:789) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
  ... 23 common frames omitted

Disconnected from the target VM, address: '127.0.0.1:54749', transport: 'socket'      

原因分析

簡要分析

我摘抄上邊“啟動時的報錯列印”的最重要的部分:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoApplication': Unsatisfied dependency expressed through field 'redissonClient'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisLockUtil': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getRedissonClient': Requested bean is currently in creation: Is there an unresolvable circular reference?

        錯誤大概是這樣的:DemoApplication=> RedissonClient=> RedisLockUtil的初始化方法=> getRedissonClient(也就是擷取RedissonClient這個bean)。

        用人話來表述:建立DemoApplication這個bean時,依賴了RedissonClient,是以去建立RedissonClient,RedissonClient由RedisLockUtil管理的,是以先建立RedisLockUtil,建立RedisLockUtil時的初始化方法又依賴了RedissonClient,導緻循環依賴。

詳細分析

執行個體化順序

        RedisLockUtil提供RedissonClient這個bean,初始化時卻擷取不到,這應該是RedissonClient這個bean的執行個體化在RedisLockUtil的執行個體化之後導緻的。打個斷點驗證一下:
Spring(SpringBoot)--解決@Configuration的循環依賴問題
啟動後發現,果然,先到達21行那個斷點。

RedissonClient何時會執行個體化?

        什麼時候會去執行個體化RedissonClient這個bean呢,整個流程如何?(這裡有助于了解bean執行個體化)。本處進行分析。

        為了代碼能走到RedissonClient執行個體化的地方,我把上邊會報錯的地方換掉:

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        // RedisLockUtil.redissonClient = applicationContext.getBean(RedissonClient.class);
        System.out.println("This is RedisLockUtil#init");
    }

    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      
打兩個斷點:
Spring(SpringBoot)--解決@Configuration的循環依賴問題
第1個斷點到達:
Spring(SpringBoot)--解決@Configuration的循環依賴問題

解釋:先執行個體化RedisLockUtil,再調用到期init方法,就到了我們的第1個斷點。

第2個斷點到達:

Spring(SpringBoot)--解決@Configuration的循環依賴問題

 解釋:通過factoryBean(即RedisLockUtil)和beanName(即getRedissonClient)來執行個體化擷取RedissonClient這個bean。

        可以發現,兩個斷點,紅框中是最靠近的一個共同的方法。通過它的名字instantiateUsingFactoryMethod可以知道,是通過工廠方法來執行個體化RedissonClient,這個工廠就是RedisLockUtil。

        已經夠清晰了。我注入一個RedissonClient來使用,Spring去容器裡查找,發現它受RedisLockUtil管理,于是先執行個體化它,執行個體化後調用其初始化方法(@PostConstruct注解的方法),初始化方法裡也是想擷取RedissonClient,而這時RedissonClient還沒有呢!是以就報錯!

解決方案

下邊5種解決方案。第5種為最好的方案。

公共代碼

package com.example.entity;

public class RedissonClient {
    public void getLock(String string) {
        System.out.println("RedissonClient#getLock:" + string);
    }

    public void getMap() {
        System.out.println("RedissonClient#getMap");
    }
}      

這個類都會用到,是以我放到這個公共位置。 

方案1:不自動注入

        SpringBoot啟動時會掃描所有被@Component注解的bean,然後去掃描這個bean裡邊的字段,如果它有@Autowired注解,那就從Spring容器中找它然後将引用位址指派給這個字段,如果找不到則去執行個體化它。

        本文的使用@Autowired注入RedissonClient就是這樣,啟動時去執行個體化它導緻的失敗。

        是以思路很簡單,我不自動注入,我使用的時候再擷取,這樣啟動時就不會去執行個體化它。擷取的方法為:使用ApplicationContext。想詳細了解ApplicationContext的可以見​​此文​​。

寫法如下:

ApplicationContext工具類

package com.example.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
 
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;
 
    @Override
    public void setApplicationContext(ApplicationContext context)
            throws BeansException {
        ApplicationContextHolder.context = context;
    }
 
    public static ApplicationContext getContext() {
        return context;
    }
}      

啟動類

package com.example;

import com.example.entity.RedissonClient;
import com.example.util.ApplicationContextHolder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    private static RedissonClient redissonClient;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        DemoApplication.redissonClient = ApplicationContextHolder.getContext().getBean(RedissonClient.class);
        DemoApplication.redissonClient.getMap();
    }

}      

原來的配置類不變

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        RedisLockUtil.redissonClient = applicationContext.getBean(RedissonClient.class);
        // System.out.println("This is RedisLockUtil#init");
    }

    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

啟動測試

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2021-09-25 10:10:23.936  INFO 49756 --- [           main] com.example.DemoApplication              : Starting DemoApplication on DESKTOP-QI6B9ME with PID 49756 (E:\work\Idea_proj\demo_JAVA\demo_SpringBoot\target\classes started by Liu in E:\work\Idea_proj\demo_JAVA\demo_SpringBoot)
2021-09-25 10:10:23.936  INFO 49756 --- [           main] com.example.DemoApplication              : No active profile set, falling back to default profiles: default
2021-09-25 10:10:24.896  INFO 49756 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-09-25 10:10:24.896  INFO 49756 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-09-25 10:10:24.896  INFO 49756 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.35]
2021-09-25 10:10:25.001  INFO 49756 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-09-25 10:10:25.001  INFO 49756 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1028 ms
2021-09-25 10:10:25.316  INFO 49756 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-09-25 10:10:25.431  INFO 49756 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-09-25 10:10:25.586  INFO 49756 --- [           main] com.example.DemoApplication              : Started DemoApplication in 1.944 seconds (JVM running for 2.964)
RedissonClient#getMap      

 可見:正常啟動了。

方案2:延遲注入

        Spring有延遲注入的方法,使用的時候再去執行個體化這個bean,而不是啟動時就執行個體化

package com.example;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Lazy;

@SpringBootApplication
public class DemoApplication {
    @Lazy
    @Autowired
    private RedissonClient redissonClient;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}      

這樣就可以啟動成功了。 

方案3:用方法獲得bean

此方法更簡單了,對代碼的改動最小。

原來配置類是這樣的:

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        RedisLockUtil.redissonClient = applicationContext.getBean(RedissonClient.class);
    }

    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

修改之後: 

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @PostConstruct
    public void init() {
        RedisLockUtil.redissonClient = getRedissonClient();
    }

    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

        可以看到,我直接調用本類裡擷取bean的方法來拿的bean。這樣是可以的,也是可以保證RedissonClient的單例模式,沒有任何影響。項目也可以啟動成功。

        看到這裡,應該要有疑問了。

疑問一:我是怎麼知道可以通過這種方法來獲得本類中的bean的?

這種用法是我自己在使用shiro架構時,看到其他人這樣寫的,就知道了這種用法。可見,技術是相通的,了解的多了,思路就會更多。

疑問2:這樣為什麼仍然可以保證單例模式,這樣擷取的bean會加入Spring容器的管理嗎?

答案是:會加入Spring容器的管理。

接下來,我就分析一下,為什麼它可以加入Spring容器的管理。老方法,加斷點:

Spring(SpringBoot)--解決@Configuration的循環依賴問題

首先到達第22行的斷點:

Spring(SpringBoot)--解決@Configuration的循環依賴問題

然後一直點選箭頭所指的位置(單步),一直找到它加入Spring容器的地方:

Spring(SpringBoot)--解決@Configuration的循環依賴問題

        看到紅框中的代碼,也就是說,它會加入Spring容器,下次有其他地方需要這個bean的時候,會直接擷取這個bean,而不是新構造一個了。

        有朋友可能會問,我怎麼知道紅框中的代碼是加入Spring容器的代碼。我之前分析過循環依賴的詳細原理,是以我一眼就能看出這是第三級緩存。​

方案4:bean放到外部管理

        對于這個RedisLockUtil配置類,它同時也是工具類,它用到了RedissonClient的鎖功能,其實RedissonClient的其他讀寫Redis的方法也巨好用,就像操作JDK中的List、Set、Map一樣,RedissonClient實際是實作了JDK中的List、Set、Map,是以使用RedissonClient操作Redis就像我們操作JDK的List、Set、Map一樣,超級便利。基于此,我把RedissonClient的配置單獨拿出來。

修改之前

配置類(工具類)

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        RedisLockUtil.redissonClient = applicationContext.getBean(RedissonClient.class);
    }

    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

啟動類 

package com.example;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    @Autowired
    private RedissonClient redissonClient;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}      

修改之後

配置類(工具類) 

package com.example.util;

import com.example.entity.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        RedisLockUtil.redissonClient = applicationContext.getBean(RedissonClient.class);
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

配置類 

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }
}      

 啟動類不變

測試修改後的結果

可以啟動成功。

方案5:工具類不加入容器(推薦)

        思考一下:RedisLockUtil是個工具類,我們想用的是它的靜态方法,是以,最好不将其加入容器。本方案為最優方案,個人感覺是最優雅的方案。

工具類

package com.example.util;

import com.example.entity.RedissonClient;

public class RedisLockUtil {
    private static RedissonClient redissonClient;
    private static final String PREFIX = "rbac:user:" ;

    static {
        System.out.println("RedisLockUtil.static initializer");
        RedisLockUtil.redissonClient = ApplicationContextHolder.getContext().getBean(RedissonClient.class);
    }

    public static void lock(String name) {
        RedisLockUtil.redissonClient.getLock(PREFIX + name);
    }
}      

RedissonClient

package com.example.entity;

public class RedissonClient {
    public void getLock(String string) {
        System.out.println("RedissonClient#getLock:" + string);
    }

    public void getMap() {
        System.out.println("RedissonClient#getMap");
    }
}      

Redisson配置類

package com.example.config;

import com.example.entity.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient getRedissonClient() {
        return new RedissonClient();
    }
}      

ApplicationContext工具類

package com.example.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
 
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;
 
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        ApplicationContextHolder.context = context;
    }
 
    public static ApplicationContext getContext() {
        return context;
    }
}      
package com.example;

import com.example.entity.RedissonClient;
import com.example.util.RedisLockUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    @Autowired
    private RedissonClient redissonClient;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        RedisLockUtil.lock("Tony");
    }

}      
.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2021-09-25 12:18:03.232  INFO 5528 --- [           main] com.example.DemoApplication              : Starting DemoApplication on DESKTOP-QI6B9ME with PID 5528 (E:\work\Idea_proj\demo_JAVA\demo_SpringBoot\target\classes started by Liu in E:\work\Idea_proj\demo_JAVA\demo_SpringBoot)
2021-09-25 12:18:03.234  INFO 5528 --- [           main] com.example.DemoApplication              : No active profile set, falling back to default profiles: default
2021-09-25 12:18:04.380  INFO 5528 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-09-25 12:18:04.388  INFO 5528 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-09-25 12:18:04.388  INFO 5528 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.35]
2021-09-25 12:18:04.499  INFO 5528 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-09-25 12:18:04.499  INFO 5528 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1226 ms
2021-09-25 12:18:04.896  INFO 5528 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-09-25 12:18:05.024  INFO 5528 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-09-25 12:18:05.202  INFO 5528 --- [           main] com.example.DemoApplication              : Started DemoApplication in 2.299 seconds (JVM running for 3.382)
RedisLockUtil.static initializer
RedissonClient#getLock:rbac:user:Tony