背景:
在使用谷歌開源的本地緩存解決經常查詢數據庫導致的查詢效率低下,將從數據庫查詢好的數據放入到緩存中,然后設計過期時間,接着設計一個get方法緩存匯總獲取數據,進一步將整個流程封裝成一個CacheSerice,然后在Controller層調用這個Service,從Service中獲取數據。
問題:
需要對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的構造函數在執行的時候發生空指針問題。那么有可能是引入的谷歌開源庫的問題有可能不是,采用排除法很快就發現了不是這個庫的問題,不含谷歌開源庫的測試類采用這種寫法也發生了空指針的問題。
問題思考:
既然跟引入的谷歌開源庫沒有關系,那就說明當CacheService被構造的時候(采用構造函數),里面依賴的其他bean還沒有被構造出來,因而導致空指針問題。針對這個問題進一步對Spring的bean構造過程進行研究。
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的裝配方式
