1. 程式人生 > >Java面試題全集(下)

Java面試題全集(下)

  這部分主要是開源Java EE框架方面的內容,包括Hibernate、MyBatis、Spring、Spring MVC等,由於Struts 2已經是明日黃花,在這裡就不討論Struts 2的面試題,如果需要了解相關內容,可以參考我的另一篇文章《Java面試題集(86-115)》。此外,這篇文章還對企業應用架構、大型網站架構和應用伺服器優化等內容進行了簡單的探討,這些內容相信對面試會很有幫助。

126、什麼是ORM?
答:物件關係對映(Object-Relational Mapping,簡稱ORM)是一種為了解決程式的面向物件模型與資料庫的關係模型互不匹配問題的技術;簡單的說,ORM是通過使用描述物件和資料庫之間對映的元資料(在Java中可以用XML或者是註解),將程式中的物件自動持久化到關係資料庫中或者將關係資料庫表中的行轉換成Java物件,其本質上就是將資料從一種形式轉換到另外一種形式。

127、持久層設計要考慮的問題有哪些?你用過的持久層框架有哪些?
答:所謂"持久"就是將資料儲存到可掉電式儲存裝置中以便今後使用,簡單的說,就是將記憶體中的資料儲存到關係型資料庫、檔案系統、訊息佇列等提供持久化支援的裝置中。持久層就是系統中專注於實現資料持久化的相對獨立的層面。

持久層設計的目標包括:
- 資料儲存邏輯的分離,提供抽象化的資料訪問介面。
- 資料訪問底層實現的分離,可以在不修改程式碼的情況下切換底層實現。
- 資源管理和排程的分離,在資料訪問層實現統一的資源排程(如快取機制)。
- 資料抽象,提供更面向物件的資料操作。

128、Hibernate中SessionFactory是執行緒安全的嗎?Session是執行緒安全的嗎(兩個執行緒能夠共享同一個Session嗎)?


答:SessionFactory對應Hibernate的一個數據儲存的概念,它是執行緒安全的,可以被多個執行緒併發訪問。SessionFactory一般只會在啟動的時候構建。對於應用程式,最好將SessionFactory通過單例模式進行封裝以便於訪問。Session是一個輕量級非執行緒安全的物件(執行緒間不能共享session),它表示與資料庫進行互動的一個工作單元。Session是由SessionFactory建立的,在任務完成之後它會被關閉。Session是持久層服務對外提供的主要介面。Session會延遲獲取資料庫連線(也就是在需要的時候才會獲取)。為了避免建立太多的session,可以使用ThreadLocal將session和當前執行緒繫結在一起,這樣可以讓同一個執行緒獲得的總是同一個session。Hibernate 3中SessionFactory的getCurrentSession()方法就可以做到。

129、Hibernate中Session的load和get方法的區別是什麼?
答:主要有以下三項區別:
① 如果沒有找到符合條件的記錄,get方法返回null,load方法丟擲異常。
② get方法直接返回實體類物件,load方法返回實體類物件的代理。
③ 在Hibernate 3之前,get方法只在一級快取中進行資料查詢,如果沒有找到對應的資料則越過二級快取,直接發出SQL語句完成資料讀取;load方法則可以從二級快取中獲取資料;從Hibernate 3開始,get方法不再是對二級快取只寫不讀,它也是可以訪問二級快取的。

說明:對於load()方法Hibernate認為該資料在資料庫中一定存在可以放心的使用代理來實現延遲載入,如果沒有資料就丟擲異常,而通過get()方法獲取的資料可以不存在。

130、Session的save()、update()、merge()、lock()、saveOrUpdate()和persist()方法分別是做什麼的?有什麼區別?
答:Hibernate的物件有三種狀態:瞬時態(transient)、持久態(persistent)和遊離態(detached),如第135題中的圖所示。瞬時態的例項可以通過呼叫save()、persist()或者saveOrUpdate()方法變成持久態;遊離態的例項可以通過呼叫 update()、saveOrUpdate()、lock()或者replicate()變成持久態。save()和persist()將會引發SQL的INSERT語句,而update()或merge()會引發UPDATE語句。save()和update()的區別在於一個是將瞬時態物件變成持久態,一個是將遊離態物件變為持久態。merge()方法可以完成save()和update()方法的功能,它的意圖是將新的狀態合併到已有的持久化物件上或建立新的持久化物件。對於persist()方法,按照官方文件的說明:① persist()方法把一個瞬時態的例項持久化,但是並不保證識別符號被立刻填入到持久化例項中,識別符號的填入可能被推遲到flush的時間;② persist()方法保證當它在一個事務外部被呼叫的時候並不觸發一個INSERT語句,當需要封裝一個長會話流程的時候,persist()方法是很有必要的;③ save()方法不保證第②條,它要返回識別符號,所以它會立即執行INSERT語句,不管是在事務內部還是外部。至於lock()方法和update()方法的區別,update()方法是把一個已經更改過的脫管狀態的物件變成持久狀態;lock()方法是把一個沒有更改過的脫管狀態的物件變成持久狀態。

131、闡述Session載入實體物件的過程。
答:Session載入實體物件的步驟是:
① Session在呼叫資料庫查詢功能之前,首先會在一級快取中通過實體型別和主鍵進行查詢,如果一級快取查詢命中且資料狀態合法,則直接返回;
② 如果一級快取沒有命中,接下來Session會在當前NonExists記錄(相當於一個查詢黑名單,如果出現重複的無效查詢可以迅速做出判斷,從而提升效能)中進行查詢,如果NonExists中存在同樣的查詢條件,則返回null;
③ 如果一級快取查詢失敗則查詢二級快取,如果二級快取命中則直接返回;
④ 如果之前的查詢都未命中,則發出SQL語句,如果查詢未發現對應記錄則將此次查詢新增到Session的NonExists中加以記錄,並返回null;
⑤ 根據對映配置和SQL語句得到ResultSet,並建立對應的實體物件;
⑥ 將物件納入Session(一級快取)的管理;
⑦ 如果有對應的攔截器,則執行攔截器的onLoad方法;
⑧ 如果開啟並設定了要使用二級快取,則將資料物件納入二級快取;
⑨ 返回資料物件。

132、Query介面的list方法和iterate方法有什麼區別?
答:
① list()方法無法利用一級快取和二級快取(對快取只寫不讀),它只能在開啟查詢快取的前提下使用查詢快取;iterate()方法可以充分利用快取,如果目標資料只讀或者讀取頻繁,使用iterate()方法可以減少效能開銷。
② list()方法不會引起N+1查詢問題,而iterate()方法可能引起N+1查詢問題

說明:關於N+1查詢問題,可以參考CSDN上的一篇文章《什麼是N+1查詢》

133、Hibernate如何實現分頁查詢?
答:通過Hibernate實現分頁查詢,開發人員只需要提供HQL語句(呼叫Session的createQuery()方法)或查詢條件(呼叫Session的createCriteria()方法)、設定查詢起始行數(呼叫Query或Criteria介面的setFirstResult()方法)和最大查詢行數(呼叫Query或Criteria介面的setMaxResults()方法),並呼叫Query或Criteria介面的list()方法,Hibernate會自動生成分頁查詢的SQL語句。

134、鎖機制有什麼用?簡述Hibernate的悲觀鎖和樂觀鎖機制。
答:有些業務邏輯在執行過程中要求對資料進行排他性的訪問,於是需要通過一些機制保證在此過程中資料被鎖住不會被外界修改,這就是所謂的鎖機制。
Hibernate支援悲觀鎖和樂觀鎖兩種鎖機制。悲觀鎖,顧名思義悲觀的認為在資料處理過程中極有可能存在修改資料的併發事務(包括本系統的其他事務或來自外部系統的事務),於是將處理的資料設定為鎖定狀態。悲觀鎖必須依賴資料庫本身的鎖機制才能真正保證資料訪問的排他性,關於資料庫的鎖機制和事務隔離級別在《Java面試題大全(上)》中已經討論過了。樂觀鎖,顧名思義,對併發事務持樂觀態度(認為對資料的併發操作不會經常性的發生),通過更加寬鬆的鎖機制來解決由於悲觀鎖排他性的資料訪問對系統性能造成的嚴重影響。最常見的樂觀鎖是通過資料版本標識來實現的,讀取資料時獲得資料的版本號,更新資料時將此版本號加1,然後和資料庫表對應記錄的當前版本號進行比較,如果提交的資料版本號大於資料庫中此記錄的當前版本號則更新資料,否則認為是過期資料無法更新。Hibernate中通過Session的get()和load()方法從資料庫中載入物件時可以通過引數指定使用悲觀鎖;而樂觀鎖可以通過給實體類加整型的版本欄位再通過XML或@Version註解進行配置。

提示:使用樂觀鎖會增加了一個版本欄位,很明顯這需要額外的空間來儲存這個版本欄位,浪費了空間,但是樂觀鎖會讓系統具有更好的併發性,這是對時間的節省。因此樂觀鎖也是典型的空間換時間的策略。

135、闡述實體物件的三種狀態以及轉換關係。
答:最新的Hibernate文件中為Hibernate物件定義了四種狀態(原來是三種狀態,面試的時候基本上問的也是三種狀態),分別是:瞬時態(new, or transient)、持久態(managed, or persistent)、遊狀態(detached)和移除態(removed,以前Hibernate文件中定義的三種狀態中沒有移除態),如下圖所示,就以前的Hibernate文件中移除態被視為是瞬時態。

這裡寫圖片描述

  • 瞬時態:當new一個實體物件後,這個物件處於瞬時態,即這個物件只是一個儲存臨時資料的記憶體區域,如果沒有變數引用這個物件,則會被JVM的垃圾回收機制回收。這個物件所儲存的資料與資料庫沒有任何關係,除非通過Session的save()、saveOrUpdate()、persist()、merge()方法把瞬時態物件與資料庫關聯,並把資料插入或者更新到資料庫,這個物件才轉換為持久態物件。
  • 持久態:持久態物件的例項在資料庫中有對應的記錄,並擁有一個持久化標識(ID)。對持久態物件進行delete操作後,資料庫中對應的記錄將被刪除,那麼持久態物件與資料庫記錄不再存在對應關係,持久態物件變成移除態(可以視為瞬時態)。持久態物件被修改變更後,不會馬上同步到資料庫,直到資料庫事務提交。
  • 遊離態:當Session進行了close()、clear()、evict()或flush()後,實體物件從持久態變成遊離態,物件雖然擁有持久和與資料庫對應記錄一致的標識值,但是因為物件已經從會話中清除掉,物件不在持久化管理之內,所以處於遊離態(也叫脫管態)。遊離態的物件與臨時狀態物件是十分相似的,只是它還含有持久化標識。

提示:關於這個問題,在Hibernate的官方文件中有更為詳細的解讀。

136、如何理解Hibernate的延遲載入機制?在實際應用中,延遲載入與Session關閉的矛盾是如何處理的?
答:延遲載入就是並不是在讀取的時候就把資料載入進來,而是等到使用時再載入。Hibernate使用了虛擬代理機制實現延遲載入,我們使用Session的load()方法載入資料或者一對多關聯對映在使用延遲載入的情況下從一的一方載入多的一方,得到的都是虛擬代理,簡單的說返回給使用者的並不是實體本身,而是實體物件的代理。代理物件在使用者呼叫getter方法時才會去資料庫載入資料。但載入資料就需要資料庫連線。而當我們把會話關閉時,資料庫連線就同時關閉了。

延遲載入與session關閉的矛盾一般可以這樣處理:
① 關閉延遲載入特性。這種方式操作起來比較簡單,因為Hibernate的延遲載入特性是可以通過對映檔案或者註解進行配置的,但這種解決方案存在明顯的缺陷。首先,出現"no session or session was closed"通常說明系統中已經存在主外來鍵關聯,如果去掉延遲載入的話,每次查詢的開銷都會變得很大。
② 在session關閉之前先獲取需要查詢的資料,可以使用工具方法Hibernate.isInitialized()判斷物件是否被載入,如果沒有被載入則可以使用Hibernate.initialize()方法載入物件。
③ 使用攔截器或過濾器延長Session的生命週期直到檢視獲得資料。Spring整合Hibernate提供的OpenSessionInViewFilter和OpenSessionInViewInterceptor就是這種做法。

137、舉一個多對多關聯的例子,並說明如何實現多對多關聯對映。
答:例如:商品和訂單、學生和課程都是典型的多對多關係。可以在實體類上通過@ManyToMany註解配置多對多關聯或者通過對映檔案中的和標籤配置多對多關聯,但是實際專案開發中,很多時候都是將多對多關聯對映轉換成兩個多對一關聯對映來實現的。

138、談一下你對繼承對映的理解。
答:繼承關係的對映策略有三種:
① 每個繼承結構一張表(table per class hierarchy),不管多少個子類都用一張表。
② 每個子類一張表(table per subclass),公共資訊放一張表,特有資訊放單獨的表。
③ 每個具體類一張表(table per concrete class),有多少個子類就有多少張表。
第一種方式屬於單表策略,其優點在於查詢子類物件的時候無需表連線,查詢速度快,適合多型查詢;缺點是可能導致表很大。後兩種方式屬於多表策略,其優點在於資料儲存緊湊,其缺點是需要進行連線查詢,不適合多型查詢。

139、簡述Hibernate常見優化策略。
答:這個問題應當挑自己使用過的優化策略回答,常用的有:
① 制定合理的快取策略(二級快取、查詢快取)。
② 採用合理的Session管理機制。
③ 儘量使用延遲載入特性。
④ 設定合理的批處理引數。
⑤ 如果可以,選用UUID作為主鍵生成器。
⑥ 如果可以,選用基於版本號的樂觀鎖替代悲觀鎖。
⑦ 在開發過程中, 開啟hibernate.show_sql選項檢視生成的SQL,從而瞭解底層的狀況;開發完成後關閉此選項。
⑧ 考慮資料庫本身的優化,合理的索引、恰當的資料分割槽策略等都會對持久層的效能帶來可觀的提升,但這些需要專業的DBA(資料庫管理員)提供支援。

140、談一談Hibernate的一級快取、二級快取和查詢快取。
答:Hibernate的Session提供了一級快取的功能,預設總是有效的,當應用程式儲存持久化實體、修改持久化實體時,Session並不會立即把這種改變提交到資料庫,而是快取在當前的Session中,除非顯示呼叫了Session的flush()方法或通過close()方法關閉Session。通過一級快取,可以減少程式與資料庫的互動,從而提高資料庫訪問效能。
SessionFactory級別的二級快取是全域性性的,所有的Session可以共享這個二級快取。不過二級快取預設是關閉的,需要顯示開啟並指定需要使用哪種二級快取實現類(可以使用第三方提供的實現)。一旦開啟了二級快取並設定了需要使用二級快取的實體類,SessionFactory就會快取訪問過的該實體類的每個物件,除非快取的資料超出了指定的快取空間。
一級快取和二級快取都是對整個實體進行快取,不會快取普通屬性,如果希望對普通屬性進行快取,可以使用查詢快取。查詢快取是將HQL或SQL語句以及它們的查詢結果作為鍵值對進行快取,對於同樣的查詢可以直接從快取中獲取資料。查詢快取預設也是關閉的,需要顯示開啟。

141、Hibernate中DetachedCriteria類是做什麼的?
答:DetachedCriteria和Criteria的用法基本上是一致的,但Criteria是由Session的createCriteria()方法建立的,也就意味著離開建立它的Session,Criteria就無法使用了。DetachedCriteria不需要Session就可以建立(使用DetachedCriteria.forClass()方法建立),所以通常也稱其為離線的Criteria,在需要進行查詢操作的時候再和Session繫結(呼叫其getExecutableCriteria(Session)方法),這也就意味著一個DetachedCriteria可以在需要的時候和不同的Session進行繫結。

142、@OneToMany註解的mappedBy屬性有什麼作用?
答:@OneToMany用來配置一對多關聯對映,但通常情況下,一對多關聯對映都由多的一方來維護關聯關係,例如學生和班級,應該在學生類中新增班級屬性來維持學生和班級的關聯關係(在資料庫中是由學生表中的外來鍵班級編號來維護學生表和班級表的多對一關係),如果要使用雙向關聯,在班級類中新增一個容器屬性來存放學生,並使用@OneToMany註解進行對映,此時mappedBy屬性就非常重要。如果使用XML進行配置,可以用<set>標籤的inverse="true"設定來達到同樣的效果。

143、MyBatis中使用#$書寫佔位符有什麼區別?
答:#將傳入的資料都當成一個字串,會對傳入的資料自動加上引號;$將傳入的資料直接顯示生成在SQL中。注意:使用$佔位符可能會導致SQL注射攻擊,能用#的地方就不要使用$,寫order by子句的時候應該用$而不是#

144、解釋一下MyBatis中名稱空間(namespace)的作用。
答:在大型專案中,可能存在大量的SQL語句,這時候為每個SQL語句起一個唯一的標識(ID)就變得並不容易了。為了解決這個問題,在MyBatis中,可以為每個對映檔案起一個唯一的名稱空間,這樣定義在這個對映檔案中的每個SQL語句就成了定義在這個名稱空間中的一個ID。只要我們能夠保證每個名稱空間中這個ID是唯一的,即使在不同對映檔案中的語句ID相同,也不會再產生衝突了。

145、MyBatis中的動態SQL是什麼意思?
答:對於一些複雜的查詢,我們可能會指定多個查詢條件,但是這些條件可能存在也可能不存在,例如在58同城上面找房子,我們可能會指定面積、樓層和所在位置來查詢房源,也可能會指定面積、價格、戶型和所在位置來查詢房源,此時就需要根據使用者指定的條件動態生成SQL語句。如果不使用持久層框架我們可能需要自己拼裝SQL語句,還好MyBatis提供了動態SQL的功能來解決這個問題。MyBatis中用於實現動態SQL的元素主要有:
- if
- choose / when / otherwise
- trim
- where
- set
- foreach

下面是對映檔案的片段。

    <select id="foo" parameterType="Blog" resultType="Blog">
        select * from t_blog where 1 = 1
        <if test="title != null">
            and title = #{title}
        </if>
        <if test="content != null">
            and content = #{content}
        </if>
        <if test="owner != null">
            and owner = #{owner}
        </if>
   </select>

當然也可以像下面這些書寫。

    <select id="foo" parameterType="Blog" resultType="Blog">
        select * from t_blog where 1 = 1 
        <choose>
            <when test="title != null">
                and title = #{title}
            </when>
            <when test="content != null">
                and content = #{content}
            </when>
            <otherwise>
                and owner = "owner1"
            </otherwise>
        </choose>
    </select>

再看看下面這個例子。

    <select id="bar" resultType="Blog">
        select * from t_blog where id in
        <foreach collection="array" index="index" 
            item="item" open="(" separator="," close=")">
            #{item}
        </foreach>
    </select>

146、什麼是IoC和DI?DI是如何實現的?
答:IoC叫控制反轉,是Inversion of Control的縮寫,DI(Dependency Injection)叫依賴注入,是對IoC更簡單的詮釋。控制反轉是把傳統上由程式程式碼直接操控的物件的呼叫權交給容器,通過容器來實現物件元件的裝配和管理。所謂的"控制反轉"就是對元件物件控制權的轉移,從程式程式碼本身轉移到了外部容器,由容器來建立物件並管理物件之間的依賴關係。IoC體現了好萊塢原則 - "Don’t call me, we will call you"。依賴注入的基本原則是應用元件不應該負責查詢資源或者其他依賴的協作物件。配置物件的工作應該由容器負責,查詢資源的邏輯應該從應用元件的程式碼中抽取出來,交給容器來完成。DI是對IoC更準確的描述,即元件之間的依賴關係由容器在執行期決定,形象的來說,即由容器動態的將某種依賴關係注入到元件之中。

舉個例子:一個類A需要用到介面B中的方法,那麼就需要為類A和介面B建立關聯或依賴關係,最原始的方法是在類A中建立一個介面B的實現類C的例項,但這種方法需要開發人員自行維護二者的依賴關係,也就是說當依賴關係發生變動的時候需要修改程式碼並重新構建整個系統。如果通過一個容器來管理這些物件以及物件的依賴關係,則只需要在類A中定義好用於關聯介面B的方法(構造器或setter方法),將類A和介面B的實現類C放入容器中,通過對容器的配置來實現二者的關聯。

依賴注入可以通過setter方法注入(設值注入)、構造器注入和介面注入三種方式來實現,Spring支援setter注入和構造器注入,通常使用構造器注入來注入必須的依賴關係,對於可選的依賴關係,則setter注入是更好的選擇,setter注入需要類提供無參構造器或者無參的靜態工廠方法來建立物件。

147、Spring中Bean的作用域有哪些?
答:在Spring的早期版本中,僅有兩個作用域:singleton和prototype,前者表示Bean以單例的方式存在;後者表示每次從容器中呼叫Bean時,都會返回一個新的例項,prototype通常翻譯為原型。

補充:設計模式中的建立型模式中也有一個原型模式,原型模式也是一個常用的模式,例如做一個室內設計軟體,所有的素材都在工具箱中,而每次從工具箱中取出的都是素材物件的一個原型,可以通過物件克隆來實現原型模式。

Spring 2.x中針對WebApplicationContext新增了3個作用域,分別是:request(每次HTTP請求都會建立一個新的Bean)、session(同一個HttpSession共享同一個Bean,不同的HttpSession使用不同的Bean)和globalSession(同一個全域性Session共享一個Bean)。

說明:單例模式和原型模式都是重要的設計模式。一般情況下,無狀態或狀態不可變的類適合使用單例模式。在傳統開發中,由於DAO持有Connection這個非執行緒安全物件因而沒有使用單例模式;但在Spring環境下,所有DAO類對可以採用單例模式,因為Spring利用AOP和Java API中的ThreadLocal對非執行緒安全的物件進行了特殊處理。

ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。ThreadLocal,顧名思義是執行緒的一個本地化物件,當工作於多執行緒中的物件使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒分配一個獨立的變數副本,所以每一個執行緒都可以獨立的改變自己的副本,而不影響其他執行緒所對應的副本。從執行緒的角度看,這個變數就像是執行緒的本地變數。

ThreadLocal類非常簡單好用,只有四個方法,能用上的也就是下面三個方法:
- void set(T value):設定當前執行緒的執行緒區域性變數的值。
- T get():獲得當前執行緒所對應的執行緒區域性變數的值。
- void remove():刪除當前執行緒中執行緒區域性變數的值。

ThreadLocal是如何做到為每一個執行緒維護一份獨立的變數副本的呢?在ThreadLocal類中有一個Map,鍵為執行緒物件,值是其執行緒對應的變數的副本,自己要模擬實現一個ThreadLocal類其實並不困難,程式碼如下所示:

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class MyThreadLocal<T> {
    private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<Thread, T>());

    public void set(T newValue) {
        map.put(Thread.currentThread(), newValue);
    }

    public T get() {
        return map.get(Thread.currentThread());
    }

    public void remove() {
        map.remove(Thread.currentThread());
    }
}

148、解釋一下什麼叫AOP(面向切面程式設計)?
答:AOP(Aspect-Oriented Programming)指一種程式設計範型,該範型以一種稱為切面(aspect)的語言構造為基礎,切面是一種新的模組化機制,用來描述分散在物件、類或方法中的橫切關注點(crosscutting concern)。

149、你是如何理解"橫切關注"這個概念的?
答:"橫切關注"是會影響到整個應用程式的關注功能,它跟正常的業務邏輯是正交的,沒有必然的聯絡,但是幾乎所有的業務邏輯都會涉及到這些關注功能。通常,事務、日誌、安全性等關注就是應用中的橫切關注功能。

150、你如何理解AOP中的連線點(Joinpoint)、切點(Pointcut)、增強(Advice)、引介(Introduction)、織入(Weaving)、切面(Aspect)這些概念?
答:
a. 連線點(Joinpoint):程式執行的某個特定位置(如:某個方法呼叫前、呼叫後,方法丟擲異常後)。一個類或一段程式程式碼擁有一些具有邊界性質的特定點,這些程式碼中的特定點就是連線點。Spring僅支援方法的連線點。
b. 切點(Pointcut):如果連線點相當於資料中的記錄,那麼切點相當於查詢條件,一個切點可以匹配多個連線點。Spring AOP的規則解析引擎負責解析切點所設定的查詢條件,找到對應的連線點。
c. 增強(Advice):增強是織入到目標類連線點上的一段程式程式碼。Spring提供的增強介面都是帶方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多資料上將增強譯為“通知”,這明顯是個詞不達意的翻譯,讓很多程式設計師困惑了許久。

說明: Advice在國內的很多書面資料中都被翻譯成"通知",但是很顯然這個翻譯無法表達其本質,有少量的讀物上將這個詞翻譯為"增強",這個翻譯是對Advice較為準確的詮釋,我們通過AOP將橫切關注功能加到原有的業務邏輯上,這就是對原有業務邏輯的一種增強,這種增強可以是前置增強、後置增強、返回後增強、拋異常時增強和包圍型增強。

d. 引介(Introduction):引介是一種特殊的增強,它為類新增一些屬性和方法。這樣,即使一個業務類原本沒有實現某個介面,通過引介功能,可以動態的未該業務類新增介面的實現邏輯,讓業務類成為這個介面的實現類。
e. 織入(Weaving):織入是將增強新增到目標類具體連線點上的過程,AOP有三種織入方式:①編譯期織入:需要特殊的Java編譯期(例如AspectJ的ajc);②裝載期織入:要求使用特殊的類載入器,在裝載類的時候對類進行增強;③執行時織入:在執行時為目標類生成代理實現增強。Spring採用了動態代理的方式實現了執行時織入,而AspectJ採用了編譯期織入和裝載期織入的方式。
f. 切面(Aspect):切面是由切點和增強(引介)組成的,它包括了對橫切關注功能的定義,也包括了對連線點的定義。

補充:代理模式是GoF提出的23種設計模式中最為經典的模式之一,代理模式是物件的結構模式,它給某一個物件提供一個代理物件,並由代理物件控制對原物件的引用。簡單的說,代理物件可以完成比原物件更多的職責,當需要為原物件新增橫切關注功能時,就可以使用原物件的代理物件。我們在開啟Office系列的Word文件時,如果文件中有插圖,當文件剛載入時,文件中的插圖都只是一個虛框佔位符,等使用者真正翻到某頁要檢視該圖片時,才會真正載入這張圖,這其實就是對代理模式的使用,代替真正圖片的虛框就是一個虛擬代理;Hibernate的load方法也是返回一個虛擬代理物件,等使用者真正需要訪問物件的屬性時,才向資料庫發出SQL語句獲得真實物件。

下面用一個找槍手代考的例子演示代理模式的使用:

/**
 * 參考人員介面
 * @author 駱昊
 *
 */
public interface Candidate {

    /**
     * 答題
     */
    public void answerTheQuestions();
}
/**
 * 懶學生
 * @author 駱昊
 *
 */
public class LazyStudent implements Candidate {
    private String name;        // 姓名

    public LazyStudent(String name) {
        this.name = name;
    }

    @Override
    public void answerTheQuestions() {
        // 懶學生只能寫出自己的名字不會答題
        System.out.println("姓名: " + name);
    }

}
/**
 * 槍手
 * @author 駱昊
 *
 */
public class Gunman implements Candidate {
    private Candidate target;   // 被代理物件

    public Gunman(Candidate target) {
        this.target = target;
    }

    @Override
    public void answerTheQuestions() {
        // 槍手要寫上代考的學生的姓名
        target.answerTheQuestions();
        // 槍手要幫助懶學生答題並交卷
        System.out.println("奮筆疾書正確答案");
        System.out.println("交卷");
    }

}
public class ProxyTest1 {

    public static void main(String[] args) {
        Candidate c = new Gunman(new LazyStudent("王小二"));
        c.answerTheQuestions();
    }
}

說明:從JDK 1.3開始,Java提供了動態代理技術,允許開發者在執行時建立介面的代理例項,主要包括Proxy類和InvocationHandler介面。下面的例子使用動態代理為ArrayList編寫一個代理,在新增和刪除元素時,在控制檯列印新增或刪除的元素以及ArrayList的大小:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;

public class ListProxy<T> implements InvocationHandler {
    private List<T> target;

    public ListProxy(List<T> target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Object retVal = null;
        System.out.println("[" + method.getName() + ": " + args[0] + "]");
        retVal = method.invoke(target, args);
        System.out.println("[size=" + target.size() + "]");
        return retVal;
    }

}
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;

public class ProxyTest2 {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        Class<?> clazz = list.getClass();
        ListProxy<String> myProxy = new ListProxy<String>(list);
        List<String> newList = (List<String>) 
                Proxy.newProxyInstance(clazz.getClassLoader(), 
                clazz.getInterfaces(), myProxy);
        newList.add("apple");
        newList.add("banana");
        newList.add("orange");
        newList.remove("banana");
    }
}

說明:使用Java的動態代理有一個侷限性就是代理的類必須要實現介面,雖然面向介面程式設計是每個優秀的Java程式都知道的規則,但現實往往不盡如人意,對於沒有實現介面的類如何為其生成代理呢?繼承!繼承是最經典的擴充套件已有程式碼能力的手段,雖然繼承常常被初學者濫用,但繼承也常常被進階的程式設計師忽視。CGLib採用非常底層的位元組碼生成技術,通過為一個類建立子類來生成代理,它彌補了Java動態代理的不足,因此Spring中動態代理和CGLib都是建立代理的重要手段,對於實現了介面的類就用動態代理為其生成代理類,而沒有實現介面的類就用CGLib通過繼承的方式為其建立代理。

151、Spring中自動裝配的方式有哪些?
答:
- no:不進行自動裝配,手動設定Bean的依賴關係。
- byName:根據Bean的名字進行自動裝配。
- byType:根據Bean的型別進行自動裝配。
- constructor:類似於byType,不過是應用於構造器的引數,如果正好有一個Bean與構造器的引數型別相同則可以自動裝配,否則會導致錯誤。
- autodetect:如果有預設的構造器,則通過constructor的方式進行自動裝配,否則使用byType的方式進行自動裝配。

說明:自動裝配沒有自定義裝配方式那麼精確,而且不能自動裝配簡單屬性(基本型別、字串等),在使用時應注意。

152、Spring中如何使用註解來配置Bean?有哪些相關的註解?
答:首先需要在Spring配置檔案中增加如下配置:

<context:component-scan base-package="org.example"/>

然後可以用@Component、@Controller、@Service、@Repository註解來標註需要由Spring IoC容器進行物件託管的類。這幾個註解沒有本質區別,只不過@Controller通常用於控制器,@Service通常用於業務邏輯類,@Repository通常用於倉儲類(例如我們的DAO實現類),普通的類用@Component來標註。

153、Spring支援的事務管理型別有哪些?你在專案中使用哪種方式?
答:Spring支援程式設計式事務管理和宣告式事務管理。許多Spring框架的使用者選擇宣告式事務管理,因為這種方式和應用程式的關聯較少,因此更加符合輕量級容器的概念。宣告式事務管理要優於程式設計式事務管理,儘管在靈活性方面它弱於程式設計式事務管理,因為程式設計式事務允許你通過程式碼控制業務。

事務分為全域性事務和區域性事務。全域性事務由應用伺服器管理,需要底層伺服器JTA支援(如WebLogic、WildFly等)。區域性事務和底層採用的持久化方案有關,例如使用JDBC進行持久化時,需要使用Connetion物件來操作事務;而採用Hibernate進行持久化時,需要使用Session物件來操作事務。

Spring提供瞭如下所示的事務管理器。

事務管理器實現類 目標物件
DataSourceTransactionManager 注入DataSource
HibernateTransactionManager 注入SessionFactory
JdoTransactionManager 管理JDO事務
JtaTransactionManager 使用JTA管理事務
PersistenceBrokerTransactionManager 管理Apache的OJB事務

這些事務的父介面都是PlatformTransactionManager。Spring的事務管理機制是一種典型的策略模式,PlatformTransactionManager代表事務管理介面,該介面定義了三個方法,該介面並不知道底層如何管理事務,但是它的實現類必須提供getTransaction()方法(開啟事務)、commit()方法(提交事務)、rollback()方法(回滾事務)的多型實現,這樣就可以用不同的實現類代表不同的事務管理策略。使用JTA全域性事務策略時,需要底層應用伺服器支援,而不同的應用伺服器所提供的JTA全域性事務可能存在細節上的差異,因此實際配置全域性事務管理器是可能需要使用JtaTransactionManager的子類,如:WebLogicJtaTransactionManager(Oracle的WebLogic伺服器提供)、UowJtaTransactionManager(IBM的WebSphere伺服器提供)等。

程式設計式事務管理如下所示。

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:p="http://www.springframework.org/schema/p"
    xmlns:p="http://www.springframework.org/schema/context"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

     <context:component-scan base-package="com.jackfrued"/>

     <bean id="propertyConfig"
         class="org.springframework.beans.factory.config.
  PropertyPlaceholderConfigurer">
         <property name="location">
             <value>jdbc.properties</value>
         </property>
     </bean>

     <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
         <property name="driverClassName">
             <value>${db.driver}</value>
         </property>
         <property name="url">
             <value>${db.url}</value>
         </property>
         <property name="username">
             <value>${db.username}</value>
         </property>
         <property name="password">
             <value>${db.password}</value>
         </property>
     </bean>

     <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
         <property name="dataSource">
             <ref bean="dataSource" />
         </property>
     </bean>

     <!-- JDBC事務管理器 -->
     <bean id="transactionManager"
         class="org.springframework.jdbc.datasource.
       DataSourceTransactionManager" scope="singleton">
         <property name="dataSource">
             <ref bean="dataSource" />
         </property>
     </bean>

     <!-- 宣告事務模板 -->
     <bean id="transactionTemplate"
         class="org.springframework.transaction.support.
   TransactionTemplate">
         <property name="transactionManager">
             <ref bean="transactionManager" />
         </property>
     </bean>

</beans>
package com.jackfrued.dao.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

import com.jackfrued.dao.EmpDao;
import com.jackfrued.entity.Emp;

@Repository
public class EmpDaoImpl implements EmpDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public boolean save(Emp emp) {
        String sql = "insert into emp values (?,?,?)";
        return jdbcTemplate.update(sql, emp.getId(), emp.getName(), emp.getBirthday()) == 1;
    }

}
package com.jackfrued.biz.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.jackfrued.biz.EmpService;
import com.jackfrued.dao.EmpDao;
import com.jackfrued.entity.Emp;

@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private TransactionTemplate txTemplate;
    @Autowired
    private EmpDao empDao;

    @Override
    public void addEmp(final Emp emp) {
        txTemplate.execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
                empDao.save(emp);
            }
        });
    }


}

宣告式事務如下圖所示,以Spring整合Hibernate 3為例,包括完整的DAO和業務邏輯程式碼。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" 
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-3.2.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

    <!-- 配置由Spring IoC容器託管的物件對應的被註解的類所在的包 -->
    <context:component-scan base-package="com.jackfrued" />

    <!-- 配置通過自動生成代理實現AOP功能 -->
    <aop:aspectj-autoproxy />

    <!-- 配置資料庫連線池 (DBCP) -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <!-- 配置驅動程式類 -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <!-- 配置連線資料庫的URL -->
        <property name="url" value="jdbc:mysql://localhost:3306/myweb" />
        <!-- 配置訪問資料庫的使用者名稱 -->
        <property name="username" value="root" />
        <!-- 配置訪問資料庫的口令 -->
        <property name="password" value="123456" />
        <!-- 配置最大連線數 -->
        <property name="maxActive" value="150" />
        <!-- 配置最小空閒連線數 -->
        <property name="minIdle" value="5" />
        <!-- 配置最大空閒連線數 -->
        <property name="maxIdle" value="20" />
        <!-- 配置初始連線數 -->
        <property name="initialSize" value="10" />
        <!-- 配置連線被洩露時是否生成日誌 -->
        <property name="logAbandoned" value="true" />
        <!-- 配置是否刪除超時連線 -->
        <property name="removeAbandoned" value="true" />
        <!-- 配置刪除超時連線的超時門限值(以秒為單位) -->
        <property name="removeAbandonedTimeout" value="120" />
        <!-- 配置超時等待時間(以毫秒為單位) -->
        <property name="maxWait" value="5000" />
        <!-- 配置空閒連接回收器執行緒執行的時間間隔(以毫秒為單位) -->
        <property name="timeBetweenEvictionRunsMillis" value="300000" />
        <!-- 配置連線空閒多長時間後(以毫秒為單位)被斷開連線 -->
        <property name="minEvictableIdleTimeMillis" value="60000" />
    </bean>