前言
今天天氣不錯,我懷著自信的笑容來到某個大廠的研發中心,開啟面試的一天。首先我不是毫無準備的,什麼java併發,多執行緒,jvm,分散式,資料庫都準備的妥妥的,沒想到今天的面試的主題是spring。不過還好,我也準備了...門開了,走來一位拿著mac本,戴眼鏡的年輕的小夥子,跟我差不多大吧。然後他示意我坐下,禮貌的說:“歡迎來我們公司面試,今天我們就聊聊spring吧”...
面試環節
-
面試官:你說下什麼是spring?
-
我:spring是一種輕量級開發框架,旨在提高開發人員的開發效率以及系統的可維護性。我們一般說的spring框架指的是Spring Framework,它是很多模組的集合,使用這些模組可以很方便的協助我們開發。這些模組是:核心容器、資料訪問/整合、Web、AOP(面向切面程式設計)、工具、訊息和測試模組。比如:Core Container中的Core元件是Spring所有元件的核心,Beans元件和Context元件是實現IOC和依賴注入的基礎,AOP元件用來實現面向切面程式設計。
-
面試官:使用Spring框架有什麼好處呢?
-
我:框架能更讓我們高效的程式設計以及更方便的維護我們的系統。
- 輕量:Spring是輕量的,相對其他框架來說。
- 控制反轉:Spring通過控制反轉實現了鬆散耦合,物件給出他們的依賴,而不是建立或查詢依賴的物件們。
- 面向切面程式設計(AOP):Spring支援面向切面程式設計,並且把業務邏輯和系統服務分開。
- 容器:Spring包含並管理應用中物件的生命週期和配置。
- MVC框架:Spring的WEB框架是個精心設計的框架,是WEB框架的一個很好的替代品。
- 事務管理:Spring提供一個持續的事務管理介面,提供宣告式事務和程式設計式事務。
- 異常處理:Spring提供方便的API把具體技術相關的異常轉化為一致的unchecked異常。
- 面試官:你第二點提到了spring的控制反轉,能解釋下嗎?
- 我:首先來解釋下控制反轉。控制反轉(Inversion Of Control,縮寫為IOC)是一個重要的物件導向程式設計的法則來削減程式的耦合問題,也是spring框架的核心。應用控制反轉,物件在被建立的時候,由一個調控系統內的所有物件的外界實體,將其所依賴的物件的引用,傳遞給它。也可以說,依賴被注入到物件中。所以,控制反轉是關於一個物件如何獲取他所依賴的物件的引用,這個責任的反轉。另外,控制反轉一般分為兩種型別,依賴注入(Dependency Injection,簡稱DI)和依賴查詢(Dependency Lookup)。依賴注入應用比較廣泛。
還有幾個常見的問題:
- 誰依賴誰-當然是應用程式依賴於IOC容器。
- 為什麼需要依賴-應用程式需要IOC容器來提供物件需要的外部資源。
- 誰注入誰-很明顯是IOC容器注入應用程式某個物件,應用程式依賴的物件
- 注入了什麼-就是注入某個物件所需要的外部資源(包括物件、資源、常量資料)
-
面試官:那IOC與new物件有什麼區別嗎
-
我:這就是正轉與反轉的區別。傳統應用程式是由我們自己在物件中主動控制去直接獲取依賴物件,也就是正轉。而反轉則是容器來幫助我們建立並注入依賴物件。
-
面試官:好的,那IOC有什麼優缺點嗎?
-
我:優點:很明顯,實現了元件之間的解耦,提高程式的靈活性和可維護性。缺點:物件生成因為是反射程式設計,在效率上有些損耗。但相對於IOC提高的維護性和靈活性來說,這點損耗是微不足道的,除非某物件的生成對效率要求特別高。
-
面試官:spring管理這麼多物件,肯定需要一個容器吧。你能說下對IOC容器的理解嗎?
-
我:首先來解釋下容器:在每個框架中都有個容器的概念,所謂的容器就是將常用的服務封裝起來,然後使用者只需要遵循一定的規則就可以達到統一、靈活、安全、方便和快速的目的。
-
我:然後IOC容器是具有依賴注入功能的容器,負責例項化、定位、配置應用程式中的物件以及建立這些物件間的依賴。
-
面試官:那你能說下IOC容器是怎麼工作的嗎?
-
我:首先說下兩個概念。
- Bean的概念:Bean就是由Spring容器初始化、裝配及管理的物件,除此之外,bean就與應用程式中的其他物件沒什麼區別了。
- 後設資料BeanDefinition:確定如何例項化Bean、管理bean之間的依賴關係以及管理bean,這就需要配置後設資料,在spring中由BeanDefinition代表。
- 我:下面說下工作原理:
- 準備配置檔案:配置檔案中宣告Bean定義也就是為Bean配置後設資料。
- 由IOC容器進行解析後設資料:IOC容器的Bean Reader讀取並解析配置檔案,根據定義生成BeanDefinition配置後設資料物件,IOC容器根據BeanDefinition進行例項化、配置以及組裝Bean。
- 例項化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的區別嗎?
- 我:
- BeanFactory是spring中最基礎的介面。它負責讀取讀取bean配置文件,管理bean的載入,例項化,維護bean之間的依賴關係,負責bean的生命週期。
- ApplicationContext是BeanFactory的子介面,除了提供上述BeanFactory的所有功能外,還提供了更完整的框架功能:如國際化支援,資源訪問,事件傳遞等。常用的獲取ApplicationContext的方法: 2.1 FileSystemXmlApplicationContext:從檔案系統或者url指定的xml配置檔案建立,引數為配置檔名或者檔名陣列。 2.2 ClassPathXmlApplicationContext:從classpath的xml配置檔案建立,可以從jar包中讀取配置檔案 2.3 WebApplicationContextUtils:從web應用的根目錄讀取配置檔案,需要先在web.xml中配置,可以配置監聽器或者servlet來實現。
- ApplicationContext的初始化和BeanFactory有一個重大區別:BeanFactory在初始化容器時,並未例項化Bean,知道第一次訪問某個Bean時才例項化Bean;而ApplicationContext則在初始化應用上下文時就例項化所有的單例Bean,因此ApplicationContext的初始化時間會比BeanFactory稍長一些。
- 面試官:很好,看來對spring的IOC容器掌握的不錯。那我們來聊聊spring的aop,你說下你對spring aop的瞭解。
- 我:Aop(面向切面程式設計)能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的擴充套件性和可維護性。這裡聚個日誌處理的栗子:
日誌處理方式 | 實現方式 | 優缺點 |
---|---|---|
硬程式碼編寫 | ||
處理程式碼相同,程式碼強耦合 | ||
抽離方法,程式碼複用 | ||
手動插入程式碼,程式碼強耦合 | ||
aop | ||
橫向的功能抽離出來形成一個獨立的模組,低耦合 |
-
面試官:那你知道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有五種作用域:
- singleton:唯一Bean例項,Spring中的Bean預設都是單例的。
- prototype:每次請求都會建立一個新的bean例項。
- request:每次HTTP請求都會產生一個新的Bean,該Bean僅在當前HTTP request內有效。
- session:每次HTTP請產生一個新的Bean,該Bean僅在當前HTTP session內有效。
- global-session:全域性session作用域,僅僅在基於portlet的web應用中才有意義,Spring5已經沒有了。
- 面試官:Spring中的單例Bean的執行緒安全問題了解嗎?
- 我:大部分時候我們並沒有在系統中使用多執行緒。單例Bean存線上程安全問題,主要是因為當多個執行緒操作同一個物件的時候,對這個物件的非靜態成員變數的寫操作會存線上程安全問題。常見的有兩種解決方法:
- 在Bean中儘量避免定義可變的成員變數(不太現實)。
- 在類中定義一個ThreadLocal成員變數,將需要的可變成員變數儲存在Threadlocal中。
- 面試官:Spring中的Bean的生命週期你瞭解嗎?
- (我心想,這個過程還挺複雜的,還好來之前小本本記了。)Spring中的Bean從建立到銷燬大概會經過這些:
- Bean容器找到配置檔案中Spring Bean的定義。
- Bean容器利用Java反射機制建立一個Bean的例項。
- 如果涉及一些屬性值,利用set()方法設定一些屬性值。
- 如果Bean實現了BeanNameAware介面,呼叫setBeanName()方法,傳入Bean的名稱。
- 如果Bean實現了BeanClassLoaderAware介面,呼叫setBeanClassLoader()方法,傳入ClassLoader物件的例項。
- 如果Bean實現了BeanFactoryAware介面,呼叫setBeanClassLoader()方法,傳入ClassLoader物件的例項。
- 與上面類似,如果實現了其他*.Aware介面,就呼叫相應的方法。
- 如果有和載入這個Bean的Spring容器相關的BeaPostProcessor物件,執行postProcessBeforeInitialization()方法
- 如果Bean實現了InitializingBean介面,執行afterPropertiesSet()方法
- 如果Bean在配置檔案中的定義包含init-method屬性,執行指定的方法。
- 如果有和載入這個 Bean的 Spring 容器相關的 BeanPostProcessor 物件,執行postProcessAfterInitialization() 方法
- 當要銷燬Bean的時候,如果 Bean 實現了 DisposableBean 介面,執行 destroy() 方法。
- 當要銷燬 Bean 的時候,如果 Bean 在配置檔案中的定義包含 destroy-method 屬性,執行指定的方法。
- 面試官:將一個類宣告為Spring的Bean的註解有哪些你知道嗎?
- 我:我們一般用@Autowried註解自動裝配Bean,要想把類識別為可用於自動裝配的Bean,採用以下註解可以實現:
- @Component:通用的註解,可標註任意類為spring元件。如果一個Bean不知道屬於哪個層,可以使用@Component註解標註
- @Repository:對應持久層即Dao層,主要用於資料庫的操作。
- @Service:對應服務層,主要涉及一些複雜的邏輯
- @Controller:對應Spring MVC控制層,主要用於接收使用者請求並呼叫Service層返回資料給前端頁面。
- 面試官:那@Component和@Bean有什麼區別呢?
- 我:那我來總結下:
- 作用物件不同:@Component作用於類,@Bean作用於方法。
- @Component通常是通過類路徑掃描來自動偵測以及自動裝配到Spring容器中(使用@ComponentScan註解定義要掃描的路徑從中找出識別了需要裝配的類自動裝配到spring的Bean容器中)。@Bean註解通常是在標有該註解的方法中定義產生這個bean,@Bean告訴Spring這是某個類的例項,當我需要用它的時候還給我。
- @Bean註解比@Component註解的自定義性更強,而且很多地方只能通過@Bean註解來註冊Bean,比如第三方庫中的類。
- 面試官:看來你對Spring的bean掌握的不錯,那你能說下自己對於spring MVC的瞭解嗎?
- 我:談到這個問題,不得不說下Model1和Model2這兩個沒有spring MVC的時代。
- Model1時代:整個Web應用幾乎都是JSP頁面組成,只用少量的JavaBean來處理資料庫連線,訪問等操作。這個模式下JSP既是控制層又是表現層。顯而易見這種模式存在很多問題:比如講控制層和表現層邏輯混雜在一起,導致程式碼重用率極低;前後端相互依賴,難以進行測試並且開發效率極低。
- 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 MVC從接受請求到返回資料的整個流程嗎?
- (我心想:幸好我還沒忘)我:可以。這個流程雖然複雜,但是理解起來也不是很難。
- 客戶端(瀏覽器)傳送請求,直接請求到DispatcherServlet。
- DispatcherServlet根據請求細膩呼叫HandlerMapping,解析請求對應的Handler。
- 解析到對應的Handler(也就是Controller)後,開始由HandlerAdapter介面卡處理。
- HandlerAdapter會根據Handler來呼叫真正的處理器來處理請求,並處理相應的業務邏輯。
- 處理器處理完業務後,會返回一個ModelAndView物件,Model是返回的資料物件,View是個邏輯上的View。
- ViewResolver會根據View查詢實際的View。
- DispatcherServlet把返回的Model傳給View(檢視渲染)。
- 把View返回給請求者(瀏覽器)。
- 面試官:你知道Spring框架中用到了哪些設計模式嗎?
- 我:那我來總結一下。
- 工廠設計模式:Spring使用工廠模式通過BeanFactory、ApplicationContext建立Bean物件。
- 代理設計模式:Spring AOP功能的實現。
- 單例設計模式:Spring中的Bean預設都是單例的。
- 模板方法模式:Spring中jdbcTemplate、hibernateTemplate等以Template結尾的對資料庫操作的類,就是用到了模板模式。
- 包裝器設計模式:我們的專案需要連結多個資料庫,而且不同的客戶在每次訪問中根據需要會去訪問不同的資料庫。這種模式讓我們可以根據客戶需求都太切換不同的資料來源。
- 觀察者模式:Spring事件驅動模型就是觀察者模式很經典的一個應用。
- 介面卡模式:Spring AOP的增強或通知使用到了介面卡模式。SpringMVC中也是用到了介面卡模式適配Controller。
- 面試官:你使用過Spring的事務嗎?是怎麼用的?
- 我:當然用過。Spring管理事務有兩種方式:
- 程式設計式事務:在程式碼中硬編碼(不推薦使用)
- 宣告式事務:在配置檔案中配置,宣告式事務又分為兩種:基於XML的方式和基於註解的方式(推薦使用) 在專案中使用Spring的事務只需要在你需要事務的方法上加上@Transaction註解,那麼這個方法就加上了事務,如果遇到異常,整個方法中的資料修改的邏輯都會被回滾掉,避免造成資料的不一致性。
- 面試官:那Spring的事務有哪幾種隔離級別?
- 我:TransactionDefinition介面中定義了五個隔離級別的常量:
- ISOLATION_DEFAULT:使用後端資料庫預設的隔離級別(一般用這個就好了),MySQL預設採用的是REPEATABLE_READ隔離級別,Oracle預設採用的是READ_COMMITTED隔離級別。
- ISOLATION_READ_UNCOMMITTED:最低的隔離級別,允許讀取尚未提交的資料,可能導致髒讀、幻讀或不可重複讀。
- ISOLATION_READ_COMMITTED:允許讀取併發事務以及提交的資料,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。
- ISOLATION_REPEATABLE_READ:對同一欄位的多次讀取結果都是一致的,除非資料是被事務自己修改的,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
- ISOLATION_SERIALIZABLE:最高的隔離級別,所有事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀和幻讀。但是這將嚴重影響程式效能,通常也不會用到。
- 面試官:那你知道Spring事務有哪幾種事務傳播行為嗎?(面試官露出神祕的一笑)
- 我:(心想:Spring事務中這裡的坑踩的最多,怎麼會不清楚呢)在TransactionDefinition中定義了7種事務傳播行為: 支援當前事務的情況
- PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
- PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
- PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常(mandatory:強制) 不支援當前事務的情況
- PROPAGATION_REQUIRES_NEW:建立一個新的事務,如果當前存在事務,則把當前事務掛起。
- PROPAGATION_NOT_SUPPORTED:以非事務的方式執行,如果當前存在事務,則把當前事務掛起。
- PROPAGATION_NEVER:以非事務的方式執行,如果當前存在事務,則丟擲異常。 其他情況
- PROPAGATION_NESTED:如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於PROPAGATION_REQUIRED。
- 我:一篇有價值的部落格,列舉了Spring事務不生效的幾大原因https://blog.csdn.net/f641385712/article/details/80445933。這裡我列舉一下:
- 原因1:是否是資料庫引擎設定不對造成的。比如我們常用的mysql,引擎MyISAM,是不支援事務操作的,需要改成InnoDB才能支援。
- 原因2:入口的方法必須是public,否則事務不起作用(這一點由Spring的AOP特性決定)private方法,final方法和static方法不能新增事務,加了也不生效。
- 原因3:Spring事務管理預設只對出現執行時異常(kava.lang.RuntimeException及其子類)進行回滾(至於Spring為什麼這麼設計:因為Spring認為Checked異常屬於業務,程式設計師應該給出解決方案而不應該直接扔給框架)。
- 原因4:@EnableTransactionManagement // 啟註解事務管理,等同於xml配置方式的 <tx:annotation-driven />。沒有使用該註解開啟事務。
- 原因5:請確認你的類是否被代理了。(因為Spring的事務實現原理是AOP,只有通過代理物件呼叫方法才能被攔截,事務才能生效)。
- 原因6:請確保你的業務和事務入口在同一個執行緒裡,否則事務也是不生效的,比如下面的程式碼:
@Transactional
@Override
public void save(User user1, User user2) {
new Thread(() -> {
saveError(user1, user2);
System.out.println(1 / 0);
}).start();
}
複製程式碼
- 原因7:同一個類中一個無事務的方法呼叫另一個有事務的方法,事務是不會起作用的。例如下面的程式碼:
- 面試官:看來你對spring框架的重點知識掌握的還不錯,基礎很紮實,今天我們就先聊到這裡,希望明天你能表現的更好。
- 我微笑著說:(暗自慶幸:通過了一關是一關)嗯,我會好好準備的。