spring + spring mvc + tomcat 面試題(史上最全)


文章很長,建議收藏起來,慢慢讀! Java 高並發 發燒友社群:瘋狂創客圈 奉上以下珍貴的學習資源:


史上最全 Java 面試題 30 專題 總目錄

精心梳理、吐血推薦、史上最強、建議收藏 阿里、京東、美團、頭條.... 隨意挑、橫着走!!!
1.Java算法面試題(史上最強、持續更新、吐血推薦) 2.Java基礎面試題(史上最全、持續更新、吐血推薦)
3.JVM面試題(史上最強、持續更新、吐血推薦) 4、架構設計面試題 (史上最全、持續更新、吐血推薦)
5、Spring面試題 專題 6、SpringMVC面試題 專題
7.SpringBoot - 面試題(史上最強、持續更新) 8、Tomcat面試題 專題部分
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 10、TCP/IP協議(圖解+秒懂+史上最全)
11.JUC並發包與容器 - 面試題(史上最強、持續更新) 12、設計模式面試題 (史上最全、持續更新、吐血推薦)
13.死鎖面試題(史上最強、持續更新) 15.Zookeeper 分布式鎖 (圖解+秒懂+史上最全)
14、Redis 面試題 - 收藏版(史上最強、持續更新) 16、Zookeeper 面試題(史上最強、持續更新)
17、分布式事務面試題 (史上最全、持續更新、吐血推薦) 18、一致性協議 (史上最全)
19、Zab協議 (史上最全) 20、Paxos 圖解 (秒懂)
21、raft 圖解 (秒懂) 26、消息隊列、RabbitMQ、Kafka、RocketMQ面試題 (史上最全、持續更新)
22.Linux面試題(史上最全、持續更新、吐血推薦) 23、Mysql 面試題(史上最強、持續更新)
24、SpringCloud 面試題 - 收藏版(史上最強、持續更新) 25、Netty 面試題 (史上最強、持續更新)
27、內存泄漏 內存溢出(史上最全) 28、JVM 內存溢出 實戰 (史上最全)
29、多線程面試題(史上最全) 30、HR面經:過五關斬六將后,小心陰溝翻船!(史上最全)

Spring面試題 專題部分

什么是spring?

Spring是一個輕量級Java開發框架,最早有Rod Johnson創建,目的是為了解決企業級應用開發的業務邏輯層和其他各層的耦合問題。它是一個分層的JavaSE/JavaEE full-stack(一站式)輕量級開源框架,為開發Java應用程序提供全面的基礎架構支持。Spring負責基礎架構,因此Java開發者可以專注於應用程序的開發。

Spring最根本的使命是解決企業級應用開發的復雜性,即簡化Java開發

Spring可以做很多事情,它為企業級開發提供給了豐富的功能,但是這些功能的底層都依賴於它的兩個核心特性,也就是依賴注入(dependency injection,DI)和面向切面編程(aspect-oriented programming,AOP)

為了降低Java開發的復雜性,Spring采取了以下4種關鍵策略

  • 基於POJO的輕量級和最小侵入性編程;
  • 通過依賴注入和面向接口實現松耦合;
  • 基於切面和慣例進行聲明式編程;
  • 通過切面和模板減少樣板式代碼。

Spring框架的設計目標,設計理念,和核心是什么

Spring設計目標:Spring為開發者提供一個一站式輕量級應用開發平台;

Spring設計理念:在JavaEE開發中,支持POJO和JavaBean開發方式,使應用面向接口開發,充分支持OO(面向對象)設計方法;Spring通過IoC容器實現對象耦合關系的管理,並實現依賴反轉,將對象之間的依賴關系交給IoC容器,實現解耦;

Spring框架的核心:IoC容器和AOP模塊。通過IoC容器管理POJO對象以及他們之間的耦合關系;通過AOP以動態非侵入的方式增強服務。

IoC讓相互協作的組件保持松散的耦合,而AOP編程允許你把遍布於應用各層的功能分離出來形成可重用的功能組件。

Spring的優缺點是什么?

優點

  • 方便解耦,簡化開發

    Spring就是一個大工廠,可以將所有對象的創建和依賴關系的維護,交給Spring管理。

  • AOP編程的支持

    Spring提供面向切面編程,可以方便的實現對程序進行權限攔截、運行監控等功能。

  • 聲明式事務的支持

    只需要通過配置就可以完成對事務的管理,而無需手動編程。

  • 方便程序的測試

    Spring對Junit4支持,可以通過注解方便的測試Spring程序。

  • 方便集成各種優秀框架

    Spring不排斥各種優秀的開源框架,其內部提供了對各種優秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。

  • 降低JavaEE API的使用難度

    Spring對JavaEE開發中非常難用的一些API(JDBC、JavaMail、遠程調用等),都提供了封裝,使這些API應用難度大大降低。

缺點

  • Spring明明一個很輕量級的框架,卻給人感覺大而全
  • Spring依賴反射,反射影響性能
  • 使用門檻升高,入門Spring需要較長時間

Spring有哪些應用場景

應用場景:JavaEE企業應用開發,包括SSH、SSM等

Spring價值

  • Spring是非侵入式的框架,目標是使應用程序代碼對框架依賴最小化;
  • Spring提供一個一致的編程模型,使應用直接使用POJO開發,與運行環境隔離開來;
  • Spring推動應用設計風格向面向對象和面向接口開發轉變,提高了代碼的重用性和可測試性;

Spring由哪些模塊組成?

Spring 總共大約有 20 個模塊, 由 1300 多個不同的文件構成。 而這些組件被分別整合在核心容器(Core Container)AOP(Aspect Oriented Programming)和設備支持(Instrmentation)數據訪問與集成(Data Access/Integeration)Web消息(Messaging)Test等 6 個模塊中。 以下是 Spring 5 的模塊結構圖:

在這里插入圖片描述

  • spring core:提供了框架的基本組成部分,包括控制反轉(Inversion of Control,IOC)和依賴注入(Dependency Injection,DI)功能。
  • spring beans:提供了BeanFactory,是工廠模式的一個經典實現,Spring將管理對象稱為Bean。
  • spring context:構建於 core 封裝包基礎上的 context 封裝包,提供了一種框架式的對象訪問方法。
  • spring jdbc:提供了一個JDBC的抽象層,消除了煩瑣的JDBC編碼和數據庫廠商特有的錯誤代碼解析, 用於簡化JDBC。
  • spring aop:提供了面向切面的編程實現,讓你可以自定義攔截器、切點等。
  • spring Web:提供了針對 Web 開發的集成特性,例如文件上傳,利用 servlet listeners 進行 ioc 容器初始化和針對 Web 的 ApplicationContext。
  • spring test:主要為測試提供支持的,支持使用JUnit或TestNG對Spring組件進行單元測試和集成測試。

Spring 框架中都用到了哪些設計模式?

  1. 工廠模式:BeanFactory就是簡單工廠模式的體現,用來創建對象的實例;
  2. 單例模式:Bean默認為單例模式。
  3. 代理模式:Spring的AOP功能用到了JDK的動態代理和CGLIB字節碼生成技術;
  4. 模板方法:用來解決代碼重復的問題。比如. RestTemplate, JmsTemplate, JpaTemplate。
  5. 觀察者模式:定義對象鍵一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都會得到通知被制動更新,如Spring中listener的實現–ApplicationListener。

詳細講解一下核心容器(spring context應用上下文) 模塊

這是基本的Spring模塊,提供spring 框架的基礎功能,BeanFactory 是 任何以spring為基礎的應用的核心。Spring 框架建立在此模塊之上,它使Spring成為一個容器。

Bean 工廠是工廠模式的一個實現,提供了控制反轉功能,用來把應用的配置和依賴從真正的應用代碼中分離。最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根據XML文件中的定義加載beans。該容器從XML 文件讀取配置元數據並用它去創建一個完全配置的系統或應用。

Spring框架中有哪些不同類型的事件

Spring 提供了以下5種標准的事件:

  1. 上下文更新事件(ContextRefreshedEvent):在調用ConfigurableApplicationContext 接口中的refresh()方法時被觸發。
  2. 上下文開始事件(ContextStartedEvent):當容器調用ConfigurableApplicationContext的Start()方法開始/重新開始容器時觸發該事件。
  3. 上下文停止事件(ContextStoppedEvent):當容器調用ConfigurableApplicationContext的Stop()方法停止容器時觸發該事件。
  4. 上下文關閉事件(ContextClosedEvent):當ApplicationContext被關閉時觸發該事件。容器被關閉時,其管理的所有單例Bean都被銷毀。
  5. 請求處理事件(RequestHandledEvent):在Web應用中,當一個http請求(request)結束觸發該事件。如果一個bean實現了ApplicationListener接口,當一個ApplicationEvent 被發布以后,bean會自動被通知。

Spring 應用程序有哪些不同組件?

Spring 應用一般有以下組件:

  • 接口 - 定義功能。
  • Bean 類 - 它包含屬性,setter 和 getter 方法,函數等。
  • Bean 配置文件 - 包含類的信息以及如何配置它們。
  • Spring 面向切面編程(AOP) - 提供面向切面編程的功能。
  • 用戶程序 - 它使用接口。

使用 Spring 有哪些方式?

使用 Spring 有以下方式:

  • 作為一個成熟的 Spring Web 應用程序。
  • 作為第三方 Web 框架,使用 Spring Frameworks 中間層。
  • 作為企業級 Java Bean,它可以包裝現有的 POJO(Plain Old Java Objects)。
  • 用於遠程使用。

Spring控制反轉(IOC)(13)

什么是Spring IOC 容器?

控制反轉即IoC (Inversion of Control),它把傳統上由程序代碼直接操控的對象的調用權交給容器,通過容器來實現對象組件的裝配和管理。所謂的“控制反轉”概念就是對組件對象控制權的轉移,從程序代碼本身轉移到了外部容器。

Spring IOC 負責創建對象,管理對象(通過依賴注入(DI),裝配對象,配置對象,並且管理這些對象的整個生命周期。

控制反轉(IoC)有什么作用

  • 管理對象的創建和依賴關系的維護。對象的創建並不是一件簡單的事,在對象關系比較復雜時,如果依賴關系需要程序猿來維護的話,那是相當頭疼的
  • 解耦,由容器去維護具體的對象
  • 托管了類的產生過程,比如我們需要在類的產生過程中做一些處理,最直接的例子就是代理,如果有容器程序可以把這部分處理交給容器,應用程序則無需去關心類是如何完成代理的

IOC的優點是什么?

  • IOC 或 依賴注入把應用的代碼量降到最低。
  • 它使應用容易測試,單元測試不再需要單例和JNDI查找機制。
  • 最小的代價和最小的侵入性使松散耦合得以實現。
  • IOC容器支持加載服務時的餓漢式初始化和懶加載。

Spring IoC 的實現機制

Spring 中的 IoC 的實現原理就是工廠模式加反射機制。

示例:

interface Fruit {
   public abstract void eat();
 }

class Apple implements Fruit {
    public void eat(){
        System.out.println("Apple");
    }
}

class Orange implements Fruit {
    public void eat(){
        System.out.println("Orange");
    }
}

class Factory {
    public static Fruit getInstance(String ClassName) {
        Fruit f=null;
        try {
            f=(Fruit)Class.forName(ClassName).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}

class Client {
    public static void main(String[] a) {
        Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple");
        if(f!=null){
            f.eat();
        }
    }
}

Spring 的 IoC支持哪些功能

Spring 的 IoC 設計支持以下功能:

  • 依賴注入
  • 依賴檢查
  • 自動裝配
  • 支持集合
  • 指定初始化方法和銷毀方法
  • 支持回調某些方法(但是需要實現 Spring 接口,略有侵入)

其中,最重要的就是依賴注入,從 XML 的配置上說,即 ref 標簽。對應 Spring RuntimeBeanReference 對象。

對於 IoC 來說,最重要的就是容器。容器管理着 Bean 的生命周期,控制着 Bean 的依賴注入。

BeanFactory 和 ApplicationContext有什么區別?

BeanFactory和ApplicationContext是Spring的兩大核心接口,都可以當做Spring的容器。

其中ApplicationContext是BeanFactory的子接口。

依賴關系

BeanFactory:是Spring里面最底層的接口,包含了各種Bean的定義,讀取bean配置文檔,管理bean的加載、實例化,控制bean的生命周期,維護bean之間的依賴關系。

ApplicationContext接口作為BeanFactory的派生,除了提供BeanFactory所具有的功能外,還提供了更完整的框架功能:

  • 繼承MessageSource,因此支持國際化。
  • 統一的資源文件訪問方式。
  • 提供在監聽器中注冊bean的事件。
  • 同時加載多個配置文件。
  • 載入多個(有繼承關系)上下文 ,使得每一個上下文都專注於一個特定的層次,比如應用的web層。

加載方式

BeanFactroy采用的是延遲加載形式來注入Bean的,即只有在使用到某個Bean時(調用getBean()),才對該Bean進行加載實例化。這樣,我們就不能發現一些存在的Spring的配置問題。如果Bean的某一個屬性沒有注入,BeanFacotry加載后,直至第一次使用調用getBean方法才會拋出異常。

ApplicationContext,它是在容器啟動時,一次性創建了所有的Bean。這樣,在容器啟動時,我們就可以發現Spring中存在的配置錯誤,這樣有利於檢查所依賴屬性是否注入。 ApplicationContext啟動后預載入所有的單實例Bean,通過預載入單實例bean ,確保當你需要的時候,你就不用等待,因為它們已經創建好了。

相對於基本的BeanFactory,ApplicationContext 唯一的不足是占用內存空間。當應用程序配置Bean較多時,程序啟動較慢。

創建方式

BeanFactory通常以編程的方式被創建,ApplicationContext還能以聲明的方式創建,如使用ContextLoader。

注冊方式

BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但兩者之間的區別是:BeanFactory需要手動注冊,而ApplicationContext則是自動注冊。

Spring 如何設計容器的,BeanFactory和ApplicationContext的關系詳解

Spring 作者 Rod Johnson 設計了兩個接口用以表示容器。

  • BeanFactory
  • ApplicationContext

BeanFactory 簡單粗暴,可以理解為就是個 HashMap,Key 是 BeanName,Value 是 Bean 實例。通常只提供注冊(put),獲取(get)這兩個功能。我們可以稱之為 “低級容器”

ApplicationContext 可以稱之為 “高級容器”。因為他比 BeanFactory 多了更多的功能。他繼承了多個接口。因此具備了更多的功能。例如資源的獲取,支持多種消息(例如 JSP tag 的支持),對 BeanFactory 多了工具級別的支持等待。所以你看他的名字,已經不是 BeanFactory 之類的工廠了,而是 “應用上下文”, 代表着整個大容器的所有功能。該接口定義了一個 refresh 方法,此方法是所有閱讀 Spring 源碼的人的最熟悉的方法,用於刷新整個容器,即重新加載/刷新所有的 bean。

當然,除了這兩個大接口,還有其他的輔助接口,這里就不介紹他們了。

BeanFactory和ApplicationContext的關系

為了更直觀的展示 “低級容器” 和 “高級容器” 的關系,這里通過常用的 ClassPathXmlApplicationContext 類來展示整個容器的層級 UML 關系。

img

有點復雜? 先不要慌,我來解釋一下。

最上面的是 BeanFactory,下面的 3 個綠色的,都是功能擴展接口,這里就不展開講。

看下面的隸屬 ApplicationContext 粉紅色的 “高級容器”,依賴着 “低級容器”,這里說的是依賴,不是繼承哦。他依賴着 “低級容器” 的 getBean 功能。而高級容器有更多的功能:支持不同的信息源頭,可以訪問文件資源,支持應用事件(Observer 模式)。

通常用戶看到的就是 “高級容器”。 但 BeanFactory 也非常夠用啦!

左邊灰色區域的是 “低級容器”, 只負載加載 Bean,獲取 Bean。容器其他的高級功能是沒有的。例如上圖畫的 refresh 刷新 Bean 工廠所有配置,生命周期事件回調等。

小結

說了這么多,不知道你有沒有理解Spring IoC? 這里小結一下:IoC 在 Spring 里,只需要低級容器就可以實現,2 個步驟:

  1. 加載配置文件,解析成 BeanDefinition 放在 Map 里。
  2. 調用 getBean 的時候,從 BeanDefinition 所屬的 Map 里,拿出 Class 對象進行實例化,同時,如果有依賴關系,將遞歸調用 getBean 方法 —— 完成依賴注入。

上面就是 Spring 低級容器(BeanFactory)的 IoC。

至於高級容器 ApplicationContext,他包含了低級容器的功能,當他執行 refresh 模板方法的時候,將刷新整個容器的 Bean。同時其作為高級容器,包含了太多的功能。一句話,他不僅僅是 IoC。他支持不同信息源頭,支持 BeanFactory 工具類,支持層級容器,支持訪問文件資源,支持事件發布通知,支持接口回調等等。

ApplicationContext通常的實現是什么?

FileSystemXmlApplicationContext :此容器從一個XML文件中加載beans的定義,XML Bean 配置文件的全路徑名必須提供給它的構造函數。

ClassPathXmlApplicationContext:此容器也從一個XML文件中加載beans的定義,這里,你需要正確設置classpath因為這個容器將在classpath里找bean配置。

WebXmlApplicationContext:此容器加載一個XML文件,此文件定義了一個WEB應用的所有bean。

什么是Spring的依賴注入?

控制反轉IoC是一個很大的概念,可以用不同的方式來實現。其主要實現方式有兩種:依賴注入和依賴查找

依賴注入:相對於IoC而言,依賴注入(DI)更加准確地描述了IoC的設計理念。所謂依賴注入(Dependency Injection),即組件之間的依賴關系由容器在應用系統運行期來決定,也就是由容器動態地將某種依賴關系的目標對象實例注入到應用系統中的各個關聯的組件之中。組件不做定位查詢,只提供普通的Java方法讓容器去決定依賴關系。

依賴注入的基本原則

依賴注入的基本原則是:應用組件不應該負責查找資源或者其他依賴的協作對象。配置對象的工作應該由IoC容器負責,“查找資源”的邏輯應該從應用組件的代碼中抽取出來,交給IoC容器負責。容器全權負責組件的裝配,它會把符合依賴關系的對象通過屬性(JavaBean中的setter)或者是構造器傳遞給需要的對象。

依賴注入有什么優勢

依賴注入之所以更流行是因為它是一種更可取的方式:讓容器全權負責依賴查詢,受管組件只需要暴露JavaBean的setter方法或者帶參數的構造器或者接口,使容器可以在初始化時組裝對象的依賴關系。其與依賴查找方式相比,主要優勢為:

  • 查找定位操作與應用代碼完全無關。
  • 不依賴於容器的API,可以很容易地在任何容器以外使用應用對象。
  • 不需要特殊的接口,絕大多數對象可以做到完全不必依賴容器。

有哪些不同類型的依賴注入實現方式?

依賴注入是時下最流行的IoC實現方式,依賴注入分為接口注入(Interface Injection),Setter方法注入(Setter Injection)和構造器注入(Constructor Injection)三種方式。其中接口注入由於在靈活性和易用性比較差,現在從Spring4開始已被廢棄。

構造器依賴注入:構造器依賴注入通過容器觸發一個類的構造器來實現的,該類有一系列參數,每個參數代表一個對其他類的依賴。

Setter方法注入:Setter方法注入是容器通過調用無參構造器或無參static工廠 方法實例化bean之后,調用該bean的setter方法,即實現了基於setter的依賴注入。

構造器依賴注入和 Setter方法注入的區別

構造函數注入 setter 注入
沒有部分注入 有部分注入
不會覆蓋 setter 屬性 會覆蓋 setter 屬性
任意修改都會創建一個新實例 任意修改不會創建一個新實例
適用於設置很多屬性 適用於設置少量屬性

兩種依賴方式都可以使用,構造器注入和Setter方法注入。最好的解決方案是用構造器參數實現強制依賴,setter方法實現可選依賴。

Spring Beans(19)

什么是Spring beans?

Spring beans 是那些形成Spring應用的主干的java對象。它們被Spring IOC容器初始化,裝配,和管理。這些beans通過容器中配置的元數據創建。比如,以XML文件中 的形式定義。

一個 Spring Bean 定義 包含什么?

一個Spring Bean 的定義包含容器必知的所有配置元數據,包括如何創建一個bean,它的生命周期詳情及它的依賴。

如何給Spring 容器提供配置元數據?Spring有幾種配置方式

這里有三種重要的方法給Spring 容器提供配置元數據。

  • XML配置文件。
  • 基於注解的配置。
  • 基於java的配置。

Spring配置文件包含了哪些信息

Spring配置文件是個XML 文件,這個文件包含了類信息,描述了如何配置它們,以及如何相互調用。

Spring基於xml注入bean的幾種方式

  1. Set方法注入;
  2. 構造器注入:①通過index設置參數的位置;②通過type設置參數類型;
  3. 靜態工廠注入;
  4. 實例工廠;

你怎樣定義類的作用域?

當定義一個 在Spring里,我們還能給這個bean聲明一個作用域。它可以通過bean 定義中的scope屬性來定義。如,當Spring要在需要的時候每次生產一個新的bean實例,bean的scope屬性被指定為prototype。另一方面,一個bean每次使用的時候必須返回同一個實例,這個bean的scope 屬性 必須設為 singleton。

解釋Spring支持的幾種bean的作用域

Spring框架支持以下五種bean的作用域:

  • singleton : bean在每個Spring ioc 容器中只有一個實例。
  • prototype:一個bean的定義可以有多個實例。
  • request:每次http請求都會創建一個bean,該作用域僅在基於web的Spring ApplicationContext情形下有效。
  • session:在一個HTTP Session中,一個bean定義對應一個實例。該作用域僅在基於web的Spring ApplicationContext情形下有效。
  • global-session:在一個全局的HTTP Session中,一個bean定義對應一個實例。該作用域僅在基於web的Spring ApplicationContext情形下有效。

注意: 缺省的Spring bean 的作用域是Singleton。使用 prototype 作用域需要慎重的思考,因為頻繁創建和銷毀 bean 會帶來很大的性能開銷。

Spring框架中的單例bean是線程安全的嗎?

不是,Spring框架中的單例bean不是線程安全的。

spring 中的 bean 默認是單例模式,spring 框架並沒有對單例 bean 進行多線程的封裝處理。

實際上大部分時候 spring bean 無狀態的(比如 dao 類),所有某種程度上來說 bean 也是安全的,但如果 bean 有狀態的話(比如 view model 對象),那就要開發者自己去保證線程安全了,最簡單的就是改變 bean 的作用域,把“singleton”變更為“prototype”,這樣請求 bean 相當於 new Bean()了,所以就可以保證線程安全了。

  • 有狀態就是有數據存儲功能。
  • 無狀態就是不會保存數據。

Spring如何處理線程並發問題?

在一般情況下,只有無狀態的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域,因為Spring對一些Bean中非線程安全狀態采用ThreadLocal進行處理,解決線程安全問題。

ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。同步機制采用了“時間換空間”的方式,僅提供一份變量,不同的線程在訪問前需要獲取鎖,沒獲得鎖的線程則需要排隊。而ThreadLocal采用了“空間換時間”的方式。

ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

問題:解釋Spring框架中bean的生命周期 (重要)

Spring Bean的生命周期是Spring面試熱點問題。這個問題即考察對Spring的微觀了解,又考察對Spring的宏觀認識,想要答好並不容易!本文希望能夠從源碼角度入手,幫助面試者徹底搞定Spring Bean的生命周期。

參考答案:

首先,回答階段的數量:只有四個!

是的,Spring Bean的生命周期只有這四個階段。把這四個階段和每個階段對應的擴展點糅合在一起雖然沒有問題,但是這樣非常凌亂,難以記憶。

要徹底搞清楚Spring的生命周期,首先要把這四個階段牢牢記住。實例化和屬性賦值對應構造方法和setter方法的注入,初始化和銷毀是用戶能自定義擴展的兩個階段。在這四步之間穿插的各種擴展點,稍后會講。

  1. 實例化 Instantiation
  2. 屬性賦值 Populate
  3. 初始化 Initialization
  4. 銷毀 Destruction

實例化 -> 屬性賦值 -> 初始化 -> 銷毀

在這里插入圖片描述

各個階段的工作:
  1. 實例化,創建一個Bean對象

  2. 填充屬性,為屬性賦值

  3. 初始化

    • 如果實現了xxxAware接口,通過不同類型的Aware接口拿到Spring容器的資源
    • 如果實現了BeanPostProcessor接口,則會回調該接口的postProcessBeforeInitialzationpostProcessAfterInitialization方法
    • 如果配置了init-method方法,則會執行init-method配置的方法
  4. 銷毀

    • 容器關閉后,如果Bean實現了DisposableBean接口,則會回調該接口的destroy方法
    • 如果配置了destroy-method方法,則會執行destroy-method配置的方法
源碼學習:

前三個階段,主要邏輯都在doCreate()方法中,邏輯很清晰,就是順序調用以下三個方法,這三個方法與三個生命周期階段一一對應,非常重要,在后續擴展接口分析中也會涉及。

  1. createBeanInstance() -> 實例化
  2. populateBean() -> 屬性賦值
  3. initializeBean() -> 初始化

注:bean的生命周期是從將bean定義全部注冊到BeanFacotry中以后開始的。

源碼如下,能證明實例化,屬性賦值和初始化這三個生命周期的存在。關於本文的Spring源碼都將忽略無關部分,便於理解:

前三個階段的源碼:

// 忽略了無關代碼
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {
   // Instantiate the bean.
   BeanWrapper instanceWrapper = null;
   if (instanceWrapper == null) {
       // 實例化階段!
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   }
   // Initialize the bean instance.
   Object exposedObject = bean;
   try {
       // 屬性賦值階段!
      populateBean(beanName, mbd, instanceWrapper);
       // 初始化階段!
      exposedObject = initializeBean(beanName, exposedObject, mbd);
   }
}

上面這些這個實例化Bean的方法是在getBean()方法中調用的,而getBean是在finishBeanFactoryInitialization方法中調用的,用來實例化單例非懶加載Bean,源碼如下:

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);
            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);
            // Register bean processors that intercept bean creation.

            // 所有BeanPostProcesser初始化的調用點
            registerBeanPostProcessors(beanFactory);
            // Initialize message source for this context.
            initMessageSource();
            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();
            // Initialize other special beans in specific context subclasses.
            onRefresh();
            // Check for listener beans and register them.
            registerListeners();
            // Instantiate all remaining (non-lazy-init) singletons.

            // 所有單例非懶加載Bean的調用點
            finishBeanFactoryInitialization(beanFactory);
            // Last step: publish corresponding event.
            finishRefresh();
        }
}

銷毀Bean階段:

至於銷毀,是在容器關閉時調用的,詳見 ConfigurableApplicationContext#close()

高分答題的技巧:

如果回答了上面的答案可以拿到100分的話,加上下面的內容,就是120分

生命周期常用擴展點

Spring生命周期相關的常用擴展點非常多,所以問題不是不知道,而是記不住或者記不牢。其實記不住的根本原因還是不夠了解,這里通過源碼+分類的方式幫大家記憶。

區分影響一個bean或者多個bean是從源碼分析得出的.

以BeanPostProcessor為例:

  1. 從refresh方法來看,BeanPostProcessor 實例化比正常的bean早.
  2. 從initializeBean方法看,每個bean初始化前后都調用所有BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法.

第一大類:影響多個Bean的接口

實現了這些接口的Bean會切入到多個Bean的生命周期中。正因為如此,這些接口的功能非常強大,Spring內部擴展也經常使用這些接口,例如自動注入以及AOP的實現都和他們有關。

  • InstantiationAwareBeanPostProcessor
  • BeanPostProcessor

這兩兄弟可能是Spring擴展中最重要的兩個接口!InstantiationAwareBeanPostProcessor作用於實例化階段的前后,BeanPostProcessor作用於初始化階段的前后。正好和第一、第三個生命周期階段對應。通過圖能更好理解:

img

InstantiationAwareBeanPostProcessor

InstantiationAwareBeanPostProcessor實際上繼承了BeanPostProcessor接口,嚴格意義上來看他們不是兩兄弟,而是兩父子。但是從生命周期角度我們重點關注其特有的對實例化階段的影響,圖中省略了從BeanPostProcessor繼承的方法。

InstantiationAwareBeanPostProcessor extends BeanPostProcessor

InstantiationAwareBeanPostProcessor源碼分析:

  • postProcessBeforeInstantiation調用點,忽略無關代碼:
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
        throws BeanCreationException {
    try {
        // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
        // postProcessBeforeInstantiation方法調用點,這里就不跟進了,
        // 有興趣的同學可以自己看下,就是for循環調用所有的InstantiationAwareBeanPostProcessor
        Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
        if (bean != null) {
            return bean;
        }
    }
    
    try {   
        // 上文提到的doCreateBean方法,可以看到
        // postProcessBeforeInstantiation方法在創建Bean之前調用
        Object beanInstance = doCreateBean(beanName, mbdToUse, args);
        if (logger.isTraceEnabled()) {
            logger.trace("Finished creating instance of bean '" + beanName + "'");
        }
        return beanInstance;
    }
    
}

可以看到,postProcessBeforeInstantiation在doCreateBean之前調用,也就是在bean實例化之前調用的,英文源碼注釋解釋道該方法的返回值會替換原本的Bean作為代理,這也是Aop等功能實現的關鍵點。

  • postProcessAfterInstantiation調用點,忽略無關代碼:
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
    // state of the bean before properties are set. This can be used, for example,
    // to support styles of field injection.
    boolean continueWithPropertyPopulation = true;

     // InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation()
     // 方法作為屬性賦值的前置檢查條件,在屬性賦值之前執行,能夠影響是否進行屬性賦值!
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
       for (BeanPostProcessor bp : getBeanPostProcessors()) {
          if (bp instanceof InstantiationAwareBeanPostProcessor) {
             InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
             if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                continueWithPropertyPopulation = false;
                break;
             }
          }
       }
    }
 
    // 忽略后續的屬性賦值操作代碼
}

可以看到該方法在屬性賦值方法內,但是在真正執行賦值操作之前。其返回值為boolean,返回false時可以阻斷屬性賦值階段(continueWithPropertyPopulation = false;)。

BeanPostProcessor

關於BeanPostProcessor執行階段的源碼穿插在下文Aware接口的調用時機分析中,因為部分Aware功能的就是通過他實現的!只需要先記住BeanPostProcessor在初始化前后調用就可以了。

接口源碼:

public interface BeanPostProcessor {
     //bean初始化之前調用
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
	
    //bean初始化之后調用
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
}
 

第二大類:只調用一次的接口

這一大類接口的特點是功能豐富,常用於用戶自定義擴展。

第二大類中又可以分為兩類:

  1. Aware類型的接口
  2. 生命周期接口

無所不知的Aware

Aware類型的接口的作用就是讓我們能夠拿到Spring容器中的一些資源。基本都能夠見名知意,Aware之前的名字就是可以拿到什么資源,例如BeanNameAware可以拿到BeanName,以此類推。調用時機需要注意:所有的Aware方法都是在初始化階段之前調用的!

Aware接口眾多,這里同樣通過分類的方式幫助大家記憶。

Aware接口具體可以分為兩組,至於為什么這么分,詳見下面的源碼分析。如下排列順序同樣也是Aware接口的執行順序,能夠見名知意的接口不再解釋。

Aware Group1

  1. BeanNameAware
  2. BeanClassLoaderAware
  3. BeanFactoryAware

Aware Group2

  1. EnvironmentAware
  2. EmbeddedValueResolverAware 這個知道的人可能不多,實現該接口能夠獲取Spring EL解析器,用戶的自定義注解需要支持spel表達式的時候可以使用,非常方便。
  3. ApplicationContextAware(ResourceLoaderAware\ApplicationEventPublisherAware\MessageSourceAware) 這幾個接口可能讓人有點懵,實際上這幾個接口可以一起記,其返回值實質上都是當前的ApplicationContext對象,因為ApplicationContext是一個復合接口,如下:
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver {}

這里涉及到另一道面試題,ApplicationContext和BeanFactory的區別,可以從ApplicationContext繼承的這幾個接口入手,除去BeanFactory相關的兩個接口就是ApplicationContext獨有的功能,這里不詳細說明。

Aware調用時機源碼分析

詳情如下,忽略了部分無關代碼。代碼位置就是我們上文提到的initializeBean方法詳情,這也說明了Aware都是在初始化階段之前調用的!

// 見名知意,初始化階段調用的方法
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
    // 這里調用的是Group1中的三個Bean開頭的Aware
    invokeAwareMethods(beanName, bean);

    Object wrappedBean = bean;
    
    // 這里調用的是Group2中的幾個Aware,
    // 而實質上這里就是前面所說的BeanPostProcessor的調用點!
    // 也就是說與Group1中的Aware不同,這里是通過BeanPostProcessor(ApplicationContextAwareProcessor)實現的。
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);

    // 這個是初始化方法,下文要介紹的InitializingBean調用點就是在這個方法里面
    invokeInitMethods(beanName, wrappedBean, mbd);

    // BeanPostProcessor的另一個調用點
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);

    return wrappedBean;
}

可以看到並不是所有的Aware接口都使用同樣的方式調用。Bean××Aware都是在代碼中直接調用的,而ApplicationContext相關的Aware都是通過BeanPostProcessor#postProcessBeforeInitialization()實現的。感興趣的可以自己看一下ApplicationContextAwareProcessor這個類的源碼,就是判斷當前創建的Bean是否實現了相關的Aware方法,如果實現了會調用回調方法將資源傳遞給Bean。

至於Spring為什么這么實現,應該沒什么特殊的考量。也許和Spring的版本升級有關。基於對修改關閉,對擴展開放的原則,Spring對一些新的Aware采用了擴展的方式添加。

BeanPostProcessor的調用時機也能在這里體現,包圍住invokeInitMethods方法,也就說明了在初始化階段的前后執行。

關於Aware接口的執行順序,其實只需要記住第一組在第二組執行之前就行了。每組中各個Aware方法的調用順序其實沒有必要記,有需要的時候點進源碼一看便知。

簡單的兩個生命周期接口

至於剩下的兩個生命周期接口就很簡單了,實例化和屬性賦值都是Spring幫助我們做的,能夠自己實現的有初始化和銷毀兩個生命周期階段。

InitializingBean接口

InitializingBean顧名思義,是初始化Bean相關的接口。

接口定義:

public interface InitializingBean {

    void afterPropertiesSet() throws Exception;

}

看方法名,是在讀完Properties文件,之后執行的方法。afterPropertiesSet()方法是在初始化過程中被調用的。

InitializingBean 對應生命周期的初始化階段,在上面源碼的invokeInitMethods(beanName, wrappedBean, mbd);方法中調用。

有一點需要注意,因為Aware方法都是執行在初始化方法之前,所以可以在初始化方法中放心大膽的使用Aware接口獲取的資源,這也是我們自定義擴展Spring的常用方式。

除了實現InitializingBean接口之外還能通過注解(@PostConstruct)或者xml配置的方式指定初始化方法(init-method),至於這幾種定義方式的調用順序其實沒有必要記。因為這幾個方法對應的都是同一個生命周期,只是實現方式不同,我們一般只采用其中一種方式。

三種實現指定初始化方法的方法:
  • 使用@PostConstruct注解,該注解作用於void方法上
  • 在配置文件中配置init-method方法
<bean id="student" class="com.demo.Student" init-method="init2">
        <property name="name" value="小明"></property>
        <property name="age" value="20"></property>
        <property name="school" ref="school"></property>
</bean>
  • 將類實現InitializingBean接口
@Component("student")
public class Student implements InitializingBean{
    private String name;
    private int age;
	        …
}

執行:

@Component("student")
public class Student implements InitializingBean{
    private String name;
    private int age;
    
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
       
    //1.使用postconstrtct注解
    @PostConstruct
    public void init(){
        System.out.println("執行 init方法");
    }
     
    //2.在xml配置文件中配置init-method方法
    public void init2(){
        System.out.println("執行init2方法 ");
    }
    
    //3.實現InitializingBean接口
    public void afterPropertiesSet() throws Exception {
        System.out.println("執行init3方法");
    }
    
}

通過測試我們可以得出結論,三種實現方式的執行順序是:

Constructor > @PostConstruct > InitializingBean > init-method

DisposableBean接口

DisposableBean 類似於InitializingBean,對應生命周期的銷毀階段,以ConfigurableApplicationContext#close()方法作為入口,實現是通過循環獲取所有實現了DisposableBean接口的Bean然后調用其destroy()方法 。

接口定義:

public interface DisposableBean {
    void destroy() throws Exception;
}

定義一個實現了DisposableBean接口的Bean:

public class IndexBean implements InitializingBean,DisposableBean {
    public void destroy() throws Exception {
        System.out.println("destroy");
    }
    public void afterPropertiesSet() throws Exception {
        System.out.println("init-afterPropertiesSet()");
    }
    public void test(){
        System.out.println("init-test()");
    }
}

執行:

public class Main {
    public static void main(String[] args) {
        AbstractApplicationContext applicationContext=new ClassPathXmlApplicationContext("classpath:application-usertag.xml");
        System.out.println("init-success");
        applicationContext.registerShutdownHook();
    }
}

執行結果:

init-afterPropertiesSet()
init-test()
init-success
destroy

也就是說,在對象銷毀的時候,會去調用DisposableBean的destroy方法。在進入到銷毀過程時先去調用一下DisposableBean的destroy方法,然后后執行 destroy-method聲明的方法(用來銷毀Bean中的各項數據)。

擴展閱讀: BeanPostProcessor注冊時機與執行順序

首先要明確一個概念,在spring中一切皆bean

所有的組件都會被作為一個bean裝配到spring容器中,過程如下圖:

img

所以我們前面所講的那些拓展點,也都會被作為一個個bean裝配到spring容器中

注冊時機

我們知道BeanPostProcessor也會注冊為Bean,那么Spring是如何保證BeanPostProcessor在我們的業務Bean之前初始化完成呢?

請看我們熟悉的refresh()方法的源碼,省略部分無關代碼(refresh的詳細注解見refresh()):

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            // 注冊所有BeanPostProcesser的方法
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            // 所有單例非懶加載Bean的創建方法
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }
}

可以看出,Spring是先執行registerBeanPostProcessors()進行BeanPostProcessors的注冊,然后再執行finishBeanFactoryInitialization創建我們的單例非懶加載的Bean。

執行順序

BeanPostProcessor有很多個,而且每個BeanPostProcessor都影響多個Bean,其執行順序至關重要,必須能夠控制其執行順序才行。關於執行順序這里需要引入兩個排序相關的接口:PriorityOrdered、Ordered

  • PriorityOrdered是一等公民,首先被執行,PriorityOrdered公民之間通過接口返回值排序
  • Ordered是二等公民,然后執行,Ordered公民之間通過接口返回值排序
  • 都沒有實現是三等公民,最后執行

在以下源碼中,可以很清晰的看到Spring注冊各種類型BeanPostProcessor的邏輯,根據實現不同排序接口進行分組。優先級高的先加入,優先級低的后加入。

// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
// 首先,加入實現了PriorityOrdered接口的BeanPostProcessors,順便根據PriorityOrdered排了序
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
    if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
        currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
        processedBeans.add(ppName);
    }
}

sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
// 然后,加入實現了Ordered接口的BeanPostProcessors,順便根據Ordered排了序
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
    if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
        currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
        processedBeans.add(ppName);
    }
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.

// 最后加入其他常規的BeanPostProcessors
boolean reiterate = true;
while (reiterate) {
    reiterate = false;
    postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
    for (String ppName : postProcessorNames) {
        if (!processedBeans.contains(ppName)) {
            currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
            processedBeans.add(ppName);
            reiterate = true;
        }
    }
    sortPostProcessors(currentRegistryProcessors, beanFactory);
    registryProcessors.addAll(currentRegistryProcessors);
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    currentRegistryProcessors.clear();
}

根據排序接口返回值排序,默認升序排序,返回值越低優先級越高。

/**
 * Useful constant for the highest precedence value.
 * @see java.lang.Integer#MIN_VALUE
 */
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
/**
 * Useful constant for the lowest precedence value.
 * @see java.lang.Integer#MAX_VALUE
 */
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

PriorityOrdered、Ordered接口作為Spring整個框架通用的排序接口,在Spring中應用廣泛,也是非常重要的接口。

Bean的生命周期流程圖

img

總結

Spring Bean的生命周期分為四個階段多個擴展點。擴展點又可以分為影響多個Bean影響單個Bean。整理如下:

四個階段
  • 實例化 Instantiation
  • 屬性賦值 Populate
  • 初始化 Initialization
  • 銷毀 Destruction
多個擴展點
  • 影響多個Bean
    • BeanPostProcessor
    • InstantiationAwareBeanPostProcessor
  • 影響單個Bean
    • Aware
      • Aware Group1
        • BeanNameAware
        • BeanClassLoaderAware
        • BeanFactoryAware
      • Aware Group2
        • EnvironmentAware
        • EmbeddedValueResolverAware
        • ApplicationContextAware(ResourceLoaderAware\ApplicationEventPublisherAware\MessageSourceAware)
    • 生命周期
      • InitializingBean
      • DisposableBean

哪些是重要的bean生命周期方法? 你能重載它們嗎?

有兩個重要的bean 生命周期方法,第一個是setup , 它是在容器加載bean的時候被調用。第二個方法是 teardown 它是在容器卸載類的時候被調用。

bean 標簽有兩個重要的屬性(init-method和destroy-method)。用它們你可以自己定制初始化和注銷方法。它們也有相應的注解(@PostConstruct和@PreDestroy)。

什么是Spring的內部bean?什么是Spring inner beans?

在Spring框架中,當一個bean僅被用作另一個bean的屬性時,它能被聲明為一個內部bean。內部bean可以用setter注入“屬性”和構造方法注入“構造參數”的方式來實現,內部bean通常是匿名的,它們的Scope一般是prototype。

在 Spring中如何注入一個java集合?

Spring提供以下幾種集合的配置元素:

類型用於注入一列值,允許有相同的值。

類型用於注入一組值,不允許有相同的值。

類型用於注入一組鍵值對,鍵和值都可以為任意類型。

類型用於注入一組鍵值對,鍵和值都只能為String類型。

什么是bean裝配?

裝配,或bean 裝配是指在Spring 容器中把bean組裝到一起,前提是容器需要知道bean的依賴關系,如何通過依賴注入來把它們裝配到一起。

什么是bean的自動裝配?

在Spring框架中,在配置文件中設定bean的依賴關系是一個很好的機制,Spring 容器能夠自動裝配相互合作的bean,這意味着容器不需要和配置,能通過Bean工廠自動處理bean之間的協作。這意味着 Spring可以通過向Bean Factory中注入的方式自動搞定bean之間的依賴關系。自動裝配可以設置在每個bean上,也可以設定在特定的bean上。

解釋不同方式的自動裝配,spring 自動裝配 bean 有哪些方式?

在spring中,對象無需自己查找或創建與其關聯的其他對象,由容器負責把需要相互協作的對象引用賦予各個對象,使用autowire來配置自動裝載模式。

在Spring框架xml配置中共有5種自動裝配:

  • no:默認的方式是不進行自動裝配的,通過手工設置ref屬性來進行裝配bean。
  • byName:通過bean的名稱進行自動裝配,如果一個bean的 property 與另一bean 的name 相同,就進行自動裝配。
  • byType:通過參數的數據類型進行自動裝配。
  • constructor:利用構造函數進行裝配,並且構造函數的參數通過byType進行裝配。
  • autodetect:自動探測,如果有構造方法,通過 construct的方式自動裝配,否則使用 byType的方式自動裝配。

使用@Autowired注解自動裝配的過程是怎樣的?

使用@Autowired注解來自動裝配指定的bean。在使用@Autowired注解之前需要在Spring配置文件進行配置,<context:annotation-config />。

在啟動spring IoC時,容器自動裝載了一個AutowiredAnnotationBeanPostProcessor后置處理器,當容器掃描到@Autowied、@Resource或@Inject時,就會在IoC容器自動查找需要的bean,並裝配給該對象的屬性。在使用@Autowired時,首先在容器中查詢對應類型的bean:

  • 如果查詢結果剛好為一個,就將該bean裝配給@Autowired指定的數據;
  • 如果查詢的結果不止一個,那么@Autowired會根據名稱來查找;
  • 如果上述查找的結果為空,那么會拋出異常。解決方法時,使用required=false。

自動裝配有哪些局限性?

自動裝配的局限性是:

重寫:你仍需用 和 配置來定義依賴,意味着總要重寫自動裝配。

基本數據類型:你不能自動裝配簡單的屬性,如基本數據類型,String字符串,和類。

模糊特性:自動裝配不如顯式裝配精確,如果有可能,建議使用顯式裝配。

你可以在Spring中注入一個null 和一個空字符串嗎?

可以。

問題: FactoryBean 和 BeanFactory有什么區別?

簡要的答案:

BeanFactory 是 Bean 的工廠, ApplicationContext 的父類,IOC 容器的核心,負責生產和管理 Bean 對象。

FactoryBean 是 Bean,可以通過實現 FactoryBean 接口定制實例化 Bean 的邏輯,通過代理一個Bean對象,對方法前后做一些操作。

具體的介紹:

(1) BeanFactory 是ioc容器的底層實現接口,是ApplicationContext 頂級接口

spring不允許我們直接操作 BeanFactory bean工廠,所以為我們提供了ApplicationContext 這個接口 此接口集成BeanFactory 接口,ApplicationContext包含BeanFactory的所有功能,同時還進行更多的擴展。

BeanFactory 接口又衍生出以下接口,其中我們經常用到的是ApplicationContext 接口

ApplicationContext 繼承圖

在這里插入圖片描述

ConfiguableApplicationContext 中添加了一些方法:

... 其他省略
    
    //刷新ioc容器上下文
    void refresh() throws BeansException, IllegalStateException;

// 關閉此應用程序上下文,釋放所有資源並鎖定,銷毀所有緩存的單例bean。
    @Override
    void close();

//確定此應用程序上下文是否處於活動狀態,即,是否至少刷新一次且尚未關閉。
    boolean isActive();

    ... 其他省略

主要作用在ioc容器進行相應的刷新,關閉等操作!

FileSystemXmlApplicationContext 和ClassPathXmlApplicationContext 是用來讀取xml文件創建bean對象
ClassPathXmlApplicationContext  : 讀取類路徑下xml 創建bean
FileSystemXmlApplicationContext :讀取文件系統下xml創建bean
AnnotationConfigApplicationContext 主要是注解開發獲取ioc中的bean實例

(2) FactoryBean 是spirng提供的工廠bean的一個接口

FactoryBean 接口提供三個方法,用來創建對象,
FactoryBean 具體返回的對象是由getObject 方法決定的。

*/
public interface FactoryBean<T> {

//創建的具體bean對象的類型
    @Nullable
    T getObject() throws Exception;

 //工廠bean 具體創建具體對象是由此getObject()方法來返回的
    @Nullable
    Class<?> getObjectType();
    
  //是否單例
    default boolean isSingleton() {
        return true;
    }

}

創建一個FactoryBean 用來生產User對象

@Component
public class FactoryBeanTest implements FactoryBean<User> {


    //創建的具體bean對象的類型
    @Override
    public Class<?> getObjectType() {
        return User.class;
    }


    //是否單例
    @Override
    public boolean isSingleton() {
        return true;
    }

    //工廠bean 具體創建具體對象是由此getObject()方法來返回的
    @Override
    public User getObject() throws Exception {
        return new User();
    }
}

Junit測試

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {FactoryBeanTest.class})
@WebAppConfiguration
public class SpringBootDemoApplicationTests {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void tesst() {
        FactoryBeanTest bean1 = applicationContext.getBean(FactoryBeanTest.class);
        try {
            User object = bean1.getObject();
            System.out.println(object==object);
            System.out.println(object);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

結果

true
User [id=null, name=null, age=0]

簡單的總結:

BeanFactory是個bean 工廠,是一個工廠類(接口), 它負責生產和管理bean的一個工廠
是ioc 容器最底層的接口,是個ioc容器,是spring用來管理和裝配普通bean的ioc容器(這些bean成為普通bean)。

FactoryBean是個bean,在IOC容器的基礎上給Bean的實現加上了一個簡單工廠模式和裝飾模式,是一個可以生產對象和裝飾對象的工廠bean,由spring管理后,生產的對象是由getObject()方法決定的(從容器中獲取到的對象不是
“ FactoryBeanTest  ” 對象)。

高頻面試題:Spring 如何解決循環依賴?

在關於Spring的面試中,我們經常會被問到一個問題:Spring是如何解決循環依賴的問題的。

這個問題算是關於Spring的一個高頻面試題,因為如果不刻意研讀,相信即使讀過源碼,面試者也不一定能夠一下子思考出個中奧秘。

本文主要針對這個問題,從源碼的角度對其實現原理進行講解。

循環依賴的簡單例子

比如幾個Bean之間的互相引用:

img

甚至自己“循環”依賴自己:

img

原型(Prototype)的場景是不支持循環依賴的

先說明前提:原型(Prototype)的場景是不支持循環依賴的. 單例的場景才能存在循環依賴

原型(Prototype)的場景通常會走到AbstractBeanFactory類中下面的判斷,拋出異常。

if (isPrototypeCurrentlyInCreation(beanName)) {
  throw new BeanCurrentlyInCreationException(beanName);
}

原因很好理解,創建新的A時,發現要注入原型字段B,又創建新的B發現要注入原型字段A...

這就套娃了, Spring就先拋出了BeanCurrentlyInCreationException

什么是原型(Prototype)的場景?

通過如下方式,可以將該類的bean設置為原型模式

@Service
@Scope("prototype")
public class MyReportExporter extends AbstractReportExporter{
    ...
}

在Spring中,@Service默認都是單例的。用了私有全局變量,若不想影響下次請求,就需要用到原型模式,即@Scope(“prototype”)

所謂單例,就是Spring的IOC機制只創建該類的一個實例,每次請求,都會用這同一個實例進行處理,因此若存在全局變量,本次請求的值肯定會影響下一次請求時該變量的值。
原型模式,指的是每次調用時,會重新創建該類的一個實例,比較類似於我們自己自己new的對象實例。

具體例子:循環依賴的代碼片段

我們先看看當時出問題的代碼片段:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

這兩段代碼中定義了兩個Service類:TestService1TestService2,在TestService1中注入了TestService2的實例,同時在TestService2中注入了TestService1的實例,這里構成了循環依賴

只不過,這不是普通的循環依賴,因為TestService1的test1方法上加了一個@Async注解。

大家猜猜程序啟動后運行結果會怎樣?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

報錯了。。。原因是出現了循環依賴。

「不科學呀,spring不是號稱能解決循環依賴問題嗎,怎么還會出現?」

如果把上面的代碼稍微調整一下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}

把TestService1的test1方法上的@Async注解去掉,TestService1TestService2都需要注入對方的實例,同樣構成了循環依賴。

但是重新啟動項目,發現它能夠正常運行。這又是為什么?

帶着這兩個問題,讓我們一起開始spring循環依賴的探秘之旅。

什么是循環依賴?

循環依賴:說白是一個或多個對象實例之間存在直接或間接的依賴關系,這種依賴關系構成了構成一個環形調用。

第一種情況:自己依賴自己的直接依賴

img

第二種情況:兩個對象之間的直接依賴

img

第三種情況:多個對象之間的間接依賴img

前面兩種情況的直接循環依賴比較直觀,非常好識別,但是第三種間接循環依賴的情況有時候因為業務代碼調用層級很深,不容易識別出來。

循環依賴的N種場景

spring中出現循環依賴主要有以下場景:

img

場景1:單例的setter注入

這種注入方式應該是spring用的最多的,代碼如下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

這是一個經典的循環依賴,但是它能正常運行,得益於spring的內部機制,讓我們根本無法感知它有問題,因為spring默默幫我們解決了。

spring內部有三級緩存:

  • singletonObjects 一級緩存,用於保存實例化、注入、初始化完成的bean實例
  • earlySingletonObjects 二級緩存,用於保存實例化完成的bean實例
  • singletonFactories 三級緩存,用於保存bean創建工廠,以便於后面擴展有機會創建代理對象。

下面用一張圖告訴你,spring是如何解決循環依賴的:

img

細心的朋友可能會發現在這種場景中第二級緩存作用不大。

那么問題來了,為什么要用第二級緩存呢?

試想一下,如果出現以下這種情況,我們要如何處理?

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}
@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;

    public void test3() {
    }
}

TestService1依賴於TestService2和TestService3,而TestService2依賴於TestService1,同時TestService3也依賴於TestService1。

按照上圖的流程可以把TestService1注入到TestService2,並且TestService1的實例是從第三級緩存中獲取的。

假設不用第二級緩存,TestService1注入到TestService3的流程如圖:

img

TestService1注入到TestService3又需要從第三級緩存中獲取實例,而第三級緩存里保存的並非真正的實例對象,而是ObjectFactory對象。

說白了,兩次從三級緩存中獲取都是ObjectFactory對象,而通過它創建的實例對象每次可能都不一樣的。

這樣不是有問題?

為了解決這個問題,spring引入的第二級緩存。前一個圖其實TestService1對象的實例已經被添加到第二級緩存中了,而在TestService1注入到TestService3時,只用從第二級緩存中獲取該對象即可。

img

還有個問題,第三級緩存中為什么要添加ObjectFactory對象,直接保存實例對象不行嗎?

答:不行,因為假如你想對添加到三級緩存中的實例對象進行增強,直接用實例對象是行不通的。

針對這種場景spring是怎么做的呢?

答案就在AbstractAutowireCapableBeanFactorydoCreateBean方法的這段代碼中:

img它定義了一個匿名內部類,通過getEarlyBeanReference方法獲取代理對象,其實底層是通過AbstractAutoProxyCreator類的getEarlyBeanReference生成代理對象。

場景二多例的setter注入

這種注入方法偶然會有,特別是在多線程的場景下,具體代碼如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

很多人說這種情況spring容器啟動會報錯,其實是不對的,我非常負責任的告訴你程序能夠正常啟動。

為什么呢?

其實在AbstractApplicationContext類的refresh方法中告訴了我們答案,它會調用finishBeanFactoryInitialization方法,該方法的作用是為了spring容器啟動的時候提前初始化一些bean。該方法的內部又調用了preInstantiateSingletons方法

img標紅的地方明顯能夠看出:非抽象、單例 並且非懶加載的類才能被提前初始bean。

而多例即SCOPE_PROTOTYPE類型的類,非單例,不會被提前初始化bean,所以程序能夠正常啟動。

如何讓他提前初始化bean呢?

只需要再定義一個單例的類,在它里面注入TestService1

@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;
}

重新啟動程序,執行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出現了循環依賴。

注意:這種循環依賴問題是無法解決的,因為它沒有用緩存,每次都會生成一個新對象。

場景三:構造器注入

這種注入方式現在其實用的已經非常少了,但是我們還是有必要了解一下,看看如下代碼:

@Service
publicclass TestService1 {

    public TestService1(TestService2 testService2) {
    }
}
@Service
publicclass TestService2 {

    public TestService2(TestService1 testService1) {
    }
}

運行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出現了循環依賴,為什么呢?

img

從圖中的流程看出構造器注入沒能添加到三級緩存,也沒有使用緩存,所以也無法解決循環依賴問題。

場景四:單例的代理對象setter注入

這種注入方式其實也比較常用,比如平時使用:@Async注解的場景,會通過AOP自動生成代理對象。

我那位同事的問題也是這種情況。

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

從前面得知程序啟動會報錯,出現了循環依賴:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

為什么會循環依賴呢?

答案就在下面這張圖中:

img

說白了,bean初始化完成之后,后面還有一步去檢查:第二級緩存 和 原始對象 是否相等。由於它對前面流程來說無關緊要,所以前面的流程圖中省略了,但是在這里是關鍵點,我們重點說說:

img

那位同事的問題正好是走到這段代碼,發現第二級緩存 和 原始對象不相等,所以拋出了循環依賴的異常。

如果這時候把TestService1改個名字,改成:TestService6,其他的都不變。

@Service
publicclass TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

再重新啟動一下程序,神奇般的好了。

what? 這又是為什么?

這就要從spring的bean加載順序說起了,默認情況下,spring是按照文件完整路徑遞歸查找的,按路徑+文件名排序,排在前面的先加載。所以TestService1比TestService2先加載,而改了文件名稱之后,TestService2比TestService6先加載。

為什么TestService2比TestService6先加載就沒問題呢?

答案在下面這張圖中:

img

這種情況testService6中其實第二級緩存是空的,不需要跟原始對象判斷,所以不會拋出循環依賴。

場景5:DependsOn循環依賴

還有一種有些特殊的場景,比如我們需要在實例化Bean A之前,先實例化Bean B,這個時候就可以使用@DependsOn注解。

@DependsOn(value = "testService2")
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

程序啟動之后,執行結果:

Circular depends-on relationship between 'testService2' and 'testService1'

這個例子中本來如果TestService1和TestService2都沒有加@DependsOn注解是沒問題的,反而加了這個注解會出現循環依賴問題。

這又是為什么?

答案在AbstractBeanFactory類的doGetBean方法的這段代碼中:

img它會檢查dependsOn的實例有沒有循環依賴,如果有循環依賴則拋異常。

總體策略:出現循環依賴如何解決?

項目中如果出現循環依賴問題,說明是spring默認無法解決的循環依賴,要看項目的打印日志,屬於哪種循環依賴。目前包含下面幾種情況:

img

生成代理對象產生的循環依賴 的解決方案:

這類循環依賴問題解決方法很多,主要有:

  1. 使用@Lazy注解,延遲加載
  2. 使用@DependsOn注解,指定加載先后關系
  3. 修改文件名稱,改變循環依賴類的加載順序
使用@DependsOn產生的循環依賴 的解決方案:

這類循環依賴問題要找到@DependsOn注解循環依賴的地方,迫使它不循環依賴就可以解決問題。

多例循環依賴 的解決方案:

這類循環依賴問題可以通過把bean改成單例的解決。

構造器循環依賴 的解決方案:

這類循環依賴問題可以通過使用@Lazy注解解決

回答提要:

按照上面的方式回答, 起碼120分。

但是答案太復雜, 如果上面的答案,記不住,就用下面的答案吧,至少也是100分。

問題: Spring是怎么解決循環依賴的?

首先,Spring 解決循環依賴有兩個前提條件:

  1. 不全是構造器方式的循環依賴
  2. 必須是單例

基於上面的問題,我們知道Bean的生命周期,本質上解決循環依賴的問題就是三級緩存,通過三級緩存提前拿到未初始化的對象。

第一級緩存:用來保存實例化、初始化都完成的對象

第二級緩存:用來保存實例化完成,但是未初始化完成的對象

第三級緩存:用來保存一個對象工廠,提供一個匿名內部類,用於創建二級緩存中的對象

圖片

假設一個簡單的循環依賴場景,A、B互相依賴。

圖片

A對象的創建過程:

  1. 創建對象A,實例化的時候把A對象工廠放入三級緩存

圖片

  1. A注入屬性時,發現依賴B,轉而去實例化B
  2. 同樣創建對象B,注入屬性時發現依賴A,一次從一級到三級緩存查詢A,從三級緩存通過對象工廠拿到A,把A放入二級緩存,同時刪除三級緩存中的A,此時,B已經實例化並且初始化完成,把B放入一級緩存。

圖片

  1. 接着繼續創建A,順利從一級緩存拿到實例化且初始化完成的B對象,A對象創建也完成,刪除二級緩存中的A,同時把A放入一級緩存
  2. 最后,一級緩存中保存着實例化、初始化都完成的A、B對象

圖片

因此,由於把實例化和初始化的流程分開了,所以如果都是用構造器的話,就沒法分離這個操作,所以都是構造器的話就無法解決循環依賴的問題了。

問題:為什么要三級緩存?二級不行嗎?

不可以,主要是為了生成代理對象。

因為三級緩存中放的是生成具體對象的匿名內部類,他可以生成代理對象,也可以是普通的實例對象。

使用三級緩存主要是為了保證不管什么時候使用的都是一個對象。

假設只有二級緩存的情況,往二級緩存中放的顯示一個普通的Bean對象,BeanPostProcessor去生成代理對象之后,覆蓋掉二級緩存中的普通Bean對象,那么多線程環境下可能取到的對象就不一致了。

圖片

Spring注解(8題目)

什么是基於Java的Spring注解配置? 給一些注解的例子

基於Java的配置,允許你在少量的Java注解的幫助下,進行你的大部分Spring配置而非通過XML文件。

以@Configuration 注解為例,它用來標記類可以當做一個bean的定義,被Spring IOC容器使用。

另一個例子是@Bean注解,它表示此方法將要返回一個對象,作為一個bean注冊進Spring應用上下文。

@Configuration
public class StudentConfig {
    @Bean
    public StudentBean myStudent() {
        return new StudentBean();
    }
}

怎樣開啟注解裝配?

注解裝配在默認情況下是不開啟的,為了使用注解裝配,我們必須在Spring配置文件中配置 <context:annotation-config/>元素。

@Component, @Controller, @Repository, @Service 有何區別?

@Component:這將 java 類標記為 bean。它是任何 Spring 管理組件的通用構造型。spring 的組件掃描機制現在可以將其拾取並將其拉入應用程序環境中。

@Controller:這將一個類標記為 Spring Web MVC 控制器。標有它的 Bean 會自動導入到 IoC 容器中。

@Service:此注解是組件注解的特化。它不會對 @Component 注解提供任何其他行為。您可以在服務層類中使用 @Service 而不是 @Component,因為它以更好的方式指定了意圖。

@Repository:這個注解是具有類似用途和功能的 @Component 注解的特化。它為 DAO 提供了額外的好處。它將 DAO 導入 IoC 容器,並使未經檢查的異常有資格轉換為 Spring DataAccessException。

@Required 注解有什么作用

這個注解表明bean的屬性必須在配置的時候設置,通過一個bean定義的顯式的屬性值或通過自動裝配,若@Required注解的bean屬性未被設置,容器將拋出BeanInitializationException。示例:

public class Employee {
    private String name;
    @Required
    public void setName(String name){
        this.name=name;
    }
    public string getName(){
        return name;
    }
}

@Autowired 注解有什么作用

@Autowired默認是按照類型裝配注入的,默認情況下它要求依賴對象必須存在(可以設置它required屬性為false)。@Autowired 注解提供了更細粒度的控制,包括在何處以及如何完成自動裝配。它的用法和@Required一樣,修飾setter方法、構造器、屬性或者具有任意名稱和/或多個參數的PN方法。

public class Employee {
    private String name;
    @Autowired
    public void setName(String name) {
        this.name=name;
    }
    public string getName(){
        return name;
    }
}

@Autowired和@Resource之間的區別

@Autowired可用於:構造函數、成員變量、Setter方法

@Autowired和@Resource之間的區別

  • @Autowired默認是按照類型裝配注入的,默認情況下它要求依賴對象必須存在(可以設置它required屬性為false)。
  • @Resource默認是按照名稱來裝配注入的,只有當找不到與名稱匹配的bean才會按照類型來裝配注入。

@Qualifier 注解有什么作用

當您創建多個相同類型的 bean 並希望僅使用屬性裝配其中一個 bean 時,您可以使用@Qualifier 注解和 @Autowired 通過指定應該裝配哪個確切的 bean 來消除歧義。

@RequestMapping 注解有什么用?

@RequestMapping 注解用於將特定 HTTP 請求方法映射到將處理相應請求的控制器中的特定類/方法。此注釋可應用於兩個級別:

  • 類級別:映射請求的 URL
  • 方法級別:映射 URL 以及 HTTP 請求方法

Spring數據訪問(14)

解釋對象/關系映射集成模塊

Spring 通過提供ORM模塊,支持我們在直接JDBC之上使用一個對象/關系映射映射(ORM)工具,Spring 支持集成主流的ORM框架,如Hiberate,JDO和 iBATIS,JPA,TopLink,JDO,OJB 。Spring的事務管理同樣支持以上所有ORM框架及JDBC。

在Spring框架中如何更有效地使用JDBC?

使用Spring JDBC 框架,資源管理和錯誤處理的代價都會被減輕。所以開發者只需寫statements 和 queries從數據存取數據,JDBC也可以在Spring框架提供的模板類的幫助下更有效地被使用,這個模板叫JdbcTemplate

解釋JDBC抽象和DAO模塊

通過使用JDBC抽象和DAO模塊,保證數據庫代碼的簡潔,並能避免數據庫資源錯誤關閉導致的問題,它在各種不同的數據庫的錯誤信息之上,提供了一個統一的異常訪問層。它還利用Spring的AOP 模塊給Spring應用中的對象提供事務管理服務。

spring DAO 有什么用?

Spring DAO(數據訪問對象) 使得 JDBC,Hibernate 或 JDO 這樣的數據訪問技術更容易以一種統一的方式工作。這使得用戶容易在持久性技術之間切換。它還允許您在編寫代碼時,無需考慮捕獲每種技術不同的異常。

spring JDBC API 中存在哪些類?

JdbcTemplate

SimpleJdbcTemplate

NamedParameterJdbcTemplate

SimpleJdbcInsert

SimpleJdbcCall

JdbcTemplate是什么

JdbcTemplate 類提供了很多便利的方法解決諸如把數據庫數據轉變成基本數據類型或對象,執行寫好的或可調用的數據庫操作語句,提供自定義的數據錯誤處理。

使用Spring通過什么方式訪問Hibernate?使用 Spring 訪問 Hibernate 的方法有哪些?

在Spring中有兩種方式訪問Hibernate:

  • 使用 Hibernate 模板和回調進行控制反轉
  • 擴展 HibernateDAOSupport 並應用 AOP 攔截器節點

如何通過HibernateDaoSupport將Spring和Hibernate結合起來?

用Spring的 SessionFactory 調用 LocalSessionFactory。集成過程分三步:

  • 配置the Hibernate SessionFactory
  • 繼承HibernateDaoSupport實現一個DAO
  • 在AOP支持的事務中裝配

Spring支持的事務管理類型, spring 事務實現方式有哪些?

Spring支持兩種類型的事務管理:

編程式事務管理:這意味你通過編程的方式管理事務,給你帶來極大的靈活性,但是難維護。

聲明式事務管理:這意味着你可以將業務代碼和事務管理分離,你只需用注解和XML配置來管理事務。

Spring事務的實現方式和實現原理

Spring事務的本質其實就是數據庫對事務的支持,沒有數據庫的事務支持,spring是無法提供事務功能的。真正的數據庫層的事務提交和回滾是通過binlog或者redo log實現的。

說一下Spring的事務傳播行為

spring事務的傳播行為說的是,當多個事務同時存在的時候,spring如何處理這些事務的行為。

① PROPAGATION_REQUIRED:如果當前沒有事務,就創建一個新事務,如果當前存在事務,就加入該事務,該設置是最常用的設置。

② PROPAGATION_SUPPORTS:支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就以非事務執行。

③ PROPAGATION_MANDATORY:支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就拋出異常。

④ PROPAGATION_REQUIRES_NEW:創建新事務,無論當前存不存在事務,都創建新事務。

⑤ PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

⑥ PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則拋出異常。

⑦ PROPAGATION_NESTED:如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則按REQUIRED屬性執行。

說一下 spring 的事務隔離?

spring 有五大隔離級別,默認值為 ISOLATION_DEFAULT(使用數據庫的設置),其他四個隔離級別和數據庫的隔離級別一致:

  1. ISOLATION_DEFAULT:用底層數據庫的設置隔離級別,數據庫設置的是什么我就用什么;
  2. ISOLATION_READ_UNCOMMITTED:未提交讀,最低隔離級別、事務未提交前,就可被其他事務讀取(會出現幻讀、臟讀、不可重復讀);
  3. ISOLATION_READ_COMMITTED:提交讀,一個事務提交后才能被其他事務讀取到(會造成幻讀、不可重復讀),SQL server 的默認級別;
  4. ISOLATION_REPEATABLE_READ:可重復讀,保證多次讀取同一個數據時,其值都和事務開始時候的內容是一致,禁止讀取到別的事務未提交的數據(會造成幻讀),MySQL 的默認級別;
  5. ISOLATION_SERIALIZABLE:序列化,代價最高最可靠的隔離級別,該隔離級別能防止臟讀、不可重復讀、幻讀。

臟讀 :表示一個事務能夠讀取另一個事務中還未提交的數據。比如,某個事務嘗試插入記錄 A,此時該事務還未提交,然后另一個事務嘗試讀取到了記錄 A。

不可重復讀 :是指在一個事務內,多次讀同一數據。

幻讀 :指同一個事務內多次查詢返回的結果集不一樣。比如同一個事務 A 第一次查詢時候有 n 條記錄,但是第二次同等條件下查詢卻有 n+1 條記錄,這就好像產生了幻覺。發生幻讀的原因也是另外一個事務新增或者刪除或者修改了第一個事務結果集里面的數據,同一個記錄的數據內容被修改了,所有數據行的記錄就變多或者變少了。

Spring框架的事務管理有哪些優點?

  • 為不同的事務API 如 JTA,JDBC,Hibernate,JPA 和JDO,提供一個不變的編程模式。
  • 為編程式事務管理提供了一套簡單的API而不是一些復雜的事務API
  • 支持聲明式事務管理。
  • 和Spring各種數據訪問抽象層很好得集成。

你更傾向用那種事務管理類型?

大多數Spring框架的用戶選擇聲明式事務管理,因為它對應用代碼的影響最小,因此更符合一個無侵入的輕量級容器的思想。聲明式事務管理要優於編程式事務管理,雖然比編程式事務管理(這種方式允許你通過代碼控制事務)少了一點靈活性。唯一不足地方是,最細粒度只能作用到方法級別,無法做到像編程式事務那樣可以作用到代碼塊級別。

Spring面向切面編程(AOP)(13)

什么是AOP

OOP(Object-Oriented Programming)面向對象編程,允許開發者定義縱向的關系,但並適用於定義橫向的關系,導致了大量代碼的重復,而不利於各個模塊的重用。

AOP(Aspect-Oriented Programming),一般稱為面向切面編程,作為面向對象的一種補充,用於將那些與業務無關,但卻對多個對象產生影響的公共行為和邏輯,抽取並封裝為一個可重用的模塊,這個模塊被命名為“切面”(Aspect),減少系統中的重復代碼,降低了模塊間的耦合度,同時提高了系統的可維護性。可用於權限認證、日志、事務處理等。

Spring AOP and AspectJ AOP 有什么區別?AOP 有哪些實現方式?

AOP實現的關鍵在於 代理模式,AOP代理主要分為靜態代理和動態代理。靜態代理的代表為AspectJ;動態代理則以Spring AOP為代表。

(1)AspectJ是靜態代理的增強,所謂靜態代理,就是AOP框架會在編譯階段生成AOP代理類,因此也稱為編譯時增強,他會在編譯階段將AspectJ(切面)織入到Java字節碼中,運行的時候就是增強之后的AOP對象。

(2)Spring AOP使用的動態代理,所謂的動態代理就是說AOP框架不會去修改字節碼,而是每次運行時在內存中臨時為方法生成一個AOP對象,這個AOP對象包含了目標對象的全部方法,並且在特定的切點做了增強處理,並回調原對象的方法。

JDK動態代理和CGLIB動態代理的區別

Spring AOP中的動態代理主要有兩種方式,JDK動態代理和CGLIB動態代理:

  • JDK動態代理只提供接口的代理,不支持類的代理。核心InvocationHandler接口和Proxy類,InvocationHandler 通過invoke()方法反射來調用目標類中的代碼,動態地將橫切邏輯和業務編織在一起;接着,Proxy利用 InvocationHandler動態創建一個符合某一接口的的實例, 生成目標類的代理對象。
  • 如果代理類沒有實現 InvocationHandler 接口,那么Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library),是一個代碼生成的類庫,可以在運行時動態的生成指定類的一個子類對象,並覆蓋其中特定方法並添加增強代碼,從而實現AOP。CGLIB是通過繼承的方式做的動態代理,因此如果某個類被標記為final,那么它是無法使用CGLIB做動態代理的。

靜態代理與動態代理區別在於生成AOP代理對象的時機不同,相對來說AspectJ的靜態代理方式具有更好的性能,但是AspectJ需要特定的編譯器進行處理,而Spring AOP則無需特定的編譯器處理。

InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最終生成的代理實例; method 是被代理目標實例的某個具體方法; args 是被代理目標實例某個方法的具體入參, 在方法反射調用時使用。

如何理解 Spring 中的代理?

將 Advice 應用於目標對象后創建的對象稱為代理。在客戶端對象的情況下,目標對象和代理對象是相同的。

Advice + Target Object = Proxy

解釋一下Spring AOP里面的幾個名詞

(1)切面(Aspect):切面是通知和切點的結合。通知和切點共同定義了切面的全部內容。 在Spring AOP中,切面可以使用通用類(基於模式的風格) 或者在普通類中以 @AspectJ 注解來實現。

(2)連接點(Join point):指方法,在Spring AOP中,一個連接點 總是 代表一個方法的執行。 應用可能有數以千計的時機應用通知。這些時機被稱為連接點。連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,並添加新的行為。

(3)通知(Advice):在AOP術語中,切面的工作被稱為通知。

(4)切入點(Pointcut):切點的定義會匹配通知所要織入的一個或多個連接點。我們通常使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。

(5)引入(Introduction):引入允許我們向現有類添加新方法或屬性。

(6)目標對象(Target Object): 被一個或者多個切面(aspect)所通知(advise)的對象。它通常是一個代理對象。也有人把它叫做 被通知(adviced) 對象。 既然Spring AOP是通過運行時代理實現的,這個對象永遠是一個 被代理(proxied) 對象。

(7)織入(Weaving):織入是把切面應用到目標對象並創建新的代理對象的過程。在目標對象的生命周期里有多少個點可以進行織入:

  • 編譯期:切面在目標類編譯時被織入。AspectJ的織入編譯器是以這種方式織入切面的。
  • 類加載期:切面在目標類加載到JVM時被織入。需要特殊的類加載器,它可以在目標類被引入應用之前增強該目標類的字節碼。AspectJ5的加載時織入就支持以這種方式織入切面。
  • 運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會為目標對象動態地創建一個代理對象。SpringAOP就是以這種方式織入切面。

Spring在運行時通知對象

通過在代理類中包裹切面,Spring在運行期把切面織入到Spring管理的bean中。代理封裝了目標類,並攔截被通知方法的調用,再把調用轉發給真正的目標bean。當代理攔截到方法調用時,在調用目標bean方法之前,會執行切面邏輯。

直到應用需要被代理的bean時,Spring才創建代理對象。如果使用的是ApplicationContext的話,在ApplicationContext從BeanFactory中加載所有bean的時候,Spring才會創建被代理的對象。因為Spring運行時才創建代理對象,所以我們不需要特殊的編譯器來織入SpringAOP的切面。

Spring只支持方法級別的連接點

因為Spring基於動態代理,所以Spring只支持方法連接點。Spring缺少對字段連接點的支持,而且它不支持構造器連接點。方法之外的連接點攔截功能,我們可以利用Aspect來補充。

在Spring AOP 中,關注點和橫切關注的區別是什么?在 spring aop 中 concern 和 cross-cutting concern 的不同之處

關注點(concern)是應用中一個模塊的行為,一個關注點可能會被定義成一個我們想實現的一個功能。

橫切關注點(cross-cutting concern)是一個關注點,此關注點是整個應用都會使用的功能,並影響整個應用,比如日志,安全和數據傳輸,幾乎應用的每個模塊都需要的功能。因此這些都屬於橫切關注點。

Spring通知有哪些類型?

在AOP術語中,切面的工作被稱為通知,實際上是程序執行時要通過SpringAOP框架觸發的代碼段。

Spring切面可以應用5種類型的通知:

  1. 前置通知(Before):在目標方法被調用之前調用通知功能;
  2. 后置通知(After):在目標方法完成之后調用通知,此時不會關心方法的輸出是什么;
  3. 返回通知(After-returning ):在目標方法成功執行之后調用通知;
  4. 異常通知(After-throwing):在目標方法拋出異常后調用通知;
  5. 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之后執行自定義的行為。

img

同一個aspect,不同advice的執行順序:

①沒有異常情況下的執行順序:

around before advice
before advice
target method 執行
around after advice
after advice
afterReturning

②有異常情況下的執行順序:

around before advice
before advice
target method 執行
around after advice
after advice
afterThrowing:異常發生
java.lang.RuntimeException: 異常發生

什么是切面 Aspect?

aspect 由 pointcount 和 advice 組成,切面是通知和切點的結合。 它既包含了橫切邏輯的定義, 也包括了連接點的定義. Spring AOP 就是負責實施切面的框架, 它將切面所定義的橫切邏輯編織到切面所指定的連接點中.
AOP 的工作重心在於如何將增強編織目標對象的連接點上, 這里包含兩個工作:

  • 如何通過 pointcut 和 advice 定位到特定的 joinpoint 上
  • 如何在 advice 中編寫切面代碼.

可以簡單地認為, 使用 @Aspect 注解的類就是切面.

在這里插入圖片描述

解釋基於XML Schema方式的切面實現

在這種情況下,切面由常規類以及基於XML的配置實現。

解釋基於注解的切面實現

在這種情況下(基於@AspectJ的實現),涉及到的切面聲明的風格與帶有java5標注的普通java類一致。

有幾種不同類型的自動代理?

BeanNameAutoProxyCreator

DefaultAdvisorAutoProxyCreator

Metadata autoproxying

Spring MVC面試題 專題部分

什么是Spring MVC?簡單介紹下你對Spring MVC的理解?

Spring MVC是一個基於Java的實現了MVC設計模式的請求驅動類型的輕量級Web框架,通過把模型-視圖-控制器分離,將web層進行職責解耦,把復雜的web應用分成邏輯清晰的幾部分,簡化開發,減少出錯,方便組內開發人員之間的配合。

Spring MVC的優點

(1)可以支持各種視圖技術,而不僅僅局限於JSP;

(2)與Spring框架集成(如IoC容器、AOP等);

(3)清晰的角色分配:前端控制器(dispatcherServlet) , 請求到處理器映射(handlerMapping), 處理器適配器(HandlerAdapter), 視圖解析器(ViewResolver)。

(4) 支持各種請求資源的映射策略。

核心組件

Spring MVC的主要組件?

(1)前端控制器 DispatcherServlet(不需要程序員開發)

作用:接收請求、響應結果,相當於轉發器,有了DispatcherServlet 就減少了其它組件之間的耦合度。

(2)處理器映射器HandlerMapping(不需要程序員開發)

作用:根據請求的URL來查找Handler

(3)處理器適配器HandlerAdapter

注意:在編寫Handler的時候要按照HandlerAdapter要求的規則去編寫,這樣適配器HandlerAdapter才可以正確的去執行Handler。

(4)處理器Handler(需要程序員開發)

(5)視圖解析器 ViewResolver(不需要程序員開發)

作用:進行視圖的解析,根據視圖邏輯名解析成真正的視圖(view)

(6)視圖View(需要程序員開發jsp)

View是一個接口, 它的實現類支持不同的視圖類型(jsp,freemarker,pdf等等)

什么是DispatcherServlet

Spring的MVC框架是圍繞DispatcherServlet來設計的,它用來處理所有的HTTP請求和響應。

什么是Spring MVC框架的控制器?

控制器提供一個訪問應用程序的行為,此行為通常通過服務接口實現。控制器解析用戶輸入並將其轉換為一個由視圖呈現給用戶的模型。Spring用一個非常抽象的方式實現了一個控制層,允許用戶創建多種用途的控制器。

Spring MVC的控制器是不是單例模式,如果是,有什么問題,怎么解決?

答:是單例模式,所以在多線程訪問的時候有線程安全問題,不要用同步,會影響性能的,解決方案是在控制器里面不能寫字段。

工作原理

請描述Spring MVC的工作流程?描述一下 DispatcherServlet 的工作流程?

(1)用戶發送請求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到請求后,調用HandlerMapping處理器映射器,請求獲取Handle;
(3)處理器映射器根據請求url找到具體的處理器,生成處理器對象及處理器攔截器(如果有則生成)一並返回給DispatcherServlet;
(4)DispatcherServlet 調用 HandlerAdapter處理器適配器;
(5)HandlerAdapter 經過適配調用 具體處理器(Handler,也叫后端控制器);
(6)Handler執行完成返回ModelAndView;
(7)HandlerAdapter將Handler執行結果ModelAndView返回給DispatcherServlet;
(8)DispatcherServlet將ModelAndView傳給ViewResolver視圖解析器進行解析;
(9)ViewResolver解析后返回具體View;
(10)DispatcherServlet對View進行渲染視圖(即將模型數據填充至視圖中)
(11)DispatcherServlet響應用戶。

img

MVC框架

MVC是什么?MVC設計模式的好處有哪些

mvc是一種設計模式(設計模式就是日常開發中編寫代碼的一種好的方法和經驗的總結)。模型(model)-視圖(view)-控制器(controller),三層架構的設計模式。用於實現前端頁面的展現與后端業務數據處理的分離。

mvc設計模式的好處

1.分層設計,實現了業務系統各個組件之間的解耦,有利於業務系統的可擴展性,可維護性。

2.有利於系統的並行開發,提升開發效率。

常用注解

注解原理是什么

注解本質是一個繼承了Annotation的特殊接口,其具體實現類是Java運行時生成的動態代理類。我們通過反射獲取注解時,返回的是Java運行時生成的動態代理對象。通過代理對象調用自定義注解的方法,會最終調用AnnotationInvocationHandler的invoke方法。該方法會從memberValues這個Map中索引出對應的值。而memberValues的來源是Java常量池。

Spring MVC常用的注解有哪些?

@RequestMapping:用於處理請求 url 映射的注解,可用於類或方法上。用於類上,則表示類中的所有響應請求的方法都是以該地址作為父路徑。

@RequestBody:注解實現接收http請求的json數據,將json轉換為java對象。

@ResponseBody:注解實現將conreoller方法返回對象轉化為json對象響應給客戶。

SpingMvc中的控制器的注解一般用哪個,有沒有別的注解可以替代?

答:一般用@Controller注解,也可以使用@RestController,@RestController注解相當於@ResponseBody + @Controller,表示是表現層,除此之外,一般不用別的注解代替。

@Controller注解的作用

在Spring MVC 中,控制器Controller 負責處理由DispatcherServlet 分發的請求,它把用戶請求的數據經過業務處理層處理之后封裝成一個Model ,然后再把該Model 返回給對應的View 進行展示。在Spring MVC 中提供了一個非常簡便的定義Controller 的方法,你無需繼承特定的類或實現特定的接口,只需使用@Controller 標記一個類是Controller ,然后使用@RequestMapping 和@RequestParam 等一些注解用以定義URL 請求和Controller 方法之間的映射,這樣的Controller 就能被外界訪問到。此外Controller 不會直接依賴於HttpServletRequest 和HttpServletResponse 等HttpServlet 對象,它們可以通過Controller 的方法參數靈活的獲取到。

@Controller 用於標記在一個類上,使用它標記的類就是一個Spring MVC Controller 對象。分發處理器將會掃描使用了該注解的類的方法,並檢測該方法是否使用了@RequestMapping 注解。@Controller 只是定義了一個控制器類,而使用@RequestMapping 注解的方法才是真正處理請求的處理器。單單使用@Controller 標記在一個類上還不能真正意義上的說它就是Spring MVC 的一個控制器類,因為這個時候Spring 還不認識它。那么要如何做Spring 才能認識它呢?這個時候就需要我們把這個控制器類交給Spring 來管理。有兩種方式:

  • 在Spring MVC 的配置文件中定義MyController 的bean 對象。
  • 在Spring MVC 的配置文件中告訴Spring 該到哪里去找標記為@Controller 的Controller 控制器。

@RequestMapping注解的作用

RequestMapping是一個用來處理請求地址映射的注解,可用於類或方法上。用於類上,表示類中的所有響應請求的方法都是以該地址作為父路徑。

RequestMapping注解有六個屬性,下面我們把她分成三類進行說明(下面有相應示例)。

value, method

value: 指定請求的實際地址,指定的地址可以是URI Template 模式(后面將會說明);

method: 指定請求的method類型, GET、POST、PUT、DELETE等;

consumes,produces

consumes: 指定處理請求的提交內容類型(Content-Type),例如application/json, text/html;

produces: 指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回;

params,headers

params: 指定request中必須包含某些參數值是,才讓該方法處理。

headers: 指定request中必須包含某些指定的header值,才能讓該方法處理請求。

@ResponseBody注解的作用

作用: 該注解用於將Controller的方法返回的對象,通過適當的HttpMessageConverter轉換為指定格式后,寫入到Response對象的body數據區。

使用時機:返回的數據不是html標簽的頁面,而是其他某種格式的數據時(如json、xml等)使用;

@PathVariable和@RequestParam的區別

請求路徑上有個id的變量值,可以通過@PathVariable來獲取 @RequestMapping(value = “/page/{id}”, method = RequestMethod.GET)

@RequestParam用來獲得靜態的URL請求入參 spring注解時action里用到。

其他

Spring MVC與Struts2區別

相同點

都是基於mvc的表現層框架,都用於web項目的開發。

不同點

1.前端控制器不一樣。Spring MVC的前端控制器是servlet:DispatcherServlet。struts2的前端控制器是filter:StrutsPreparedAndExcutorFilter。

2.請求參數的接收方式不一樣。Spring MVC是使用方法的形參接收請求的參數,基於方法的開發,線程安全,可以設計為單例或者多例的開發,推薦使用單例模式的開發(執行效率更高),默認就是單例開發模式。struts2是通過類的成員變量接收請求的參數,是基於類的開發,線程不安全,只能設計為多例的開發。

3.Struts采用值棧存儲請求和響應的數據,通過OGNL存取數據,Spring MVC通過參數解析器是將request請求內容解析,並給方法形參賦值,將數據和視圖封裝成ModelAndView對象,最后又將ModelAndView中的模型數據通過reques域傳輸到頁面。Jsp視圖解析器默認使用jstl。

4.與spring整合不一樣。Spring MVC是spring框架的一部分,不需要整合。在企業項目中,Spring MVC使用更多一些。

Spring MVC怎么樣設定重定向和轉發的?

(1)轉發:在返回值前面加"forward:",譬如"forward:user.do?name=method4"

(2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com"

Spring MVC怎么和AJAX相互調用的?

通過Jackson框架就可以把Java里面的對象直接轉化成Js可以識別的Json對象。具體步驟如下 :

(1)加入Jackson.jar

(2)在配置文件中配置json的映射

(3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。

如何解決POST請求中文亂碼問題,GET的又如何處理呢?

(1)解決post請求亂碼問題:

在web.xml中配置一個CharacterEncodingFilter過濾器,設置成utf-8;

<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>

    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

(2)get請求中文參數出現亂碼解決方法有兩個:

①修改tomcat配置文件添加編碼與工程編碼一致,如下:

<ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>

②另外一種方法對參數進行重新編碼:

String userName = new String(request.getParamter(“userName”).getBytes(“ISO8859-1”),“utf-8”)

ISO8859-1是tomcat默認編碼,需要將tomcat編碼后的內容按utf-8編碼。

Spring MVC的異常處理?

答:可以將異常拋給Spring框架,由Spring框架來處理;我們只需要配置簡單的異常處理器,在異常處理器中添視圖頁面即可。

如果在攔截請求中,我想攔截get方式提交的方法,怎么配置

答:可以在@RequestMapping注解里面加上method=RequestMethod.GET。

怎樣在方法里面得到Request,或者Session?

答:直接在方法的形參中聲明request,Spring MVC就自動把request對象傳入。

如果想在攔截的方法里面得到從前台傳入的參數,怎么得到?

答:直接在形參里面聲明這個參數就可以,但必須名字和傳過來的參數一樣。

如果前台有很多個參數傳入,並且這些參數都是一個對象的,那么怎么樣快速得到這個對象?

答:直接在方法中聲明這個對象,Spring MVC就自動會把屬性賦值到這個對象里面。

Spring MVC中函數的返回值是什么?

答:返回值可以有很多類型,有String, ModelAndView。ModelAndView類把視圖和數據都合並的一起的,但一般用String比較好。

Spring MVC用什么對象從后台向前台傳遞數據的?

答:通過ModelMap對象,可以在這個對象里面調用put方法,把對象加到里面,前台就可以通過el表達式拿到。

怎么樣把ModelMap里面的數據放入Session里面?

答:可以在類上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。

Spring MVC里面攔截器是怎么寫的

有兩種寫法,一種是實現HandlerInterceptor接口,另外一種是繼承適配器類,接着在接口方法當中,實現處理邏輯;然后在Spring MVC的配置文件中配置攔截器即可:

<!-- 配置Spring MVC的攔截器 -->
<mvc:interceptors>
    <!-- 配置一個攔截器的Bean就可以了 默認是對所有請求都攔截 -->
    <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean>
    <!-- 只針對部分請求攔截 -->
    <mvc:interceptor>
       <mvc:mapping path="/modelMap.do" />
       <bean class="com.zwp.action.MyHandlerInterceptorAdapter" />
    </mvc:interceptor>
</mvc:interceptors>

介紹一下 WebApplicationContext

WebApplicationContext 繼承了ApplicationContext 並增加了一些WEB應用必備的特有功能,它不同於一般的ApplicationContext ,因為它能處理主題,並找到被關聯的servlet。

Tomcat 專題 部分

Tomcat是什么?

Tomcat 服務器Apache軟件基金會項目中的一個核心項目,是一個免費的開放源代碼的Web 應用服務器,屬於輕量級應用服務器,在中小型系統和並發訪問用戶不是很多的場合下被普遍使用,是開發和調試JSP 程序的首選。

Tomcat的缺省端口是多少,怎么修改

  1. 找到Tomcat目錄下的conf文件夾
  2. 進入conf文件夾里面找到server.xml文件
  3. 打開server.xml文件
  4. 在server.xml文件里面找到下列信息
  5. 把Connector標簽的8080端口改成你想要的端口
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" />

tomcat 有哪幾種Connector 運行模式(優化)?

下面,我們先大致了解Tomcat Connector的三種運行模式。

  • BIO:同步並阻塞 一個線程處理一個請求。缺點:並發量高時,線程數較多,浪費資源。Tomcat7或以下,在Linux系統中默認使用這種方式。

配制項:protocol=”HTTP/1.1”

  • NIO:同步非阻塞IO

    利用Java的異步IO處理,可以通過少量的線程處理大量的請求,可以復用同一個線程處理多個connection(多路復用)。

    Tomcat8在Linux系統中默認使用這種方式。

    Tomcat7必須修改Connector配置來啟動。

    配制項:protocol=”org.apache.coyote.http11.Http11NioProtocol”

    備注:我們常用的Jetty,Mina,ZooKeeper等都是基於java nio實現.

  • APR:即Apache Portable Runtime,從操作系統層面解決io阻塞問題。AIO方式,****異步非阻塞IO(Java NIO2又叫AIO) 主要與NIO的區別主要是操作系統的底層區別.可以做個比喻:比作快遞,NIO就是網購后要自己到官網查下快遞是否已經到了(可能是多次),然后自己去取快遞;AIO就是快遞員送貨上門了(不用關注快遞進度)。

    配制項:protocol=”org.apache.coyote.http11.Http11AprProtocol”

    備注:需在本地服務器安裝APR庫。Tomcat7或Tomcat8在Win7或以上的系統中啟動默認使用這種方式。Linux如果安裝了apr和native,Tomcat直接啟動就支持apr。

Tomcat有幾種部署方式?

在Tomcat中部署Web應用的方式主要有如下幾種:

  1. 利用Tomcat的自動部署。

    把web應用拷貝到webapps目錄。Tomcat在啟動時會加載目錄下的應用,並將編譯后的結果放入work目錄下。

  2. 使用Manager App控制台部署。

    在tomcat主頁點擊“Manager App” 進入應用管理控制台,可以指定一個web應用的路徑或war文件。

  3. 修改conf/server.xml文件部署。

    修改conf/server.xml文件,增加Context節點可以部署應用。

  4. 增加自定義的Web部署文件。

    在conf/Catalina/localhost/ 路徑下增加 xyz.xml文件,內容是Context節點,可以部署應用。

tomcat容器是如何創建servlet類實例?用到了什么原理?

  1. 當容器啟動時,會讀取在webapps目錄下所有的web應用中的web.xml文件,然后對 xml文件進行解析,並讀取servlet注冊信息。然后,將每個應用中注冊的servlet類都進行加載,並通過 反射的方式實例化。(有時候也是在第一次請求時實例化)
  2. 在servlet注冊時加上1如果為正數,則在一開始就實例化,如果不寫或為負數,則第一次請求實例化。

Tomcat工作模式

Tomcat作為servlet容器,有三種工作模式:

  • 1、獨立的servlet容器,servlet容器是web服務器的一部分;
  • 2、進程內的servlet容器,servlet容器是作為web服務器的插件和java容器的實現,web服務器插件在內部地址空間打開一個jvm使得java容器在內部得以運行。反應速度快但伸縮性不足;
  • 3、進程外的servlet容器,servlet容器運行於web服務器之外的地址空間,並作為web服務器的插件和java容器實現的結合。反應時間不如進程內但伸縮性和穩定性比進程內優;

進入Tomcat的請求可以根據Tomcat的工作模式分為如下兩類:

  • Tomcat作為應用程序服務器:請求來自於前端的web服務器,這可能是Apache, IIS, Nginx等;
  • Tomcat作為獨立服務器:請求來自於web瀏覽器;

面試時問到Tomcat相關問題的幾率並不高,正式因為如此,很多人忽略了對Tomcat相關技能的掌握,下面這一篇文章整理了Tomcat相關的系統架構,介紹了Server、Service、Connector、Container之間的關系,各個模塊的功能,可以說把這幾個掌握住了,Tomcat相關的面試題你就不會有任何問題了!另外,在面試的時候你還要有意識無意識的往Tomcat這個地方引,就比如說常見的Spring MVC的執行流程,一個URL的完整調用鏈路,這些相關的題目你是可以往Tomcat處理請求的這個過程去說的!掌握了Tomcat這些技能,面試官一定會佩服你的!

學了本章之后你應該明白的是:

  • Server、Service、Connector、Container四大組件之間的關系和聯系,以及他們的主要功能點;
  • Tomcat執行的整體架構,請求是如何被一步步處理的;
  • Engine、Host、Context、Wrapper相關的概念關系;
  • Container是如何處理請求的;
  • Tomcat用到的相關設計模式;

Tomcat頂層架構

俗話說,站在巨人的肩膀上看世界,一般學習的時候也是先總覽一下整體,然后逐個部分個個擊破,最后形成思路,了解具體細節,Tomcat的結構很復雜,但是 Tomcat 非常的模塊化,找到了 Tomcat 最核心的模塊,問題才可以游刃而解,了解了 Tomcat 的整體架構對以后深入了解 Tomcat 來說至關重要!

先上一張Tomcat的頂層結構圖(圖A),如下:

在這里插入圖片描述

Tomcat中最頂層的容器是Server,代表着整個服務器,從上圖中可以看出,一個Server可以包含至少一個Service,即可以包含多個Service,用於具體提供服務。

Service主要包含兩個部分:Connector和Container。從上圖中可以看出 Tomcat 的心臟就是這兩個組件,他們的作用如下:

  • Connector用於處理連接相關的事情,並提供Socket與Request請求和Response響應相關的轉化;
  • Container用於封裝和管理Servlet,以及具體處理Request請求;

一個Tomcat中只有一個Server,一個Server可以包含多個Service,一個Service只有一個Container,但是可以有多個Connectors,這是因為一個服務可以有多個連接,如同時提供Http和Https鏈接,也可以提供向相同協議不同端口的連接,示意圖如下(Engine、Host、Context下面會說到):

在這里插入圖片描述

多個 Connector 和一個 Container 就形成了一個 Service,有了 Service 就可以對外提供服務了,但是 Service 還要一個生存的環境,必須要有人能夠給她生命、掌握其生死大權,那就非 Server 莫屬了!所以整個 Tomcat 的生命周期由 Server 控制。

另外,上述的包含關系或者說是父子關系,都可以在tomcat的conf目錄下的server.xml配置文件中看出,下圖是刪除了注釋內容之后的一個完整的server.xml配置文件(Tomcat版本為8.0)

在這里插入圖片描述

詳細的配置文件內容可以到Tomcat官網查看:Tomcat配置文件

上邊的配置文件,還可以通過下邊的一張結構圖更清楚的理解:

在這里插入圖片描述

Server標簽設置的端口號為8005,shutdown=”SHUTDOWN” ,表示在8005端口監聽“SHUTDOWN”命令,如果接收到了就會關閉Tomcat。一個Server有一個Service,當然還可以進行配置,一個Service有多個Connector,Service左邊的內容都屬於Container的,Service下邊是Connector。

Tomcat頂層架構小結

  1. Tomcat中只有一個Server,一個Server可以有多個Service,一個Service可以有多個Connector和一個Container;
  2. Server掌管着整個Tomcat的生死大權;
  3. Service 是對外提供服務的;
  4. Connector用於接受請求並將請求封裝成Request和Response來具體處理;
  5. Container用於封裝和管理Servlet,以及具體處理request請求;

知道了整個Tomcat頂層的分層架構和各個組件之間的關系以及作用,對於絕大多數的開發人員來說Server和Service對我們來說確實很遠,而我們開發中絕大部分進行配置的內容是屬於Connector和Container的,所以接下來介紹一下Connector和Container。

Connector和Container的微妙關系

由上述內容我們大致可以知道一個請求發送到Tomcat之后,首先經過Service然后會交給我們的Connector,Connector用於接收請求並將接收的請求封裝為Request和Response來具體處理,Request和Response封裝完之后再交由Container進行處理,Container處理完請求之后再返回給Connector,最后在由Connector通過Socket將處理的結果返回給客戶端,這樣整個請求的就處理完了!

Connector最底層使用的是Socket來進行連接的,Request和Response是按照HTTP協議來封裝的,所以Connector同時需要實現TCP/IP協議和HTTP協議!

Tomcat既然需要處理請求,那么肯定需要先接收到這個請求,接收請求這個東西我們首先就需要看一下Connector!

Connector架構分析

Connector用於接受請求並將請求封裝成Request和Response,然后交給Container進行處理,Container處理完之后在交給Connector返回給客戶端。

因此,我們可以把Connector分為四個方面進行理解:

  1. Connector如何接受請求的?
  2. 如何將請求封裝成Request和Response的?
  3. 封裝完之后的Request和Response如何交給Container進行處理的?
  4. Container處理完之后如何交給Connector並返回給客戶端的?

首先看一下Connector的結構圖(圖B),如下所示:

在這里插入圖片描述

Connector就是使用ProtocolHandler來處理請求的,不同的ProtocolHandler代表不同的連接類型,比如:Http11Protocol使用的是普通Socket來連接的,Http11NioProtocol使用的是NioSocket來連接的。

其中ProtocolHandler由包含了三個部件:Endpoint、Processor、Adapter。

  1. Endpoint用來處理底層Socket的網絡連接,Processor用於將Endpoint接收到的Socket封裝成Request,Adapter用於將Request交給Container進行具體的處理。
  2. Endpoint由於是處理底層的Socket網絡連接,因此Endpoint是用來實現TCP/IP協議的,而Processor用來實現HTTP協議的,Adapter將請求適配到Servlet容器進行具體的處理。
  3. Endpoint的抽象實現AbstractEndpoint里面定義的Acceptor和AsyncTimeout兩個內部類和一個Handler接口。Acceptor用於監聽請求,AsyncTimeout用於檢查異步Request的超時,Handler用於處理接收到的Socket,在內部調用Processor進行處理。

至此,我們應該很輕松的回答1,2,3的問題了,但是4還是不知道,那么我們就來看一下Container是如何進行處理的以及處理完之后是如何將處理完的結果返回給Connector的?

Container架構分析

Container用於封裝和管理Servlet,以及具體處理Request請求,在Container內部包含了4個子容器,結構圖如下(圖C):

在這里插入圖片描述

4個子容器的作用分別是:

  1. Engine:引擎,用來管理多個站點,一個Service最多只能有一個Engine;
  2. Host:代表一個站點,也可以叫虛擬主機,通過配置Host就可以添加站點;
  3. Context:代表一個應用程序,對應着平時開發的一套程序,或者一個WEB-INF目錄以及下面的web.xml文件;
  4. Wrapper:每一Wrapper封裝着一個Servlet;

下面找一個Tomcat的文件目錄對照一下,如下圖所示:

在這里插入圖片描述

Context和Host的區別是Context表示一個應用,我們的Tomcat中默認的配置下webapps下的每一個文件夾目錄都是一個Context,其中ROOT目錄中存放着主應用,其他目錄存放着子應用,而整個webapps就是一個Host站點。

我們訪問應用Context的時候,如果是ROOT下的則直接使用域名就可以訪問,例如:www.baidu.com,如果是Host(webapps)下的其他應用,則可以使用www.baidu.com/docs進行訪問,當然默認指定的根應用(ROOT)是可以進行設定的,只不過Host站點下默認的主應用是ROOT目錄下的。

看到這里我們知道Container是什么,但是還是不知道Container是如何進行請求處理的以及處理完之后是如何將處理完的結果返回給Connector的?別急!下邊就開始探討一下Container是如何進行處理的!

Container如何處理請求的

Container處理請求是使用Pipeline-Valve管道來處理的!(Valve是閥門之意)

Pipeline-Valve是責任鏈模式,責任鏈模式是指在一個請求處理的過程中有很多處理者依次對請求進行處理,每個處理者負責做自己相應的處理,處理完之后將處理后的結果返回,再讓下一個處理者繼續處理。

在這里插入圖片描述

但是!Pipeline-Valve使用的責任鏈模式和普通的責任鏈模式有些不同!區別主要有以下兩點:

  • 每個Pipeline都有特定的Valve,而且是在管道的最后一個執行,這個Valve叫做BaseValve,BaseValve是不可刪除的;
  • 在上層容器的管道的BaseValve中會調用下層容器的管道。

我們知道Container包含四個子容器,而這四個子容器對應的BaseValve分別在:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。

Pipeline的處理流程圖如下(圖D):

在這里插入圖片描述

  • Connector在接收到請求后會首先調用最頂層容器的Pipeline來處理,這里的最頂層容器的Pipeline就是EnginePipeline(Engine的管道);
  • 在Engine的管道中依次會執行EngineValve1、EngineValve2等等,最后會執行StandardEngineValve,在StandardEngineValve中會調用Host管道,然后再依次執行Host的HostValve1、HostValve2等,最后在執行StandardHostValve,然后再依次調用Context的管道和Wrapper的管道,最后執行到StandardWrapperValve。
  • 當執行到StandardWrapperValve的時候,會在StandardWrapperValve中創建FilterChain,並調用其doFilter方法來處理請求,這個FilterChain包含着我們配置的與請求相匹配的Filter和Servlet,其doFilter方法會依次調用所有的Filter的doFilter方法和Servlet的service方法,這樣請求就得到了處理!
  • 當所有的Pipeline-Valve都執行完之后,並且處理完了具體的請求,這個時候就可以將返回的結果交給Connector了,Connector在通過Socket的方式將結果返回給客戶端。

參考文獻:

https://www.cnblogs.com/leeego-123/p/12159574.html

https://www.jianshu.com/p/1dec08d290c1

https://blog.csdn.net/weixin_40006977/article/details/112711947


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM