1. 程式人生 > 程式設計 >詳解配置類為什麼要新增@Configuration註解

詳解配置類為什麼要新增@Configuration註解

不加@Configuration導致的問題

我們先來看看如果不在配置類上新增@Configuration註解會有什麼問題,程式碼示例如下:

@ComponentScan("com.dmz.source.code")
//@Configuration
public class Config{
	@Bean
	public A a(){
	return new A(dmzService());
	}

	@Bean
	public DmzService dmzService(){
		return new DmzService();
	}
}

public class A {
	public A(DmzService dmzService){
		System.out.println("create A by dmzService");
	}
}

@Component
public class DmzService {
	public DmzService(){
		System.out.println("create dmzService");
	}
}

不新增@Configuration註解執行結果:

create dmzService
create A by dmzService
create dmzService

新增@Configuration註解執行結果:

create dmzService
create A by dmzService

在上面的例子中,我們會發現沒有新增@Configuraion註解時dmzService被建立了兩次, 這是因為第一次建立是被Spring容器所建立的,Spring呼叫這個dmzService()建立了一個Bean被放入了單例池中(沒有新增其它配置預設是單例的),第二次建立是Spring容器在建立a時呼叫了a(),而a()又呼叫了dmzService()方法。

這樣的話,就出現問題了。

第一,對於dmzService而言,它被建立了兩次,單例被打破了

第二,對於a而言,它所依賴的dmzService不是Spring所管理的,而是直接呼叫的一個普通的java method建立的普通物件。這個物件不被Spring所管理意味著,首先它的域(Scope)定義失效了,其次它沒有經過一個完整的生命週期,那麼我們所定義所有的Bean的後置處理器都沒有作用到它身上,其中就包括了完成AOP的後置處理器,所以AOP也失效了。

上面的分析不能說服你的話,我們可以看看官方在@Bean上給出的這一段註釋

詳解配置類為什麼要新增@Configuration註解

首先,Spring就在註釋中指出了,通常來說,BeanMethod一般都申明在一個被@Configuration註解標註的類中,在這種情況下,BeanMethod可能直接引用了在同一個類中申明的beanMethod,就像本文給出的例子那樣,a()直接引用了dmzService(),我們重點再看看劃紅線的部分,通過呼叫另外一個beanMethod進入的Bean的引用會被保證是遵從域定義以及AOP語義的,就像getBean所做的那樣。這是怎麼實現的呢?在最後被紅線標註的地方也有說明,是通過在執行時期為沒有被@Configuration註解標註的配置類生成一個CGLIB的子類。

原始碼分析

Spring是在什麼時候建立的代理呢?到目前為止我們應該沒有落掉Spring整個啟動流程的任何關鍵程式碼,那麼我們不妨帶著這個問題繼續往下看。目前來說我們已經閱讀到了Spring執行流程圖中的3-5步,也就是org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors方法,在之前的分析中我們已經知道了,這個方法的主要作用就是執行BeanFactoryPostProcessor中的方法,首先執行的是BeanDefinitionRegistryPostProcessor(繼承了BeanFactoryPostProcessor)的postProcessBeanDefinitionRegistry方法,然後執行postProcessBeanFactory方法。而到目前為止我們並沒有向容器中註冊bean工廠的後置處理器(BeanFactoryPostProcessor),這就意味著當前容器中只有一個ConfigurationClassPostProcessor會被執行,在前文中我們已經分析過了它的postProcessBeanDefinitionRegistry方法,緊接著我們就來看看它的postProcessBeanFactory方法做了什麼。其原始碼如下:

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
  int factoryId = System.identityHashCode(beanFactory);
  // 防止重複處理
  if (this.factoriesPostProcessed.contains(factoryId)) {
    throw new IllegalStateException(
      "postProcessBeanFactory already called on this post-processor against " + beanFactory);
  }
  this.factoriesPostProcessed.add(factoryId);
  // 在執行postProcessBeanDefinitionRegistry方法的時就已經將這個id新增到registriesPostProcessed集合中了
  if (!this.registriesPostProcessed.contains(factoryId)) {
    processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
  }
	// 看起來這個方法就是完成了代理
  enhanceConfigurationClasses(beanFactory);
  // 添加了一個後置處理器
  beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

enhanceConfigurationClasses原始碼分析

public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {		// map中放置的是所有需要被代理的類
		Map<String,AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
		for (String beanName : beanFactory.getBeanDefinitionNames()) {
			BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
			if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {
				// 省略異常跟日誌程式碼....
        // 這個程式碼的含義就是如果是一個被@Configuration註解標註的類,那麼將其放入到configBeanDefs這個集合中
				configBeanDefs.put(beanName,(AbstractBeanDefinition) beanDef);
			}
		}
    
		if (configBeanDefs.isEmpty()) {
			// nothing to enhance -> return immediately
			return;
		}
		
    // 對配置類進行代理的核心類
		ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
		for (Map.Entry<String,AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
			AbstractBeanDefinition beanDef = entry.getValue();
			// 對於配置類永遠使用cglib代理
			beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE,Boolean.TRUE);
			try {
				// cglib代理是基於類實現的,所以在這之前要明確代理的類是什麼
				Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);
				if (configClass != null) {
          // 通過ConfigurationClassEnhancer獲取到一個經過代理的class
					Class<?> enhancedClass = enhancer.enhance(configClass,this.beanClassLoader);
         // 省略日誌....
          
          // 將原有的配置類的bd中的beanClass屬性替換成代理後的class
						beanDef.setBeanClass(enhancedClass);
					}
				}
			}
			catch (Throwable ex) {
				throw new IllegalStateException("Cannot load configuration class: " + beanDef.getBeanClassName(),ex);
			}
		}
	}

這段程式碼非常簡單,核心的程式碼在ConfigurationClassEnhancer中,所以我們要分析下ConfigurationClassEnhancer的原始碼,在分析它的原始碼前,我們需要對cglib有一定的瞭解。

1、cglib原理分析

1.1、使用示例

public class Target{
  public void f(){
    System.out.println("Target f()");
  }
  public void g(){
    System.out.println("Target g()");
  }
}

public class Interceptor implements MethodInterceptor {
  @Override
  public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable {
    System.out.println("I am intercept begin");
//Note: 此處一定要使用proxy的invokeSuper方法來呼叫目標類的方法
    proxy.invokeSuper(obj,args);
    System.out.println("I am intercept end");
    return null;
  }
}

public class Test {
  public static void main(String[] args) {
    // 設定這個屬性,將代理類的位元組碼檔案生成到F盤的code目錄下
  System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"F:\\code");
    //例項化一個增強器,也就是cglib中的一個class generator
    Enhancer eh = new Enhancer();
    //設定目標類
    eh.setSuperclass(Target.class);
    // 設定攔截物件
    eh.setCallback(new Interceptor());
    // 生成代理類並返回一個例項
    Target t = (Target) eh.create();
    t.f();
    t.g();
  }
}

執行結果為:

I am intercept begin
Target f()
I am intercept end
I am intercept begin
Target g()
I am intercept end

1.2、原理分析

檢視F盤的code目錄,會發現多了以下幾個檔案

詳解配置類為什麼要新增@Configuration註解

其中第二個檔案就是我們的代理類位元組碼,將其直接用IDEA開啟

// 省略多餘的方法,我們就關注g方法
public class Target$$EnhancerByCGLIB$$788444a0 extends Target implements Factory
{
  
  final void CGLIB$g$0()
  {
   super.g();
  }
  
  // 經過代理過的g方法
  public final void g()
  {
  
  // 檢視是否有攔截器存在
   MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
   if (tmp4_1 == null)
   {
     CGLIB$BIND_CALLBACKS(this);
     tmp4_1 = this.CGLIB$CALLBACK_0;
   }
   
   // 如果有攔截器的存在的話,直接呼叫攔截器的方法
   if (this.CGLIB$CALLBACK_0 != null) {
     tmp4_1.intercept(this,CGLIB$g$0$Method,CGLIB$emptyArgs,CGLIB$g$0$Proxy);
   }
   
   // 如果沒有攔截器,說明不需要代理,直接呼叫父類方法,也就是目標類的方法
   else{
     super.g();
   }
  }
}

可以看到,代理類繼承了目標類(Target),代理類為每個目標類的方法生成兩個方法,例如針對目標類中的每個非private方法,代理類會生成兩個方法,以g方法為例:一個是@Override的g方法,一個是CGLIB$g$0(CGLIB$g$0相當於目標類的g方法)。我們在示例程式碼中呼叫目標類的方法t.g()時,實際上呼叫的是代理類中的g()方法。

從這裡就能看出,跟JDK動態代理不同的是,cglib代理採用的是繼承的方式生成的代理物件。

在上面的例子中,我們實現了對cglib中方法的攔截,但是就目前而言我們沒有辦法選擇性的攔截目標類中的某一個方法,假設現在我們只想攔截Target中的g方法而不攔截f方法有什麼方法呢?我們看下面這個例子

public class Main {
	public static void main(String[] args) {
		System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"F:\\code");
		//例項化一個增強器,也就是cglib中的一個class generator
		Enhancer eh = new Enhancer();
		//設定目標類
		eh.setSuperclass(Target.class);
		// 設定攔截物件
		eh.setCallbacks(new Callback[]{new Interceptor(),NoOp.INSTANCE});
		eh.setCallbackFilter(new CallbackFilter() {
			@Override
			public int accept(Method method) {
				if(method.getName().equals("g"))
       // 這裡返回的是上面定義的callback陣列的下標,0就是我們的Interceptor物件,1是內建的NoOp物件,代表不做任何操作
				return 0;
				else return 1;
			}
		});
		// 生成代理類並返回一個例項
		Target t = (Target) eh.create();
		t.f();
		t.g();
	}
}

執行結果:

Target f()
I am intercept begin
Target g()
I am intercept end

此時f方法已經不會被代理了

2、ConfigurationClassEnhancer原始碼分析

2.1、建立代理過程分析

在對cglib的原理有了一定了解後,我們再來看ConfigurationClassEnhancer的原始碼就輕鬆多了

我們就關注其中核心的幾個方法,程式碼如下:

public Class<?> enhance(Class<?> configClass,@Nullable ClassLoader classLoader) {
  // 如果已經實現了EnhancedConfiguration介面,說明被代理過了,直接返回
  if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
    return configClass;
  }
  // 否則呼叫newEnhancer方法先建立一個增強器,然後直接使用這個增強器生成代理類的位元組碼物件
  Class<?> enhancedClass = createClass(newEnhancer(configClass,classLoader));
  if (logger.isDebugEnabled()) {
    logger.debug(String.format("Successfully enhanced %s; enhanced class name is: %s",configClass.getName(),enhancedClass.getName()));
  }
  return enhancedClass;
}

private Enhancer newEnhancer(Class<?> configSuperClass,@Nullable ClassLoader classLoader) {
  Enhancer enhancer = new Enhancer();
  // 設定目標類
  enhancer.setSuperclass(configSuperClass);
  // 讓代理類實現EnhancedConfiguration介面,這個介面繼承了BeanFactoryAware介面
  // 主要兩個作用:1.起到標記作用,如果實現了,代表已經被代理過了
  // 2.代理類需要訪問BeanFactory,所有實現了BeanFactoryAware介面
  enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
  // 設定生成的代理類不實現factory介面
  enhancer.setUseFactory(false);
  // 設定代理類名稱的生成策略
  enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
  // 代理類中引入一個BeanFactory欄位
  enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
  // 設定過濾器,CALLBACK_FILTER中也同時設定了攔截器
  enhancer.setCallbackFilter(CALLBACK_FILTER);
  enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
  return enhancer;
}

// 使用增強器生成代理類的位元組碼物件
private Class<?> createClass(Enhancer enhancer) {
  Class<?> subclass = enhancer.createClass();
  Enhancer.registerStaticCallbacks(subclass,CALLBACKS);
  return subclass;
}

並且我們會發現,在最開始這個類就申明瞭三個攔截器

// 宣告的三個攔截器
private static final Callback[] CALLBACKS = new Callback[] {
  new BeanMethodInterceptor(),new BeanFactoryAwareMethodInterceptor(),NoOp.INSTANCE
};

2.2、攔截器原始碼分析

基於我們之前對cglib的學習,肯定能知道,代理的核心邏輯就是依賴於攔截器實現的。其中NoOp.INSTANCE代表什麼都沒做,我們就關注前面兩個。

BeanFactoryAwareMethodInterceptor

之所以把這個攔截器放到前面分析是因為這個攔截器的執行時機是在建立配置類的時候,其原始碼如下:

private static class BeanFactoryAwareMethodInterceptor implements MethodInterceptor,ConditionalCallback {

		@Override
		@Nullable
		public Object intercept(Object obj,MethodProxy proxy) throws Throwable {
			// 在生成代理類的位元組碼時,使用了BeanFactoryAwareGeneratorStrategy策略
			// 這個策略會在代理類中新增一個欄位,BEAN_FACTORY_FIELD = "$$beanFactory"
			Field field = ReflectionUtils.findField(obj.getClass(),BEAN_FACTORY_FIELD);
			Assert.state(field != null,"Unable to find generated BeanFactory field");
			// 此時呼叫的方法是setBeanFactory方法,
			// 直接通過反射將beanFactory賦值給BEAN_FACTORY_FIELD欄位
			field.set(obj,args[0]);

			// Does the actual (non-CGLIB) superclass implement BeanFactoryAware?
			// If so,call its setBeanFactory() method. If not,just exit.
			// 如果目標配置類直接實現了BeanFactoryAware介面,那麼直接呼叫目標類的setBeanFactory方法
			if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
				return proxy.invokeSuper(obj,args);
			}
			return null;
		}

		@Override
		// 在呼叫setBeanFactory方法時才會攔截
		// 從前文我們知道,代理類是實現了實現EnhancedConfiguration介面的,
		// 這就意味著它也實現了BeanFactoryAware介面,那麼在建立配置類時,
		// setBeanFactory方法就會被呼叫,之後會就進入到這個攔截器的intercept方法邏輯中
		public boolean isMatch(Method candidateMethod) {
			return isSetBeanFactory(candidateMethod);
		}

		public static boolean isSetBeanFactory(Method candidateMethod) {
			return (candidateMethod.getName().equals("setBeanFactory") &&
					candidateMethod.getParameterCount() == 1 &&
					BeanFactory.class == candidateMethod.getParameterTypes()[0] &&
					BeanFactoryAware.class.isAssignableFrom(candidateMethod.getDeclaringClass()));
		}
	}

BeanMethodInterceptor

相比於上面一個攔截器,這個攔截器的邏輯就要複雜多了,我們先來看看它的執行時機,也就是isMatch方法

public boolean isMatch(Method candidateMethod) {
  // 第一個條件,不能是Object,這個必定是滿足的
  // 第二個條件,不能是setBeanFactory方法,顯而易見的嘛,我們要攔截的方法實際只應該是添加了@Bean註解的方法
  // 第三個條件,添加了@Bean註解
  return (candidateMethod.getDeclaringClass() != Object.class &&
      !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) &&
      BeanAnnotationHelper.isBeanAnnotated(candidateMethod));
}

簡而言之,就是攔截@Bean標註的方法,知道了執行時機後,我們再來看看它的攔截邏輯,程式碼其實不是很長,但是理解起來確很不容易,牽涉到AOP以及Bean的建立了,不過放心,我會結合例項給你講明白這段程式碼,下面我們先看原始碼:

public Object intercept(Object enhancedConfigInstance,Method beanMethod,Object[] beanMethodArgs,MethodProxy cglibMethodProxy) throws Throwable {
			// 之前不是給BEAN_FACTORY_FIELD這個欄位賦值了BeanFactory嗎,這裡就是反射獲取之前賦的值
			ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
			// 確定Bean的名稱
			String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);

			// Determine whether this bean is a scoped-proxy
			// 判斷這個Bean是否是一個域代理的類
			Scope scope = AnnotatedElementUtils.findMergedAnnotation(beanMethod,Scope.class);
			// 存在@Scope註解,並且開啟了域代理模式
			if (scope != null && scope.proxyMode() != ScopedProxyMode.NO) {
				String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
				// 域代理物件的目標物件正在被建立,什麼時候會被建立?當然是使用的時候嘛
				if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
					// 使用的時候呼叫@Bean方法來建立這個域代理的目標物件,所以@Bean方法代理的時候針對的是域代理的目標物件,目標物件需要通過getBean的方式建立
					beanName = scopedBeanName;
				}
			}

			// 判斷這個bean是否是一個factoryBean
			if (factoryContainsBean(beanFactory,BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
					factoryContainsBean(beanFactory,beanName)) {
				Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
				if (factoryBean instanceof ScopedProxyFactoryBean) {
					// ScopedProxyFactoryBean還記得嗎?在進行域代理時使用的就是這個物件
					// 對於這個FactoryBean我們是不需要進行代理的,因為這個factoryBean的getObject方法
					// 只是為了得到一個類似於佔位符的Bean,這個Bean只是為了讓依賴它的Bean在建立的過程中不會報錯
					// 所以對於這個FactoryBean我們是不需要進行代理的
					// 我們只需要保證這個FactoryBean所生成的代理物件的目標物件是通過getBean的方式建立的即可
				} else {
					// 而對於普通的FactoryBean我們需要代理其getObject方法,確保getObject方法產生的Bean是通過getBean的方式建立的
					// It is a candidate FactoryBean - go ahead with enhancement
					return enhanceFactoryBean(factoryBean,beanMethod.getReturnType(),beanFactory,beanName);
				}
			}
			// 舉個例子,假設我們被@Bean標註的是A方法,當前建立的BeanName也是a,這樣就符合了這個條件
			// 但是如果是這種請求,a(){b()},a方法中呼叫的b方法,那麼此時呼叫b方法建立b物件時正在執行的就是a方法
			// 此時就不滿足這個條件,會呼叫這個resolveBeanReference方法來解決方法引用
			if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
				// 如果當前執行的方法就是這個被攔截的方法,(說明是在建立這個Bean的過程中)
				// 那麼直接執行目標類中的方法,也就是我們在配置類中用@Bean標註的方法
				return cglibMethodProxy.invokeSuper(enhancedConfigInstance,beanMethodArgs);
			}
  // 說明不是在建立中了,而是別的地方直接呼叫了這個方法,這時候就需要代理了,實際呼叫getBean方法
  return resolveBeanReference(beanMethod,beanMethodArgs,beanName);
		private Object resolveBeanReference(Method beanMethod,ConfigurableBeanFactory beanFactory,String beanName) {

			// The user (i.e. not the factory) is requesting this bean through a call to
			// the bean method,direct or indirect. The bean may have already been marked
			// as 'in creation' in certain autowiring scenarios; if so,temporarily set
			// the in-creation status to false in order to avoid an exception.
			// 什麼時候會是alreadyInCreation?就是正在建立中,當Spring完成掃描後得到了所有的BeanDefinition
			// 那麼之後就會遍歷所有的BeanDefinition,根據BeanDefinition一個個的建立Bean,在建立Bean前會將這個Bean
			// 標記為正在建立的,如果是正在建立的Bean,先將其標記為非正在建立,也就是這行程式碼beanFactory.setCurrentlyInCreation(beanName,false)
			// 這是因為之後又會呼叫getBean方法,如果已經被標記為建立中了,那麼在呼叫getBean時會報錯
			boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName);
			try {
        // 如果是正在建立的Bean,先將其標記為非正在建立,避免後續呼叫getBean時報錯
				if (alreadyInCreation) {
					beanFactory.setCurrentlyInCreation(beanName,false);
				}
        
        // 在呼叫beanMthod的時候,也就是被@Bean註解標註的方法的時候如果使用了引數,只要有一個引數為null,就直接呼叫getBean(beanName),否則帶引數呼叫getBean(beanName,args),後面通過例子解釋這段程式碼
				boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs);
				if (useArgs && beanFactory.isSingleton(beanName)) {
					for (Object arg : beanMethodArgs) {
						if (arg == null) {
							useArgs = false;
							break;
						}
					}
				}
				Object beanInstance = (useArgs ? beanFactory.getBean(beanName,beanMethodArgs) :
						beanFactory.getBean(beanName));
        // 這裡發現getBean返回的型別不是我們方法返回的型別,這意味著什麼呢?
        // 在《你知道Spring是怎麼解析配置類的嗎?》我有提到過BeanDefinition的覆蓋
        // 這個地方說明beanMethod所定義的bd被覆蓋了
				if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(),beanInstance)) {

					if (beanInstance.equals(null)) {
						beanInstance = null;
					} else {
						// 省略日誌
						throw new IllegalStateException(msg);
					}
				}
        // 註冊Bean之間的依賴關係
        // 這個method是當前執行的一個建立bean的方法
				Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
        // 不等於null意味著currentlyInvoked這個方法建立的bean依賴了beanName所代表的Bean
     		// 在開頭的例子中,currentlyInvoked就是a(),beanName就是dmzService,outBeanName就是a
				if (currentlyInvoked != null) {
					String outerBeanName = BeanAnnotationHelper.determineBeanNafanhr(currentlyInvoked);
          // 註冊的就是a跟dmzService的依賴關係,註冊到容器中的dependentBeanMap中
          // key為依賴,value為依賴所在的bean
					beanFactory.registerDependentBean(beanName,outerBeanName);
				}
				return beanInstance;
			} finally {
				if (alreadyInCreation) {
          // 實際還在建立中,要走完整個生命週期流程
					beanFactory.setCurrentlyInCreation(beanName,true);
				}
			}
		}

3、結合例子講解難點程式碼

這部分內容非常細節,不感興趣可以跳過,主要是BeanMethodInterceptor中的方法。

3.1、判斷這個Bean是否是一個域代理的類示例程式碼

@Configuration
@EnableAspectJAutoProxy
public class Config {
  @Bean
  @Scope(value = WebApplicationContext.SCOPE_REQUEST,proxyMode = ScopedProxyMode.TARGET_CLASS)
  public DmzService dmzService() {
    return new DmzService();
  }
}

@RestController
@RequestMapping("/test")
public class Controller {

  DmzService dmzService;

  @Autowired
  public void setDmzService(DmzService dmzService) {
    this.dmzService = dmzService;
  }

  @GetMapping("/get")
  public ResponseEntity<?> get() {
    System.out.println(dmzService);
    return ResponseEntity.ok().build();
  }
}

我們需要除錯兩種情況

建立Controller時,注入dmzService,因為dmzService是一個request域的物件,正常情況下注入肯定是報錯的,但是我們在配置類上對域物件開啟了代理模式,所以在建立Controller時會注入一個代理物件。

詳解配置類為什麼要新增@Configuration註解

端點除錯,也確實如我們所料,這個地方注入的確實是一個代理物件,因為我們在配置類上申明瞭proxyMode = ScopedProxyMode.TARGET_CLASS,所以這裡是一個cglib的代理物件。

使用dmzService的時候,這個時候使用的應該是實際的目標物件。所以按照我們的分析應該通過getBean(targetBeanName)的方式來獲取到這個Bean,執行流程應該是代理物件cglibDmzService呼叫了toString方法,然後呼叫getBean,getBean要根據BeanDefinition建立Bean,而根據BeanDefinition的定義,需要使用配置類中的BeanMethod來建立Bean,所以此時會進入到BeanMethodInterceptor的intecept方法。

我們直接在intecept方法中進行斷點,會發現此時的呼叫棧如下

詳解配置類為什麼要新增@Configuration註解

  • 列印時,呼叫了toString方法
  • 實際將會去建立目標Bean,所以此時getBean時對應的BeanName為targetBeanName(“scopedTarget.”+beanName)
  • 在getBean時根據BeanDefinition的定義會通過執行配置類中的beanMethod方法來建立Bean
  • 最終就進入了攔截器中這個方法

這種情況下就會進入到下面這段程式碼的邏輯中

// 判斷這個Bean是否是一個域代理的類
Scope scope = AnnotatedElementUtils.findMergedAnnotation(beanMethod,Scope.class);
// 存在@Scope註解,並且開啟了域代理模式
if (scope != null && scope.proxyMode() != ScopedProxyMode.NO) {
  String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
  // 域代理物件的目標物件正在被建立,什麼時候會被建立?當然是使用的時候嘛
  if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
    // 使用的時候呼叫@Bean方法來建立這個域代理的目標物件,所以@Bean方法代理的時候針對的是域代理的目標物件
    beanName = scopedBeanName;
  }
}

3.3、方法引用的情況下,為什麼會出現Bean正在建立中(isCurrentlyInCreation)?

也就是下面這段程式碼什麼時候會成立

if (alreadyInCreation) {
  beanFactory.setCurrentlyInCreation(beanName,false);
}

示例程式碼

@ComponentScan(value = "com.dmz.spring.first")
@Configuration
public class Config {
	@Bean
	public A a(){
		return new A();
	}

	@Bean
	public B b(){
		a();
		return new B();
	}
}

class A{
	B b;

	@Autowired
	public void setB(B b) {
		this.b = b;
	}
}
class B{

}

上面這種配置,在啟動的時候就會進入到if條件中,在建立a的時候發現需要注入b,那麼Spring此時就會去建立b,b在建立的過程中又呼叫了a方法,此時a方法在執行時又被攔截了,然後就會進入到if判斷中去。對Spring有一定了解的同學應該能感覺到,這個其實跟迴圈依賴的原理是一樣的。關於迴圈依賴,在後面我單獨寫一篇文章進行說明。

3.4、if (arg == null) {useArgs = false;}是什麼意思?

這個程式碼我初看時也很不明白,為什麼只要有一個引數為null就直接標記成不使用引數呢?我說說自己的理解。

beanMethodArgs代表了呼叫beanMethod時傳入的引數,正常Spring自身是不會傳入這個引數的,因為沒有必要,建立Bean時其依賴早就通過BeanDefinition確定了,但是可能出現下面這種情況

示例程式碼

@Configuration
public class AnotherConfig {
	@Bean
	public DmzService dmzService(IndexService indexService) {
		return new DmzService(indexService);
	}

	@Bean
	public OrderService orderService() {
		DmzService dmzService = dmzService(null);
		return dmzService.createOrder();
	}
}


@Component
public class IndexService {
}

public class DmzService {
	public DmzService(IndexService indexService) {

	}

	public OrderService createOrder() {
		return new OrderService();
	}
}

public class OrderService {
}

這種情況下,我們在orderService()為了得到當前容器中的dmzService呼叫了對應的BeanMethod,但是按照方法的定義我們不得不傳入一個引數,但是實際上我們知道BeanMethod等價於getBean,所以上面這段程式碼可以等價於

@Configuration
public class AnotherConfig {
	
	@Autowired
	ApplicationContext applicationContext;
	
	@Bean
	public DmzService dmzService(IndexService indexService) {
		return new DmzService(indexService);
	}

	@Bean
	public OrderService orderService() {
		DmzService dmzService = (DmzService) applicationContext.getBean("dmzService");
		return dmzService.createOrder();
	}
}

對於getBean而言,傳入引數跟不傳引數在建立Bean時是有區別的,但是建立後從容器中獲取Bean時跟傳入的引數沒有一毛錢關係(單例情況),因為這是從快取中獲取嘛。也就是說單例下,傳入的引數只會影響第一次建立。正因為如此,getBean在單純的做獲取的時候不需要引數,那就意味著beanMthod在獲取Bean的時候也可以不傳入引數嘛,但是beanMthod作為一個方法又定義了形參,Spring就說,這種情況你就傳個null吧,反正我知道要去getBean,當然,這只是筆者的個人理解。

4、結合Spring整體對ConfigurationClassEnhancer相關原始碼分析總結

4.1、Bean工廠後置處理器修改bd,對應enhance方法執行流程

修改bd的整個過程都發生在Bean工廠後置處理器的執行邏輯中

詳解配置類為什麼要新增@Configuration註解

執行邏輯

詳解配置類為什麼要新增@Configuration註解

在上文中我們已經知道了,在執行bean工廠後置處理器前,Spring容器的狀態如下:

詳解配置類為什麼要新增@Configuration註解

那麼執行完成Bean工廠後置處理器後(不考慮程式設計師自定義的後置處理器),容器的狀態應該是這樣的

詳解配置類為什麼要新增@Configuration註解

4.2、BeanFactoryAwareMethodInterceptor執行流程

在容器中的bd就緒後,Spring會通過bd來建立Bean了,會先建立配置類,然後建立配置類中beanMethod定義的bean。在建立配置類的過程中在初始化Bean時,如果實現了Aware介面,會呼叫對於的setXxx方法,具體程式碼位於org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean

詳解配置類為什麼要新增@Configuration註解

在呼叫setBeanFactory方法時,會被攔截,進入到攔截器的邏輯中

執行邏輯

詳解配置類為什麼要新增@Configuration註解

4.3、BeanMethodInterceptor執行流程

以下面這段程式碼為例:

@Configuration
public class AnotherConfig {
	@Bean
	public DmzService dmzService(){
		return new DmzService();
	}

	@Bean
	public OrderService orderService(){
		return new OrderService(dmzService());
	}
}

Spring會根據beanMethod在配置類中定義順序來建立Bean,所以上面這段配置會先建立dmzServcice,之後在建立orderService

那麼BeanMethodInterceptor的攔截將會發生在兩個地方

  • 直接建立dmzService的過程中,攔截的是dmzService()方法
  • 建立orderService過程中,第一次攔截的是orderService()方法
  • orderService()方法呼叫了dmzService()方法,dmzService()方法又被攔截

在直接建立dmzService時,由於isCurrentlyInvokedFactoryMethod(beanMethod)這句程式碼會成立,所以會直接呼叫目標類的方法,也就是cglibMethodProxy.invokeSuper(enhancedConfigInstance,beanMethodArgs),就是我們在配置類中定義的dmzService()方法,通過這個方法返回一個dmzService

而建立orderService時,方法的呼叫就略顯複雜,首先它類似於上面的直接建立dmzService的流程,orderService()方法會被攔截,但是由於正在執行的方法就是orderService()方法,所以orderService()也會被直接呼叫。但是orderService()中又呼叫了dmzService()方法,dmzService()方法又被攔截了,此時orderService()還沒被執行完成,也就是說正在執行的方法是orderService()方法,所以isCurrentlyInvokedFactoryMethod(beanMethod)這句程式碼就不成立了,那麼就會進入org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#resolveBeanReference這個方法的邏輯中,在這個方法中,最終又通過getBean方法來獲取dmzService,因為dmzService之前已經被建立過了,所以在單例模式下,就直接從單例池中返回了,而不會再次呼叫我們在配置類中定義的dmzService()方法。

執行邏輯

詳解配置類為什麼要新增@Configuration註解

總結

這裡就在上篇文章的基礎上對流程圖再做一次完善吧,因為圖片太大了,就放個連結~

Spring建立bean前的執行流程

到此這篇關於詳解配置類為什麼要新增@Configuration註解 的文章就介紹到這了,更多相關配置類新增@Configuration註解 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!