Spring的bean加載以及JVM類加載過程

背景:html

在使用谷歌開源的本地緩存解決常常查詢數據庫致使的查詢效率低下,將從數據庫查詢好的數據放入到緩存中,而後設計過時時間,接着設計一個get方法緩存彙總獲取數據,進一步將整個流程封裝成一個CacheSerice,而後在Controller層調用這個Service,從Service中獲取數據。java

問題:數據庫

須要對CacheService進行初始化,設計的初衷是:當Service的bean被加載以後,其中的緩存數據就已經被初始化(即利用數據庫查詢Service獲取數據,並塞入緩存),而這個初始化的過程被我放到了CacheService類的構造函數中。結果在發佈的時候就一直報空指針。緩存

@Service("test")
public class Test implements IAppnameCache {

    @Autowired
    IAppnameService iAppnameService;
    public Test(){
        iAppnameService.queryAppname();// 拋出空指針
    }

    @Override
    public List<AppnameViewModel> get(String app){
        return iAppnameService.queryAppname();
    }
}

 

問題定位:安全

通過查詢日誌,發現是CacheService的構造函數在執行的時候發生空指針問題。那麼有多是引入的谷歌開源庫的問題有可能不是,採用排除法很快就發現了不是這個庫的問題,不含谷歌開源庫的測試類採用這種寫法也發生了空指針的問題。多線程

問題思考:app

既然跟引入的谷歌開源庫沒有關係,那就說明當CacheService被構造的時候(採用構造函數),裏面依賴的其餘bean尚未被構造出來,於是致使空指針問題。針對這個問題進一步對Spring的bean構造過程進行研究。ide

Spring的bean加載過程:函數

bean的主要生成過程以下:工具

1,AbstractBeanFactory.getBean(String)
2,AbstractBeanFactory.doGetBean(String, Class<T>, Object[], boolean)
3,DefaultSingletonBeanRegistry.getSingleton(String)
4,AbstractAutowireCapableBeanFactory.createBean(String, RootBeanDefinition, Object[])
5,AbstractAutowireCapableBeanFactory.doCreateBean(String, RootBeanDefinition, Object[])
6,AbstractAutowireCapableBeanFactory.createBeanInstance(String, RootBeanDefinition, Object[])
7,AbstractAutowireCapableBeanFactory.instantiateBean(String, RootBeanDefinition)
8,SimpleInstantiationStrategy.instantiate(RootBeanDefinition, String, BeanFactory)
9, AbstractAutowireCapableBeanFactory.populateBean(String, RootBeanDefinition, BeanWrapper)
10,AbstractAutowireCapableBeanFactory.initializeBean(String, Object, RootBeanDefinition)
11,AbstractAutowireCapableBeanFactory.invokeAwareMethods(String, Object)
12,AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(Object, String)
13,AbstractAutowireCapableBeanFactory.invokeInitMethods(String, Object, RootBeanDefinition)
14,AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(Object, String)

  

(1)在經過BeanFactory獲取bean實例對象的時候,會先去單例集合中找是否已經建立了對應的實例,若是有就直接返回了,這裏是第一次獲取,因此沒有拿到;

(2)而後AbastractBeanFactory會根據bean的名稱獲取對應的BeanDefinition對象,BeanDefinition對象表明了對應類的各類元數據,因此根據BeanDefinition對象就能夠判斷是不是單例,是否依賴其餘對象,若是依賴了其餘對象那麼先生成其依賴,這裏是遞歸調用。

在步驟7以前都是爲了生成bean作準備,真正生成bean是在AbstractAutowireCapableBeanFactory的instantiateBean方法:

protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
	try {
		Object beanInstance;
		final BeanFactory parent = this;
		if (System.getSecurityManager() != null) {
			beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() {
				@Override
				public Object run() {
					return getInstantiationStrategy().instantiate(mbd, beanName, parent);
				}
			}, getAccessControlContext());
		}
		else {
			beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
		}
		BeanWrapper bw = new BeanWrapperImpl(beanInstance);
		initBeanWrapper(bw);
		return bw;
	}
	catch (Throwable ex) {
		throw new BeanCreationException(
				mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
	}
}

-----------------------------------------------------------------------------------------------------
@Override public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { // Don't override the class with CGLIB if no overrides. if (bd.getMethodOverrides().isEmpty()) { Constructor<?> constructorToUse; synchronized (bd.constructorArgumentLock) { //這裏一堆安全檢查 } //默認使用構造函數利用反射實例化bean return BeanUtils.instantiateClass(constructorToUse); } else { // Must generate CGLIB subclass. return instantiateWithMethodInjection(bd, beanName, owner); } }

  能夠看到實際上bean的生成是直接使用BeanUtils工具類經過反射獲取類的實例。

而反射獲取類實例的過程以下:

Class<?> cls = Class.forName("cn.mldn.demo.Person"); // 取得Class對象
Object obj = cls.newInstance() //反射實例化對象
Constructor<?> cons = cls.getConstructor(String.class, int.class);//得到構造方法
Method m3 = cls.getDeclaredMethod("getName"); //得到get方法
Field nameField = cls.getDeclaredField("name"); // 得到name屬性

同時在JVM進行類加載的時,再進行到初始化這一步驟的時候,首先會調用默認構造器進行變量初始化:

  • 類構造器<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句快能夠賦值,可是不能訪問。
  • 類構造器<clinit>()方法與類的構造函數(實例構造函數<init>()方法)不一樣,它不須要顯式調用父類構造,虛擬機會保證在子類<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中的第一個執行的<clinit>()方法的類確定是java.lang.Object。
  • 因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句快要優先於子類的變量賦值操做。
  • <clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句,也沒有變量賦值的操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。(默認值是內存分配的時候賦予的,與初始化過程無關)
  • 接口中不能使用靜態語句塊,但接口與類不一樣是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
  • 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個進程阻塞。

CacheService類中沒有賦值行爲,而後則會調用默認的構造函數,因此在CacheService類中,被反射獲取構造器的時候會調用明確的構造器。迴歸到本次的問題,在構造器中使用了其餘的bean,而Spring的bean生成實際上是沒有規律的(也就是依賴的bean尚未被注入),因此拋出空指針的異常。

那麼問題來了,Spring說好的有自動檢測依賴的功能呢?


請列位看官慢慢往下看,請小生爲各位一一分解。

咱們把目光往前看,若是在容器中沒有拿到目標bean,而後AbastractBeanFactory會根據bean的名稱獲取對應的BeanDefinition對象,BeanDefinition對象表明了對應類的各類元數據,

// 運行到這裏說明bean沒有被建立,先獲取此bean依賴的bean
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
  for (String dep : dependsOn) {
	if (isDependent(beanName, dep)) {
	  throw new BeanCreationException(mbd.getResourceDescription(), beanName,
		"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
		}
	registerDependentBean(dep, beanName);
	//實例化依賴的bean
	getBean(dep);
  }
}

能夠看到是經過getDependsOn方法獲取依賴的bean,而這個過程是經過Setter將CacheService的屬性bean進行注入,而後獲取bean。那麼既然屬性注入的時候

IAppnameService就已經完成bean注入了,爲什麼構造器仍是拋出了異常呢?原來上述過程注入明確指出的依賴,即在bean的配置中加入depends-on(也支持註解),
若是沒有配置,那麼CacheService的屬性注入是在getbean()完成以後。因此在執行CacheService的構造函數時固然拋出異常啦!那麼也就是說Spring的依賴檢查其實沒有開啓的,
是須要手動在配置文件中開啓的,在Spring中有四種依賴檢查的方式。依賴檢查有四種模式:simple,objects,all,none,都經過bean的dependency-check屬性進行模式設置。

固然Spring中的加載過程當中,其加載過程仍是遵循JVM的加載過程。


JVM的主要加載過程

在JVM中是經過類加載器以及類的全限定名來保證類的惟一性的,也就是說若是兩個類的路徑名稱徹底同樣,可是隻要是加載它們的類加載器不同就能夠認爲是兩個不同的類。而在JVM中含有以下幾種類加載器:

JVM中包括集中類加載器:

  1 BootStrapClassLoader 引導類加載器

  2 ExtClassLoader 擴展類加載器

  3 AppClassLoader 應用類加載器

  4 CustomClassLoader 用戶自定義類加載器

而且在JVM中使用雙親委派模型進行加載。什麼是雙親委派模型呢?就是在2,3,4的類加載器加載類的時候,都會向上調用父類加載器來實現,也就是說最後都是交給BootStrapClassLoader加載器完成加載的。這樣作的好處一是由於安全性,由於JVM的類加載過程當中有驗證這一步驟,會對class文件進行校驗,判斷是否符合JVM規範。二是由於保證類不會被重複加載,由於在執行new的時候,會首先從元數據區查找類符號,若是沒有則會加載相應的文件。因此爲了不在這個過程當中重複加載的現象,最終都是經過系統提供的類加載器完成加載。

加載細節:

(未完待續)參考資料:
1. 深刻理解JVM虛擬機
2. https://blog.51cto.com/wenshengzhu/1950146
3. https://blog.csdn.net/h12kjgj/article/details/54312766
4
. https://www.cnblogs.com/kjitboy/p/12076303.html [Spring Bean的裝配方式

相關文章
相關標籤/搜索