《今天面試了嗎》-Spring

堅持就是勝利發表於2020-03-15

前言

今天天氣不錯,我懷著自信的笑容來到某個大廠的研發中心,開啟面試的一天。首先我不是毫無準備的,什麼java併發,多執行緒,jvm,分散式,資料庫都準備的妥妥的,沒想到今天的面試的主題是spring。不過還好,我也準備了...門開了,走來一位拿著mac本,戴眼鏡的年輕的小夥子,跟我差不多大吧。然後他示意我坐下,禮貌的說:“歡迎來我們公司面試,今天我們就聊聊spring吧”...

面試環節

  • 面試官:你說下什麼是spring?

  • 我:spring是一種輕量級開發框架,旨在提高開發人員的開發效率以及系統的可維護性。我們一般說的spring框架指的是Spring Framework,它是很多模組的集合,使用這些模組可以很方便的協助我們開發。這些模組是:核心容器、資料訪問/整合、Web、AOP(面向切面程式設計)、工具、訊息和測試模組。比如:Core Container中的Core元件是Spring所有元件的核心,Beans元件和Context元件是實現IOC和依賴注入的基礎,AOP元件用來實現面向切面程式設計。

  • 面試官:使用Spring框架有什麼好處呢?

  • 我:框架能更讓我們高效的程式設計以及更方便的維護我們的系統。

  1. 輕量:Spring是輕量的,相對其他框架來說。
  2. 控制反轉:Spring通過控制反轉實現了鬆散耦合,物件給出他們的依賴,而不是建立或查詢依賴的物件們。
  3. 面向切面程式設計(AOP):Spring支援面向切面程式設計,並且把業務邏輯和系統服務分開。
  4. 容器:Spring包含並管理應用中物件的生命週期和配置。
  5. MVC框架:Spring的WEB框架是個精心設計的框架,是WEB框架的一個很好的替代品。
  6. 事務管理:Spring提供一個持續的事務管理介面,提供宣告式事務和程式設計式事務。
  7. 異常處理:Spring提供方便的API把具體技術相關的異常轉化為一致的unchecked異常。
  • 面試官:你第二點提到了spring的控制反轉,能解釋下嗎?
  • 我:首先來解釋下控制反轉。控制反轉(Inversion Of Control,縮寫為IOC)是一個重要的物件導向程式設計的法則來削減程式的耦合問題,也是spring框架的核心。應用控制反轉,物件在被建立的時候,由一個調控系統內的所有物件的外界實體,將其所依賴的物件的引用,傳遞給它。也可以說,依賴被注入到物件中。所以,控制反轉是關於一個物件如何獲取他所依賴的物件的引用,這個責任的反轉。另外,控制反轉一般分為兩種型別,依賴注入(Dependency Injection,簡稱DI)和依賴查詢(Dependency Lookup)。依賴注入應用比較廣泛。
    還有幾個常見的問題:
  1. 誰依賴誰-當然是應用程式依賴於IOC容器。
  2. 為什麼需要依賴-應用程式需要IOC容器來提供物件需要的外部資源。
  3. 誰注入誰-很明顯是IOC容器注入應用程式某個物件,應用程式依賴的物件
  4. 注入了什麼-就是注入某個物件所需要的外部資源(包括物件、資源、常量資料)
  • 面試官:那IOC與new物件有什麼區別嗎

  • 我:這就是正轉與反轉的區別。傳統應用程式是由我們自己在物件中主動控制去直接獲取依賴物件,也就是正轉。而反轉則是容器來幫助我們建立並注入依賴物件。

  • 面試官:好的,那IOC有什麼優缺點嗎?

  • 我:優點:很明顯,實現了元件之間的解耦,提高程式的靈活性和可維護性。缺點:物件生成因為是反射程式設計,在效率上有些損耗。但相對於IOC提高的維護性和靈活性來說,這點損耗是微不足道的,除非某物件的生成對效率要求特別高。

  • 面試官:spring管理這麼多物件,肯定需要一個容器吧。你能說下對IOC容器的理解嗎?

  • 我:首先來解釋下容器:在每個框架中都有個容器的概念,所謂的容器就是將常用的服務封裝起來,然後使用者只需要遵循一定的規則就可以達到統一、靈活、安全、方便和快速的目的。

  • 我:然後IOC容器是具有依賴注入功能的容器,負責例項化、定位、配置應用程式中的物件以及建立這些物件間的依賴。

  • 面試官:那你能說下IOC容器是怎麼工作的嗎?

  • 我:首先說下兩個概念。

  1. Bean的概念:Bean就是由Spring容器初始化、裝配及管理的物件,除此之外,bean就與應用程式中的其他物件沒什麼區別了。
  2. 後設資料BeanDefinition:確定如何例項化Bean、管理bean之間的依賴關係以及管理bean,這就需要配置後設資料,在spring中由BeanDefinition代表。
  • 我:下面說下工作原理:
  1. 準備配置檔案:配置檔案中宣告Bean定義也就是為Bean配置後設資料。
  2. 由IOC容器進行解析後設資料:IOC容器的Bean Reader讀取並解析配置檔案,根據定義生成BeanDefinition配置後設資料物件,IOC容器根據BeanDefinition進行例項化、配置以及組裝Bean。
  3. 例項化IOC容器:由客戶端例項化容器,獲取需要的Bean。
    下面舉個例子:
@Test  
       public void testHelloWorld() {  
             //1、讀取配置檔案例項化一個IoC容器  
             ApplicationContext context = new ClassPathXmlApplicationContext("helloworld.xml");  
             //2、從容器中獲取Bean,注意此處完全“面向介面程式設計,而不是面向實現”  
              HelloApi helloApi = context.getBean("hello", HelloApi.class);  
              //3、執行業務邏輯  
              helloApi.sayHello();  
       }
複製程式碼
  • 面試官:那你知道BeanFactory和ApplicationContext的區別嗎?
  • 我:
  1. BeanFactory是spring中最基礎的介面。它負責讀取讀取bean配置文件,管理bean的載入,例項化,維護bean之間的依賴關係,負責bean的生命週期。
  2. ApplicationContext是BeanFactory的子介面,除了提供上述BeanFactory的所有功能外,還提供了更完整的框架功能:如國際化支援,資源訪問,事件傳遞等。常用的獲取ApplicationContext的方法: 2.1 FileSystemXmlApplicationContext:從檔案系統或者url指定的xml配置檔案建立,引數為配置檔名或者檔名陣列。 2.2 ClassPathXmlApplicationContext:從classpath的xml配置檔案建立,可以從jar包中讀取配置檔案 2.3 WebApplicationContextUtils:從web應用的根目錄讀取配置檔案,需要先在web.xml中配置,可以配置監聽器或者servlet來實現。
  3. ApplicationContext的初始化和BeanFactory有一個重大區別:BeanFactory在初始化容器時,並未例項化Bean,知道第一次訪問某個Bean時才例項化Bean;而ApplicationContext則在初始化應用上下文時就例項化所有的單例Bean,因此ApplicationContext的初始化時間會比BeanFactory稍長一些。
  • 面試官:很好,看來對spring的IOC容器掌握的不錯。那我們來聊聊spring的aop,你說下你對spring aop的瞭解。
  • 我:Aop(面向切面程式設計)能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的擴充套件性和可維護性。這裡聚個日誌處理的栗子:
日誌處理方式 實現方式 優缺點
硬程式碼編寫
《今天面試了嗎》-Spring
處理程式碼相同,程式碼強耦合
抽離方法,程式碼複用
《今天面試了嗎》-Spring
手動插入程式碼,程式碼強耦合
aop
《今天面試了嗎》-Spring
橫向的功能抽離出來形成一個獨立的模組,低耦合
  • 面試官:那你知道spring aop的原理嗎?

  • 我:spring aop就是基於動態代理的,如果要代理的物件實現了某個介面,那麼spring aop會使用jdk proxy,去建立代理物件,而對於沒有實現介面的物件,就無法使用jdk的動態代理,這時spring aop會使用cglib動態代理,這時候spring aop會使用cglib生成一個被代理物件的子類作為代理。

  • 我:關於動態代理的原理可以參考我的這篇文章:juejin.im/post/5cea01…

  • 面試官:那你知道Spring Aop和AspecJ Aop有什麼區別嗎?

  • 我:Spring AOP屬於執行時增強,而AspectJ是編譯時增強。Spring Aop基於代理,而AspectJ基於位元組碼操作。Spring Aop已經整合了AspectJ,AspectJ應該算得上Java生態系統中最完整的AOP框架了。AspectJ相對於Spring Aop功能更加強大,但是Spring AOP相對來說更簡單。如果我們的切面比較少,那麼兩者效能差異不大。但是,當且切面太多的話,最好選擇AspectJ,它比Spring Aop快很多。

  • 面試官:你對Spring中的bean瞭解嗎?都有哪些作用域?

  • 我:Spring中的Bean有五種作用域:

  1. singleton:唯一Bean例項,Spring中的Bean預設都是單例的。
  2. prototype:每次請求都會建立一個新的bean例項。
  3. request:每次HTTP請求都會產生一個新的Bean,該Bean僅在當前HTTP request內有效。
  4. session:每次HTTP請產生一個新的Bean,該Bean僅在當前HTTP session內有效。
  5. global-session:全域性session作用域,僅僅在基於portlet的web應用中才有意義,Spring5已經沒有了。
  • 面試官:Spring中的單例Bean的執行緒安全問題了解嗎?
  • 我:大部分時候我們並沒有在系統中使用多執行緒。單例Bean存線上程安全問題,主要是因為當多個執行緒操作同一個物件的時候,對這個物件的非靜態成員變數的寫操作會存線上程安全問題。常見的有兩種解決方法:
  1. 在Bean中儘量避免定義可變的成員變數(不太現實)。
  2. 在類中定義一個ThreadLocal成員變數,將需要的可變成員變數儲存在Threadlocal中。
  • 面試官:Spring中的Bean的生命週期你瞭解嗎?
  • (我心想,這個過程還挺複雜的,還好來之前小本本記了。)Spring中的Bean從建立到銷燬大概會經過這些:
  1. Bean容器找到配置檔案中Spring Bean的定義。
  2. Bean容器利用Java反射機制建立一個Bean的例項。
  3. 如果涉及一些屬性值,利用set()方法設定一些屬性值。
  4. 如果Bean實現了BeanNameAware介面,呼叫setBeanName()方法,傳入Bean的名稱。
  5. 如果Bean實現了BeanClassLoaderAware介面,呼叫setBeanClassLoader()方法,傳入ClassLoader物件的例項。
  6. 如果Bean實現了BeanFactoryAware介面,呼叫setBeanClassLoader()方法,傳入ClassLoader物件的例項。
  7. 與上面類似,如果實現了其他*.Aware介面,就呼叫相應的方法。
  8. 如果有和載入這個Bean的Spring容器相關的BeaPostProcessor物件,執行postProcessBeforeInitialization()方法
  9. 如果Bean實現了InitializingBean介面,執行afterPropertiesSet()方法
  10. 如果Bean在配置檔案中的定義包含init-method屬性,執行指定的方法。
  11. 如果有和載入這個 Bean的 Spring 容器相關的 BeanPostProcessor 物件,執行postProcessAfterInitialization() 方法
  12. 當要銷燬Bean的時候,如果 Bean 實現了 DisposableBean 介面,執行 destroy() 方法。
  13. 當要銷燬 Bean 的時候,如果 Bean 在配置檔案中的定義包含 destroy-method 屬性,執行指定的方法。

《今天面試了嗎》-Spring

  • 面試官:將一個類宣告為Spring的Bean的註解有哪些你知道嗎?
  • 我:我們一般用@Autowried註解自動裝配Bean,要想把類識別為可用於自動裝配的Bean,採用以下註解可以實現:
  1. @Component:通用的註解,可標註任意類為spring元件。如果一個Bean不知道屬於哪個層,可以使用@Component註解標註
  2. @Repository:對應持久層即Dao層,主要用於資料庫的操作。
  3. @Service:對應服務層,主要涉及一些複雜的邏輯
  4. @Controller:對應Spring MVC控制層,主要用於接收使用者請求並呼叫Service層返回資料給前端頁面。
  • 面試官:那@Component和@Bean有什麼區別呢?
  • 我:那我來總結下:
  1. 作用物件不同:@Component作用於類,@Bean作用於方法。
  2. @Component通常是通過類路徑掃描來自動偵測以及自動裝配到Spring容器中(使用@ComponentScan註解定義要掃描的路徑從中找出識別了需要裝配的類自動裝配到spring的Bean容器中)。@Bean註解通常是在標有該註解的方法中定義產生這個bean,@Bean告訴Spring這是某個類的例項,當我需要用它的時候還給我。
  3. @Bean註解比@Component註解的自定義性更強,而且很多地方只能通過@Bean註解來註冊Bean,比如第三方庫中的類。
  • 面試官:看來你對Spring的bean掌握的不錯,那你能說下自己對於spring MVC的瞭解嗎?
  • 我:談到這個問題,不得不說下Model1和Model2這兩個沒有spring MVC的時代。
  1. Model1時代:整個Web應用幾乎都是JSP頁面組成,只用少量的JavaBean來處理資料庫連線,訪問等操作。這個模式下JSP既是控制層又是表現層。顯而易見這種模式存在很多問題:比如講控制層和表現層邏輯混雜在一起,導致程式碼重用率極低;前後端相互依賴,難以進行測試並且開發效率極低。
  2. Model2時代:學過Servlet的朋友應該瞭解“Java Bean(Model)+JSP(VIEW)+Servlet(Controller)”這種開發模式就是早期的JavaWeb開發模式。Model2模式下還存在很多問題,抽象和封裝程度還遠遠不夠,使用Model2進行開發時不可避免的會重複造輪子。
  • 我想了想,接著說:MVC是一種設計模式,Spring MVC是一款很優秀的MVC框架。Spring MVC可以幫助我們進行更簡潔的Web層的開發,並且它天生與Spring框架整合。Spring MVC下我們一般把後端專案分為Service層(處理業務)、Dao層(資料庫操作)、Entity層(實體類)、Controller層(控制層、返回資料給前端)。我畫個Spring MVC的簡單原理圖:

《今天面試了嗎》-Spring

  • 面試官:你能詳細說下Spring MVC從接受請求到返回資料的整個流程嗎?
  • (我心想:幸好我還沒忘)我:可以。這個流程雖然複雜,但是理解起來也不是很難。
  1. 客戶端(瀏覽器)傳送請求,直接請求到DispatcherServlet。
  2. DispatcherServlet根據請求細膩呼叫HandlerMapping,解析請求對應的Handler。
  3. 解析到對應的Handler(也就是Controller)後,開始由HandlerAdapter介面卡處理。
  4. HandlerAdapter會根據Handler來呼叫真正的處理器來處理請求,並處理相應的業務邏輯。
  5. 處理器處理完業務後,會返回一個ModelAndView物件,Model是返回的資料物件,View是個邏輯上的View。
  6. ViewResolver會根據View查詢實際的View。
  7. DispatcherServlet把返回的Model傳給View(檢視渲染)。
  8. 把View返回給請求者(瀏覽器)。
  • 面試官:你知道Spring框架中用到了哪些設計模式嗎?
  • 我:那我來總結一下。
  1. 工廠設計模式:Spring使用工廠模式通過BeanFactory、ApplicationContext建立Bean物件。
  2. 代理設計模式:Spring AOP功能的實現。
  3. 單例設計模式:Spring中的Bean預設都是單例的。
  4. 模板方法模式:Spring中jdbcTemplate、hibernateTemplate等以Template結尾的對資料庫操作的類,就是用到了模板模式。
  5. 包裝器設計模式:我們的專案需要連結多個資料庫,而且不同的客戶在每次訪問中根據需要會去訪問不同的資料庫。這種模式讓我們可以根據客戶需求都太切換不同的資料來源。
  6. 觀察者模式:Spring事件驅動模型就是觀察者模式很經典的一個應用。
  7. 介面卡模式:Spring AOP的增強或通知使用到了介面卡模式。SpringMVC中也是用到了介面卡模式適配Controller。
  • 面試官:你使用過Spring的事務嗎?是怎麼用的?
  • 我:當然用過。Spring管理事務有兩種方式:
  1. 程式設計式事務:在程式碼中硬編碼(不推薦使用)
  2. 宣告式事務:在配置檔案中配置,宣告式事務又分為兩種:基於XML的方式和基於註解的方式(推薦使用) 在專案中使用Spring的事務只需要在你需要事務的方法上加上@Transaction註解,那麼這個方法就加上了事務,如果遇到異常,整個方法中的資料修改的邏輯都會被回滾掉,避免造成資料的不一致性。
  • 面試官:那Spring的事務有哪幾種隔離級別?
  • 我:TransactionDefinition介面中定義了五個隔離級別的常量:
  1. ISOLATION_DEFAULT:使用後端資料庫預設的隔離級別(一般用這個就好了),MySQL預設採用的是REPEATABLE_READ隔離級別,Oracle預設採用的是READ_COMMITTED隔離級別。
  2. ISOLATION_READ_UNCOMMITTED:最低的隔離級別,允許讀取尚未提交的資料,可能導致髒讀、幻讀或不可重複讀。
  3. ISOLATION_READ_COMMITTED:允許讀取併發事務以及提交的資料,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。
  4. ISOLATION_REPEATABLE_READ:對同一欄位的多次讀取結果都是一致的,除非資料是被事務自己修改的,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
  5. ISOLATION_SERIALIZABLE:最高的隔離級別,所有事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀和幻讀。但是這將嚴重影響程式效能,通常也不會用到。
  • 面試官:那你知道Spring事務有哪幾種事務傳播行為嗎?(面試官露出神祕的一笑)
  • 我:(心想:Spring事務中這裡的坑踩的最多,怎麼會不清楚呢)在TransactionDefinition中定義了7種事務傳播行為: 支援當前事務的情況
  1. PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
  2. PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
  3. PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常(mandatory:強制) 不支援當前事務的情況
  4. PROPAGATION_REQUIRES_NEW:建立一個新的事務,如果當前存在事務,則把當前事務掛起。
  5. PROPAGATION_NOT_SUPPORTED:以非事務的方式執行,如果當前存在事務,則把當前事務掛起。
  6. PROPAGATION_NEVER:以非事務的方式執行,如果當前存在事務,則丟擲異常。 其他情況
  7. PROPAGATION_NESTED:如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於PROPAGATION_REQUIRED。
  • 我:一篇有價值的部落格,列舉了Spring事務不生效的幾大原因https://blog.csdn.net/f641385712/article/details/80445933。這裡我列舉一下:
  1. 原因1:是否是資料庫引擎設定不對造成的。比如我們常用的mysql,引擎MyISAM,是不支援事務操作的,需要改成InnoDB才能支援。
  2. 原因2:入口的方法必須是public,否則事務不起作用(這一點由Spring的AOP特性決定)private方法,final方法和static方法不能新增事務,加了也不生效。
  3. 原因3:Spring事務管理預設只對出現執行時異常(kava.lang.RuntimeException及其子類)進行回滾(至於Spring為什麼這麼設計:因為Spring認為Checked異常屬於業務,程式設計師應該給出解決方案而不應該直接扔給框架)。
  4. 原因4:@EnableTransactionManagement // 啟註解事務管理,等同於xml配置方式的 <tx:annotation-driven />。沒有使用該註解開啟事務。
  5. 原因5:請確認你的類是否被代理了。(因為Spring的事務實現原理是AOP,只有通過代理物件呼叫方法才能被攔截,事務才能生效)。
  6. 原因6:請確保你的業務和事務入口在同一個執行緒裡,否則事務也是不生效的,比如下面的程式碼:
@Transactional
@Override
public void save(User user1, User user2) {
    new Thread(() -> {
          saveError(user1, user2);
          System.out.println(1 / 0);
    }).start();
}

複製程式碼
  1. 原因7:同一個類中一個無事務的方法呼叫另一個有事務的方法,事務是不會起作用的。例如下面的程式碼:

《今天面試了嗎》-Spring

  • 面試官:看來你對spring框架的重點知識掌握的還不錯,基礎很紮實,今天我們就先聊到這裡,希望明天你能表現的更好。
  • 我微笑著說:(暗自慶幸:通過了一關是一關)嗯,我會好好準備的。

相關文章