Spring常問的------真實大廠面試題彙總(含答案)

面試題1. Spring中bean的循環依賴怎麼解決?

(一). 首先說一下什麼是Spring的循環依賴:

  • 其實就是在進行getBean的時候,A對象中去依賴B對象,而B對象又依賴C對象,但是對象C又去依賴A對象,結果就造成A、B、C三個對象都不能完成實例化,出現了循環依賴。就會出現死循環,最終導致內存溢出的錯誤。

(二).如何去解決Spring的循環依賴呢?

1.先知道什麼是Spring的“三級緩存”:就是下面的三個大的Map對象,因爲Spring中的循環依賴的理論基礎其實是基於java中的值傳遞的,然後其實Spring中的單例對象的創建是分爲三個步驟的:

  • createBeanInstance,其實第一步就是通過構造方法去進行實例化對象。但是這一步只是實例對象而已,並沒有把對象的屬性也給注入進去
  • 然後這一步就是進行注入實例對象的屬性,也就是從這步對spring xml中指定的property進行populate
  • 最後一步其實是初始化XML中的init方法,來進行最終完成實例對象的創建。但是AfterPropertiesSet方法會發生循環依賴的步驟集中在第一步和第二步。
singletonObjects指單例對象的cache (一級緩存)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

singletonFactories指單例對象工廠的cache(三級緩存)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

earlySingletonObjects指提前曝光的單例對象的cache(二級緩存)
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

2. 然後是怎麼具體使用到這個三級緩存的呢,或者說三級緩存的思路?

  • 首先第一步是在Spring中會先去調用getSingleton(String beanName, boolean allowEarlyReference)來獲取想要的單例對象。
  • 然後第一步會先進行通過singletonObjects這個一級緩存的集合中去獲取對象,如果沒有獲取成功的話並且使用isSingletonCurrentlyInCreation(beanName)去判斷對應的單例對象是否正在創建中(也就是說當單例對象沒有被初始化完全,走到初始化的第一步或者第二的時候),如果是正在創建中的話,會繼續走到下一步
  • 然後會去從earlySingletonObjects中繼續獲取這個對象,如果又沒有獲取到這個單例對象的話,並且通過參數傳進來的allowEarlyReference標誌,看是不是允許singletonFactories(三級緩存集合)去拿到該實例對象,如果allowEarlyReference爲Ture的話,那麼繼續下一步
  • 此時上一步中並沒有從earlySingletonObjects二級緩存集合中拿到想要的實例對象,最後只能從三級緩存singletonFactories (單例工廠集合中)去獲取實例對象,
  • 然後把獲取的對象通過Put(beanName, singletonObject)放到earlySingletonObjects(二級緩存中),然後在再從singletonFactories(三級緩存)對象中的集合中把該對象給remove(beanName)出去。
  • 附上核心代碼
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
 從一級緩存獲取
   Object singletonObject = this.singletonObjects.get(beanName);
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      synchronized (this.singletonObjects) {
       從二級緩存獲取
         singletonObject = this.earlySingletonObjects.get(beanName);
         if (singletonObject == null && allowEarlyReference) {
          從三級緩存獲取
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
               singletonObject = singletonFactory.getObject();
               this.earlySingletonObjects.put(beanName, singletonObject);
               this.singletonFactories.remove(beanName);
            }
         }
      }
   }
   return (singletonObject != NULL_OBJECT ? singletonObject : null);}

3. 總結一下爲什麼這麼做就能解決Spring中的循環依賴問題。

  • 其實在沒有真正創建出來一個實例對象的時候,這個對象已經被生產出來了,雖然還不完美(還沒有進行初始化的第二步和第三步),但是已經能被人認出來了(根據對象引用能定位到堆中的對象),所以Spring此時將這個對象提前曝光出來讓大家認識,讓大家使用
  • A首先完成了初始化的第一步,並且將自己提前曝光到singletonFactories中,此時進行初始化的第二步,發現自己依賴對象B,此時就嘗試去get(B),發現B還沒有被create,所以走create流程,B在初始化第一步的時候發現自己依賴了對象A,於是嘗試get(A),嘗試一級緩存singletonObjects(肯定沒有,因爲A還沒初始化完全),嘗試二級緩存earlySingletonObjects(也沒有),嘗試三級緩存singletonFactories,由於A通過ObjectFactory將自己提前曝光了,所以B能夠通過ObjectFactory.getObject拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀),B拿到A對象後順利完成了初始化階段1、2、3,完全初始化之後將自己放入到一級緩存singletonObjects中。此時返回A中,A此時能拿到B的對象順利完成自己的初始化階段2、3,最終A也完成了初始化,長大成人,進去了一級緩存singletonObjects中,而且更加幸運的是,由於B拿到了A的對象引用,所以B現在hold住的A對象也蛻變完美了!一切都是這麼神奇!!

總結:Spring通過三級緩存加上“提前曝光”機制,配合Java的對象引用原理,比較完美地解決了某些情況下的循環依賴問題!

面試題2. Spring中bean的加載過程?

首先從大的幾個核心步驟來去說明,因爲Spring中的具體加載過程和用到的類實在是太多了。

(1)、首先是先從AbstractBeanFactory中去調用doGetBean(name, requiredType, final Object[] args, boolean typeCheckOnly【這個是判斷進行創建bean還是僅僅用來做類型檢查】)方法,然後第一步要做的就是先去對傳入的參數name進行做轉換,因爲有可能傳進來的name=“&XXX”之類,需要去除&符號

(2)、然後接着是去調用getSingleton()方法,其實在上一個面試題中已經提到了這個方法,這個方法就是利用“三級緩存” 來去避免循環依賴問題的出現的。【這裏補充一下,只有在是單例的情況下才會去解決循環依賴問題】

(3)、對從緩存中拿到的bean其實是最原始的bean,還未長大,所以這裏還需要調用getObjectForBeanInstance(Object beanInstance, String name, String beanName, RootBeanDefinition mbd)方法去進行實例化。

(4)、然後會解決單例情況下嘗試去解決循環依賴,如果isPrototypeCurrentlyInCreation(beanName)返回爲true的話,會繼續下一步,否則throw new BeanCurrentlyInCreationException(beanName);

(5)、因爲第三步中緩存中如果沒有數據的話,就直接去parentBeanFactory中去獲取bean,然後判斷containsBeanDefinition(beanName)中去檢查已加載的XML文件中是否包含有這樣的bean存在,不存在的話遞歸去getBean()獲取,如果沒有繼續下一步

(6)、這一步是吧存儲在XML配置文件中的GernericBeanDifinition轉換爲RootBeanDifinition對象。這裏主要進行一個轉換,如果父類的bean不爲空的話,會一併合併父類的屬性

(7)、這一步核心就是需要跟這個Bean有關的所有依賴的bean都要被加載進來,通過剛剛的那個RootBeanDifinition對象去拿到所有的beanName,然後通過registerDependentBean(dependsOnBean, beanName)註冊bean的依賴

(8)、然後這一步就是會根據我們在定義bean的作用域的時候定義的作用域是什麼,然後進行判斷在進行不同的策略進行創建(比如isSingleton、isPrototype)

(9)、這個是最後一步的類型裝換,會去檢查根據需要的類型是否符合bean的實際類型去做一個類型轉換。Spring中提供了許多的類型轉換器

面試題3. Spring中bean的生命週期?

在這裏插入圖片描述

  1. 首先會先進行實例化bean對象
  2. 然後是進行對bean的一個屬性進行設置
  3. 接着是對BeanNameAware(其實就是爲了讓Spring容器來獲取bean的名稱)、BeanFactoryAware(讓bean的BeanFactory調用容器的服務)、ApplicationContextAware(讓bean當前的applicationContext可以來取調用Spring容器的服務)
  4. 然後是實現BeanPostProcessor 這個接口中的兩個方法,主要是對調用接口的前置初始化postProcessBeforeInitialization
  5. 這裏是主要是對xml中自己定義的初始化方法 init-method = “xxxx”進行調用
  6. 然後是繼續對BeanPostProcessor 這個接口中的後置初始化方法進行一個調用postProcessAfterInitialization()
  7. 其實到這一步,基本上這個bean的初始化基本已經完成,就處於就緒狀態
  8. 然後就是當Spring容器中如果使用完畢的話,就會調用destory()方法
  9. 最後會去執行我們自己定義的銷燬方法來進行銷燬,然後結束生命週期

補充:當在執行第六步之後,如果Spring初始的這個bean的作用域是Prototype的話,之後的過程就不歸Spring來去管理了,直接就交給用戶就可以了。

面試題4. 說一下Spring中的IOC核心思想和DI?

之前自己有總結過的一篇:https://blog.csdn.net/qq_36520235/article/details/79383238

面試題5. 說說Spring中的幾種事務和隔離級別?

面試題6. 說一下SpringMVC中的攔截器和Servlet中的filter有什麼區別?

  • 首先最核心的一點他們的攔截側重點是不同的,SpringMVC中的攔截器是依賴JDK的反射實現的,SpringMVC的攔截器主要是進行攔截請求,通過對Handler進行處理的時候進行攔截,先聲明的攔截器中的preHandle方法會先執行,然而它的postHandle方法(他是介於處理完業務之後和返回結果之前)和afterCompletion方法卻會後執行。並且Spring的攔截器是按照配置的先後順序進行攔截的。
  • 而Servlet的filter是基於函數回調實現的過濾器,Filter主要是針對URL地址做一個編碼的事情、過濾掉沒用的參數、安全校驗(比較泛的,比如登錄不登錄之類)

面試題7. spring容器的bean什麼時候被實例化?

(1)如果你使用BeanFactory作爲Spring Bean的工廠類,則所有的bean都是在第一次使用該Bean的時候實例化

(2)如果你使用ApplicationContext作爲Spring Bean的工廠類,則又分爲以下幾種情況:

  • 如果bean的scope是singleton的,並且lazy-init爲false(默認是false,所以可以不用設置),則 ApplicationContext啓動的時候就實例化該Bean,並且將實例化的Bean放在一個map結構的緩存中,下次再使 用該 Bean的時候,直接從這個緩存中取
  • 如果bean的scope是singleton的,並且lazy-init爲true,則該Bean的實例化是在第一次使用該Bean的時候進 行實例化
  • 如果bean的scope是prototype的,則該Bean的實例化是在第一次使用該Bean的時候進行實例化

面試題8.說一下Spring中AOP的底層是怎麼實現的?,再說說一下動態代理和cglib區別?

Spring中AOP底層的實現其實是基於JDK的動態代理和cglib動態創建類進行動態代理來實現的:

1. 第一種基於JDK的動態代理的原理是:

需要用到的幾個關鍵成員

  • InvocationHandler (你想要通過動態代理生成的對象都必須實現這個接口)
  • 真實的需要代理的對象(幫你代理的對象)
  • Proxy對象(是JDK中java.lang.reflect包下的)

下面是具體如何動態利用這三個組件生成代理對象

  • (1)首先你的真是要代理的對象必須要實現InvocationHandler 這個接口,並且覆蓋這個接口的invoke(Object proxyObject, Method method, Object[] args)方法,這個Invoker中方法的參數的proxyObject就是你要代理的真實目標對象,方法調用會被轉發到該類的invoke()方法, method是真實對象中調用方法的Method類,Object[] args是真實對象中調用方法的參數
  • (2)然後通過Proxy類去調用newProxyInstance(classLoader, interfaces, handler)方法,classLoader是指真實代理對象的類加載器,interfaces是指真實代理對象需要實現的接口,還可以同時指定多個接口,handler方法調用的實際處理者(其實就是幫你代理的那個對象),代理對象的方法調用都會轉發到這裏,然後直接就能生成你想要的對象類了。

下面是Proxy調用newProxyInstance的方法源碼


 public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) throws IllegalArgumentException {
    //驗證傳入的InvocationHandler不能爲空
    Objects.requireNonNull(h);
    //複製代理類實現的所有接口
    final Class<?>[] intfs = interfaces.clone();
    //獲取安全管理器
    final SecurityManager sm = System.getSecurityManager();
    //進行一些權限檢驗
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }
    //該方法先從緩存獲取代理類, 如果沒有再去生成一個代理類
    Class<?> cl = getProxyClass0(loader, intfs);
    try {
        //進行一些權限檢驗
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
        //獲取參數類型是InvocationHandler.class的代理類構造器
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        //如果代理類是不可訪問的, 就使用特權將它的構造器設置爲可訪問
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        //傳入InvocationHandler實例去構造一個代理類的實例
        //所有代理類都繼承自Proxy, 因此這裏會調用Proxy的構造器將InvocationHandler引用傳入
        return cons.newInstance(new Object[]{h});
    } catch (Exception e) {
        //爲了節省篇幅, 筆者統一用Exception捕獲了所有異常
        throw new InternalError(e.toString(), e);
    }
}

大佬的博客借鑑一下; https://www.cnblogs.com/liuyun1995/p/8157098.html

面試題9. Spring的幾種注入方式說一下?

(1)構造方法注入:

<!-- 註冊userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
	<constructor-arg ref="userDaoJdbc"></constructor-arg>
</bean>
<!-- 註冊jdbc實現的dao -->
<bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>

注意的點:

  • 如果有多個構造參數,那麼與構造方法參數列表參數的順序無關
  • 如果有多個構造方法且參數順序不同,那麼會按第一個構造方法進行注入

(2)set方法注入:

<!-- 註冊userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
	<!-- 寫法一 -->
	<!-- <property name="UserDao" ref="userDaoMyBatis"></property> -->
	<!-- 寫法二 -->
	<property name="userDao" ref="userDaoMyBatis"></property>
</bean>

<!-- 註冊mybatis實現的dao -->
<bean id="userDaoMyBatis" class="com.lyu.spring.dao.impl.UserDaoMyBatis"></bean>

注意的點:

  • 其實set方法的注入原理就是,spring會將name值的每個單詞首字母轉換成大寫,然後再在前面拼接上"set"構成一個方法名,然後去對應的類中查找該方法,通過反射調用,實現注入
  • 如果通過set方法注入屬性,那麼spring會通過默認的空參構造方法來實例化對象,所以如果在類中寫了一個帶有參數的構造方法,一定要把空參數的構造方法寫上,否則spring沒有辦法實例化對象,導致報錯。

(3)註解的方式注入:

  • @Resource:java的註解,默認以byName的方式去匹配與屬性名相同的bean的id,如果沒有找到就會以byType的方式查找,如果byType查找到多個的話,使用@Qualifier註解(spring註解)指定某個具體名稱的bean
  • @Autowired:spring註解,默認是以byType的方式去匹配類型相同的bean,如果只匹配到一個,那麼就直接注入該bean,無論要注入的 bean 的 name 是什麼;如果匹配到多個,就會調用 DefaultListableBeanFactory 的 determineAutowireCandidate 方法來決定具體注入哪個bean。determineAutowireCandidate 方法的內容如下:
// candidateBeans 爲上一步通過類型匹配到的多個bean,該 Map 中至少有兩個元素。
protected String determineAutowireCandidate(Map<String, Object> candidateBeans, DependencyDescriptor descriptor) {
    //  requiredType 爲匹配到的接口的類型
   Class<?> requiredType = descriptor.getDependencyType();
   // 1. 先找 Bean 上有@Primary 註解的,有則直接返回
   String primaryCandidate = this.determinePrimaryCandidate(candidateBeans, requiredType);
   if (primaryCandidate != null) {
       return primaryCandidate;
   } else {
       // 2.再找 Bean 上有 @Order,@PriorityOrder 註解的,有則返回
       String priorityCandidate = this.determineHighestPriorityCandidate(candidateBeans, requiredType);
       if (priorityCandidate != null) {
           return priorityCandidate;
       } else {
           Iterator var6 = candidateBeans.entrySet().iterator();

           String candidateBeanName;
           Object beanInstance;
           do {
               if (!var6.hasNext()) {
                   return null;
               }

               // 3. 再找 bean 的名稱匹配的
               Entry<String, Object> entry = (Entry)var6.next();
               candidateBeanName = (String)entry.getKey();
               beanInstance = entry.getValue();
           } while(!this.resolvableDependencies.values().contains(beanInstance) && !this.matchesBeanName(candidateBeanName, descriptor.getDependencyName()));

           return candidateBeanName;
       }
   }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章