Java面試題全集(下)

youthfulness88發表於2020-11-04

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

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

持久層框架有:
Hibernate
MyBatis
TopLink
Guzz
jOOQ
Spring Data
ActiveJDBC

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>

    <!-- 配置Spring提供的支援註解ORM對映的Hibernate會話工廠 -->
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <!-- 通過setter注入資料來源屬性 -->
        <property name="dataSource" ref="dataSource" />
        <!-- 配置實體類所在的包 -->
        <property name="packagesToScan" value="com.jackfrued.entity" />
        <!-- 配置Hibernate的相關屬性 -->
        <property name="hibernateProperties">
            <!-- 在專案除錯完成後要刪除show_sql和format_sql屬性否則對效能有顯著影響 -->
            <value>
                hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
            </value>
        </property>
    </bean>

    <!-- 配置Spring提供的Hibernate事務管理器 -->
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <!-- 通過setter注入Hibernate會話工廠 -->
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

    <!-- 配置基於註解配置宣告式事務 -->
    <tx:annotation-driven />

</beans>
package com.jackfrued.dao;

import java.io.Serializable;
import java.util.List;

import com.jackfrued.comm.QueryBean;
import com.jackfrued.comm.QueryResult;

/**
 * 資料訪問物件介面(以物件為單位封裝CRUD操作)
 * @author 駱昊
 *
 * @param <E> 實體型別
 * @param <K> 實體標識欄位的型別
 */
public interface BaseDao <E, K extends Serializable> {

    /**
     * 新增
     * @param entity 業務實體物件
     * @return 增加成功返回實體物件的標識
     */
    public K save(E entity);

    /**
     * 刪除
     * @param entity 業務實體物件
     */
    public void delete(E entity);

    /**
     * 根據ID刪除
     * @param id 業務實體物件的標識
     * @return 刪除成功返回true否則返回false
     */
    public boolean deleteById(K id);

    /**
     * 修改
     * @param entity 業務實體物件
     * @return 修改成功返回true否則返回false
     */
    public void update(E entity);

    /**
     * 根據ID查詢業務實體物件
     * @param id 業務實體物件的標識
     * @return 業務實體物件物件或null
     */
    public E findById(K id);

    /**
     * 根據ID查詢業務實體物件
     * @param id 業務實體物件的標識
     * @param lazy 是否使用延遲載入
     * @return 業務實體物件物件
     */
    public E findById(K id, boolean lazy);

    /**
     * 查詢所有業務實體物件
     * @return 裝所有業務實體物件的列表容器
     */
    public List<E> findAll();

    /**
     * 分頁查詢業務實體物件
     * @param page 頁碼
     * @param size 頁面大小
     * @return 查詢結果物件
     */
    public QueryResult<E> findByPage(int page, int size);

    /**
     * 分頁查詢業務實體物件
     * @param queryBean 查詢條件物件
     * @param page 頁碼
     * @param size 頁面大小
     * @return 查詢結果物件
     */
    public QueryResult<E> findByPage(QueryBean queryBean, int page, int size);

}
package com.jackfrued.dao;

import java.io.Serializable;
import java.util.List;

import com.jackfrued.comm.QueryBean;
import com.jackfrued.comm.QueryResult;

/**
 * BaseDao的預設介面卡
 * @author 駱昊
 *
 * @param <E> 實體型別
 * @param <K> 實體標識欄位的型別
 */
public abstract class BaseDaoAdapter<E, K extends Serializable> implements
        BaseDao<E, K> {

    @Override
    public K save(E entity) {
        return null;
    }

    @Override
    public void delete(E entity) {
    }

    @Override
    public boolean deleteById(K id) {
        E entity = findById(id);
        if(entity != null) {
            delete(entity);
            return true;
        }
        return false;
    }

    @Override
    public void update(E entity) {
    }

    @Override
    public E findById(K id) {
        return null;
    }

    @Override
    public E findById(K id, boolean lazy) {
        return null;
    }

    @Override
    public List<E> findAll() {
        return null;
    }

    @Override
    public QueryResult<E> findByPage(int page, int size) {
        return null;
    }

    @Override
    public QueryResult<E> findByPage(QueryBean queryBean, int page, int size) {
        return null;
    }

}
package com.jackfrued.dao;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.jackfrued.comm.HQLQueryBean;
import com.jackfrued.comm.QueryBean;
import com.jackfrued.comm.QueryResult;

/**
 * 基於Hibernate的BaseDao實現類
 * @author 駱昊
 *
 * @param <E> 實體型別
 * @param <K> 主鍵型別
 */
@SuppressWarnings(value = {"unchecked"})
public abstract class BaseDaoHibernateImpl<E, K extends Serializable> extends BaseDaoAdapter<E, K> {
    @Autowired
    protected SessionFactory sessionFactory;

    private Class<?> entityClass;       // 業務實體的類物件
    private String entityName;          // 業務實體的名字

    public BaseDaoHibernateImpl() {
        ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
        entityClass = (Class<?>) pt.getActualTypeArguments()[0];
        entityName = entityClass.getSimpleName();
    }

    @Override
    public K save(E entity) {
        return (K) sessionFactory.getCurrentSession().save(entity);
    }

    @Override
    public void delete(E entity) {
        sessionFactory.getCurrentSession().delete(entity);
    }

    @Override
    public void update(E entity) {
        sessionFactory.getCurrentSession().update(entity);
    }

    @Override
    public E findById(K id) {
        return findById(id, false);
    }

    @Override
    public E findById(K id, boolean lazy) {
        Session session = sessionFactory.getCurrentSession();
        return (E) (lazy? session.load(entityClass, id) : session.get(entityClass, id));
    }

    @Override
    public List<E> findAll() {
        return sessionFactory.getCurrentSession().createCriteria(entityClass).list();
    }

    @Override
    public QueryResult<E> findByPage(int page, int size) {
        return new QueryResult<E>(
            findByHQLAndPage("from " + entityName , page, size),
            getCountByHQL("select count(*) from " + entityName)
        );
    }

    @Override
    public QueryResult<E> findByPage(QueryBean queryBean, int page, int size) {
        if(queryBean instanceof HQLQueryBean) {
            HQLQueryBean hqlQueryBean = (HQLQueryBean) queryBean;
            return new QueryResult<E>(
                findByHQLAndPage(hqlQueryBean.getQueryString(), page, size, hqlQueryBean.getParameters()),
                getCountByHQL(hqlQueryBean.getCountString(), hqlQueryBean.getParameters())
            );
        }
        return null;
    }

    /**
     * 根據HQL和可變引數列表進行查詢
     * @param hql 基於HQL的查詢語句
     * @param params 可變引數列表
     * @return 持有查詢結果的列表容器或空列表容器
     */
    protected List<E> findByHQL(String hql, Object... params) {
        return this.findByHQL(hql, getParamList(params));
    }

    /**
     * 根據HQL和引數列表進行查詢
     * @param hql 基於HQL的查詢語句
     * @param params 查詢引數列表
     * @return 持有查詢結果的列表容器或空列表容器
     */
    protected List<E> findByHQL(String hql, List<Object> params) {
        List<E> list = createQuery(hql, params).list();
        return list != null && list.size() > 0 ? list : Collections.EMPTY_LIST;
    }

    /**
     * 根據HQL和引數列表進行分頁查詢
     * @param hql 基於HQL的查詢語句
     * @page 頁碼
     * @size 頁面大小
     * @param params 可變引數列表
     * @return 持有查詢結果的列表容器或空列表容器
     */
    protected List<E> findByHQLAndPage(String hql, int page, int size, Object... params) {
        return this.findByHQLAndPage(hql, page, size, getParamList(params));
    }

    /**
     * 根據HQL和引數列表進行分頁查詢
     * @param hql 基於HQL的查詢語句
     * @page 頁碼
     * @size 頁面大小
     * @param params 查詢引數列表
     * @return 持有查詢結果的列表容器或空列表容器
     */
    protected List<E> findByHQLAndPage(String hql, int page, int size, List<Object> params) {
        List<E> list = createQuery(hql, params)
                .setFirstResult((page - 1) * size)
                .setMaxResults(size)
                .list();
        return list != null && list.size() > 0 ? list : Collections.EMPTY_LIST;
    }

    /**
     * 查詢滿足條件的記錄數
     * @param hql 基於HQL的查詢語句
     * @param params 可變引數列表
     * @return 滿足查詢條件的總記錄數
     */
    protected long getCountByHQL(String hql, Object... params) {
        return this.getCountByHQL(hql, getParamList(params));
    }

    /**
     * 查詢滿足條件的記錄數
     * @param hql 基於HQL的查詢語句
     * @param params 引數列表容器
     * @return 滿足查詢條件的總記錄數
     */
    protected long getCountByHQL(String hql, List<Object> params) {
        return (Long) createQuery(hql, params).uniqueResult();
    }

    // 建立Hibernate查詢物件(Query)
    private Query createQuery(String hql, List<Object> params) {
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        for(int i = 0; i < params.size(); i++) {
            query.setParameter(i, params.get(i));
        }
        return query;
    }

    // 將可變引數列表組裝成列表容器
    private List<Object> getParamList(Object... params) {
        List<Object> paramList = new ArrayList<>();
        if(params != null) {
            for(int i = 0; i < params.length; i++) {
                paramList.add(params[i]);
            }
        }
        return paramList.size() == 0? Collections.EMPTY_LIST : paramList;
    }

}
package com.jackfrued.comm;

import java.util.List;

/**
 * 查詢條件的介面
 * @author 駱昊
 *
 */
public interface QueryBean {

    /**
     * 新增排序欄位
     * @param fieldName 用於排序的欄位
     * @param asc 升序還是降序
     * @return 查詢條件物件自身(方便級聯程式設計)
     */
    public QueryBean addOrder(String fieldName, boolean asc);

    /**
     * 新增排序欄位
     * @param available 是否新增此排序欄位
     * @param fieldName 用於排序的欄位
     * @param asc 升序還是降序
     * @return 查詢條件物件自身(方便級聯程式設計)
     */
    public QueryBean addOrder(boolean available, String fieldName, boolean asc);

    /**
     * 新增查詢條件
     * @param condition 條件
     * @param params 替換掉條件中引數佔位符的引數
     * @return 查詢條件物件自身(方便級聯程式設計)
     */
    public QueryBean addCondition(String condition, Object... params);

    /**
     * 新增查詢條件
     * @param available 是否需要新增此條件
     * @param condition 條件
     * @param params 替換掉條件中引數佔位符的引數
     * @return 查詢條件物件自身(方便級聯程式設計)
     */
    public QueryBean addCondition(boolean available, String condition, Object... params);

    /**
     * 獲得查詢語句
     * @return 查詢語句
     */
    public String getQueryString();

    /**
     * 獲取查詢記錄數的查詢語句
     * @return 查詢記錄數的查詢語句
     */
    public String getCountString();

    /**
     * 獲得查詢引數
     * @return 查詢引數的列表容器
     */
    public List<Object> getParameters();
}
package com.jackfrued.comm;

import java.util.List;

/**
 * 查詢結果
 * @author 駱昊
 *
 * @param <T> 泛型引數
 */
public class QueryResult<T> {
    private List<T> result;     // 持有查詢結果的列表容器
    private long totalRecords;  // 查詢到的總記錄數

    /**
     * 構造器
     */
    public QueryResult() {
    }

    /**
     * 構造器
     * @param result 持有查詢結果的列表容器
     * @param totalRecords 查詢到的總記錄數
     */
    public QueryResult(List<T> result, long totalRecords) {
        this.result = result;
        this.totalRecords = totalRecords;
    }

    public List<T> getResult() {
        return result;
    }

    public void setResult(List<T> result) {
        this.result = result;
    }

    public long getTotalRecords() {
        return totalRecords;
    }

    public void setTotalRecords(long totalRecords) {
        this.totalRecords = totalRecords;
    }
}
package com.jackfrued.dao;

import com.jackfrued.comm.QueryResult;
import com.jackfrued.entity.Dept;

/**
 * 部門資料訪問物件介面
 * @author 駱昊
 *
 */
public interface DeptDao extends BaseDao<Dept, Integer> {

    /**
     * 分頁查詢頂級部門
     * @param page 頁碼
     * @param size 頁碼大小
     * @return 查詢結果物件
     */
    public QueryResult<Dept> findTopDeptByPage(int page, int size);

}
package com.jackfrued.dao.impl;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.jackfrued.comm.QueryResult;
import com.jackfrued.dao.BaseDaoHibernateImpl;
import com.jackfrued.dao.DeptDao;
import com.jackfrued.entity.Dept;

@Repository
public class DeptDaoImpl extends BaseDaoHibernateImpl<Dept, Integer> implements DeptDao {
    private static final String HQL_FIND_TOP_DEPT = " from Dept as d where d.superiorDept is null ";

    @Override
    public QueryResult<Dept> findTopDeptByPage(int page, int size) {
        List<Dept> list = findByHQLAndPage(HQL_FIND_TOP_DEPT, page, size);
        long totalRecords = getCountByHQL(" select count(*) " + HQL_FIND_TOP_DEPT);
        return new QueryResult<>(list, totalRecords);
    }

}
package com.jackfrued.comm;

import java.util.List;

/**
 * 分頁器
 * @author 駱昊
 *
 * @param <T> 分頁資料物件的型別
 */
public class PageBean<T> {
    private static final int DEFAUL_INIT_PAGE = 1;
    private static final int DEFAULT_PAGE_SIZE = 10;
    private static final int DEFAULT_PAGE_COUNT = 5;

    private List<T> data;           // 分頁資料
    private PageRange pageRange;    // 頁碼範圍
    private int totalPage;          // 總頁數
    private int size;               // 頁面大小
    private int currentPage;        // 當前頁碼
    private int pageCount;          // 頁碼數量

    /**
     * 構造器
     * @param currentPage 當前頁碼
     * @param size 頁碼大小
     * @param pageCount 頁碼數量
     */
    public PageBean(int currentPage, int size, int pageCount) {
        this.currentPage = currentPage > 0 ? currentPage : 1;
        this.size = size > 0 ? size : DEFAULT_PAGE_SIZE;
        this.pageCount = pageCount > 0 ? size : DEFAULT_PAGE_COUNT;
    }

    /**
     * 構造器
     * @param currentPage 當前頁碼
     * @param size 頁碼大小
     */
    public PageBean(int currentPage, int size) {
        this(currentPage, size, DEFAULT_PAGE_COUNT);
    }

    /**
     * 構造器
     * @param currentPage 當前頁碼
     */
    public PageBean(int currentPage) {
        this(currentPage, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_COUNT);
    }

    /**
     * 構造器
     */
    public PageBean() {
        this(DEFAUL_INIT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_COUNT);
    }

    public List<T> getData() {
        return data;
    }

    public int getStartPage() {
        return pageRange != null ? pageRange.getStartPage() : 1;
    }

    public int getEndPage() {
        return pageRange != null ? pageRange.getEndPage() : 1;
    }

    public long getTotalPage() {
        return totalPage;
    }

    public int getSize() {
        return size;
    }

    public int getCurrentPage() {
        return currentPage;
    }

    /**
     * 將查詢結果轉換為分頁資料
     * @param queryResult 查詢結果物件
     */
    public void transferQueryResult(QueryResult<T> queryResult) {
        long totalRecords = queryResult.getTotalRecords();

        data = queryResult.getResult();
        totalPage = (int) ((totalRecords + size - 1) / size); 
        totalPage = totalPage >= 0 ? totalPage : Integer.MAX_VALUE;
        this.pageRange = new PageRange(pageCount, currentPage, totalPage);
    }

}
package com.jackfrued.comm;

/**
 * 頁碼範圍
 * @author 駱昊
 *
 */
public class PageRange {
    private int startPage;  // 起始頁碼
    private int endPage;    // 終止頁碼

    /**
     * 構造器
     * @param pageCount 總共顯示幾個頁碼
     * @param currentPage 當前頁碼
     * @param totalPage 總頁數
     */
    public PageRange(int pageCount, int currentPage, int totalPage) {
        startPage = currentPage - (pageCount - 1) / 2;
        endPage = currentPage + pageCount / 2;
        if(startPage < 1) {
            startPage = 1;
            endPage = totalPage > pageCount ? pageCount : totalPage;
        }
        if (endPage > totalPage) {
            endPage = totalPage;
            startPage = (endPage - pageCount > 0) ? endPage - pageCount + 1 : 1;
        }
    }

    /**
     * 獲得起始頁頁碼
     * @return 起始頁頁碼
     */
    public int getStartPage() {
        return startPage;
    }

    /**
     * 獲得終止頁頁碼
     * @return 終止頁頁碼
     */
    public int getEndPage() {
        return endPage;
    }

}
package com.jackfrued.biz;

import com.jackfrued.comm.PageBean;
import com.jackfrued.entity.Dept;

/**
 * 部門業務邏輯介面
 * @author 駱昊
 *
 */
public interface DeptService {

    /**
     * 建立新的部門
     * @param department 部門物件
     * @return 建立成功返回true否則返回false
     */
    public boolean createNewDepartment(Dept department);

    /**
     * 刪除指定部門
     * @param id 要刪除的部門的編號
     * @return 刪除成功返回true否則返回false
     */
    public boolean deleteDepartment(Integer id);

    /**
     * 分頁獲取頂級部門
     * @param page 頁碼
     * @param size 頁碼大小
     * @return 部門物件的分頁器物件
     */
    public PageBean<Dept> getTopDeptByPage(int page, int size);

}
package com.jackfrued.biz.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.jackfrued.biz.DeptService;
import com.jackfrued.comm.PageBean;
import com.jackfrued.comm.QueryResult;
import com.jackfrued.dao.DeptDao;
import com.jackfrued.entity.Dept;

@Service
@Transactional  // 宣告式事務的註解
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptDao deptDao;

    @Override
    public boolean createNewDepartment(Dept department) {
        return deptDao.save(department) != null;
    }

    @Override
    public boolean deleteDepartment(Integer id) {
        return deptDao.deleteById(id);
    }

    @Override
    public PageBean<Dept> getTopDeptByPage(int page, int size) {
        QueryResult<Dept> queryResult = deptDao.findTopDeptByPage(page, size);
        PageBean<Dept> pageBean = new PageBean<>(page, size);
        pageBean.transferQueryResult(queryResult);
        return pageBean;
    }

}

154、如何在Web專案中配置Spring的IoC容器?
答:如果需要在Web專案中使用Spring的IoC容器,可以在Web專案配置檔案web.xml中做出如下配置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

155、如何在Web專案中配置Spring MVC?
答:要使用Spring MVC需要在Web專案配置檔案中配置其前端控制器DispatcherServlet,如下所示:

<web-app>

    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>

</web-app>

說明:上面的配置中使用了*.html的字尾對映,這樣做一方面不能夠通過URL推斷採用了何種伺服器端的技術,另一方面可以欺騙搜尋引擎,因為搜尋引擎不會搜尋動態頁面,這種做法稱為偽靜態化。

156、Spring MVC的工作原理是怎樣的?
答:Spring MVC的工作原理如下圖所示:
這裡寫圖片描述
① 客戶端的所有請求都交給前端控制器DispatcherServlet來處理,它會負責呼叫系統的其他模組來真正處理使用者的請求。
② DispatcherServlet收到請求後,將根據請求的資訊(包括URL、HTTP協議方法、請求頭、請求引數、Cookie等)以及HandlerMapping的配置找到處理該請求的Handler(任何一個物件都可以作為請求的Handler)。
③在這個地方Spring會通過HandlerAdapter對該處理器進行封裝。
④ HandlerAdapter是一個介面卡,它用統一的介面對各種Handler中的方法進行呼叫。
⑤ Handler完成對使用者請求的處理後,會返回一個ModelAndView物件給DispatcherServlet,ModelAndView顧名思義,包含了資料模型以及相應的檢視的資訊。
⑥ ModelAndView的檢視是邏輯檢視,DispatcherServlet還要藉助ViewResolver完成從邏輯檢視到真實檢視物件的解析工作。
⑦ 當得到真正的檢視物件後,DispatcherServlet會利用檢視物件對模型資料進行渲染。
⑧ 客戶端得到響應,可能是一個普通的HTML頁面,也可以是XML或JSON字串,還可以是一張圖片或者一個PDF檔案。

157、如何在Spring IoC容器中配置資料來源?
答:
DBCP配置:

<bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

C3P0配置:

<bean id="dataSource"
        class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass" value="${jdbc.driverClassName}"/>
    <property name="jdbcUrl" value="${jdbc.url}"/>
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

提示: DBCP的詳細配置在第153題中已經完整的展示過了。

158、如何配置配置事務增強?
答:

<?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: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.xsd
     http://www.springframework.org/schema/tx
     http://www.springframework.org/schema/tx/spring-tx.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop.xsd">

  <!-- this is the service object that we want to make transactional -->
  <bean id="fooService" class="x.y.service.DefaultFooService"/>

  <!-- the transactional advice -->
  <tx:advice id="txAdvice" transaction-manager="txManager">
  <!-- the transactional semantics... -->
  <tx:attributes>
    <!-- all methods starting with 'get' are read-only -->
    <tx:method name="get*" read-only="true"/>
    <!-- other methods use the default transaction settings (see below) -->
    <tx:method name="*"/>
  </tx:attributes>
  </tx:advice>

  <!-- ensure that the above transactional advice runs for any execution
    of an operation defined by the FooService interface -->
  <aop:config>
  <aop:pointcut id="fooServiceOperation" 
    expression="execution(* x.y.service.FooService.*(..))"/>
  <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
  </aop:config>

  <!-- don't forget the DataSource -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
    destroy-method="close">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
  <property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl"/>
  <property name="username" value="scott"/>
  <property name="password" value="tiger"/>
  </bean>

  <!-- similarly, don't forget the PlatformTransactionManager -->
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
  </bean>

  <!-- other <bean/> definitions here -->

</beans>

159、選擇使用Spring框架的原因(Spring框架為企業級開發帶來的好處有哪些)?
答:可以從以下幾個方面作答:
- 非侵入式:支援基於POJO的程式設計模式,不強制性的要求實現Spring框架中的介面或繼承Spring框架中的類。
- IoC容器:IoC容器幫助應用程式管理物件以及物件之間的依賴關係,物件之間的依賴關係如果發生了改變只需要修改配置檔案而不是修改程式碼,因為程式碼的修改可能意味著專案的重新構建和完整的迴歸測試。有了IoC容器,程式設計師再也不需要自己編寫工廠、單例,這一點特別符合Spring的精神"不要重複的發明輪子"。
- AOP(面向切面程式設計):將所有的橫切關注功能封裝到切面(aspect)中,通過配置的方式將橫切關注功能動態新增到目的碼上,進一步實現了業務邏輯和系統服務之間的分離。另一方面,有了AOP程式設計師可以省去很多自己寫代理類的工作。
- MVC:Spring的MVC框架是非常優秀的,從各個方面都可以甩Struts 2幾條街,為Web表示層提供了更好的解決方案。
- 事務管理:Spring以寬廣的胸懷接納多種持久層技術,並且為其提供了宣告式的事務管理,在不需要任何一行程式碼的情況下就能夠完成事務管理。
- 其他:選擇Spring框架的原因還遠不止於此,Spring為Java企業級開發提供了一站式選擇,你可以在需要的時候使用它的部分和全部,更重要的是,你甚至可以在感覺不到Spring存在的情況下,在你的專案中使用Spring提供的各種優秀的功能。

160、Spring IoC容器配置Bean的方式?
答:
- 基於XML檔案進行配置。
- 基於註解進行配置。
- 基於Java程式進行配置(Spring 3+)

package com.jackfrued.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private int age;
    @Autowired
    private Car car;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setCar(Car car) {
        this.car = car;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
    }

}
package com.jackfrued.bean;

import org.springframework.stereotype.Component;

@Component
public class Car {
    private String brand;
    private int maxSpeed;

    public Car(String brand, int maxSpeed) {
        this.brand = brand;
        this.maxSpeed = maxSpeed;
    }

    @Override
    public String toString() {
        return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
    }

}
package com.jackfrued.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.jackfrued.bean.Car;
import com.jackfrued.bean.Person;

@Configuration
public class AppConfig {

    @Bean
    public Car car() {
        return new Car("Benz", 320);
    }

    @Bean
    public Person person() {
        return new Person("駱昊", 34);
    }
}
package com.jackfrued.test;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.jackfrued.bean.Person;
import com.jackfrued.config.AppConfig;

class Test {

    public static void main(String[] args) {
        // TWR (Java 7+)
        try(ConfigurableApplicationContext factory = new AnnotationConfigApplicationContext(AppConfig.class)) {
            Person person = factory.getBean(Person.class);
            System.out.println(person);
        }
    }
}

161、闡述Spring框架中Bean的生命週期?
答:
① Spring IoC容器找到關於Bean的定義並例項化該Bean。
② Spring IoC容器對Bean進行依賴注入。
③ 如果Bean實現了BeanNameAware介面,則將該Bean的id傳給setBeanName方法。
④ 如果Bean實現了BeanFactoryAware介面,則將BeanFactory物件傳給setBeanFactory方法。
⑤ 如果Bean實現了BeanPostProcessor介面,則呼叫其postProcessBeforeInitialization方法。
⑥ 如果Bean實現了InitializingBean介面,則呼叫其afterPropertySet方法。
⑦ 如果有和Bean關聯的BeanPostProcessors物件,則這些物件的postProcessAfterInitialization方法被呼叫。
⑧ 當銷燬Bean例項時,如果Bean實現了DisposableBean介面,則呼叫其destroy方法。

162、依賴注入時如何注入集合屬性?
答:可以在定義Bean屬性時,通過<list> / <set> / <map> / <props>分別為其注入列表、集合、對映和鍵值都是字串的對映屬性。

163、Spring中的自動裝配有哪些限制?
答:
- 如果使用了構造器注入或者setter注入,那麼將覆蓋自動裝配的依賴關係。
- 基本資料型別的值、字串字面量、類字面量無法使用自動裝配來注入。
- 優先考慮使用顯式的裝配來進行更精確的依賴注入而不是使用自動裝配。

164、在Web專案中如何獲得Spring的IoC容器?
答:

WebApplicationContext ctx = 
WebApplicationContextUtils.getWebApplicationContext(servletContext);

165. 大型網站在架構上應當考慮哪些問題?
答:
- 分層:分層是處理任何複雜系統最常見的手段之一,將系統橫向切分成若干個層面,每個層面只承擔單一的職責,然後通過下層為上層提供的基礎設施和服務以及上層對下層的呼叫來形成一個完整的複雜的系統。計算機網路的開放系統互聯參考模型(OSI/RM)和Internet的TCP/IP模型都是分層結構,大型網站的軟體系統也可以使用分層的理念將其分為持久層(提供資料儲存和訪問服務)、業務層(處理業務邏輯,系統中最核心的部分)和表示層(系統互動、檢視展示)。需要指出的是:(1)分層是邏輯上的劃分,在物理上可以位於同一裝置上也可以在不同的裝置上部署不同的功能模組,這樣可以使用更多的計算資源來應對使用者的併發訪問;(2)層與層之間應當有清晰的邊界,這樣分層才有意義,才更利於軟體的開發和維護。
- 分割:分割是對軟體的縱向切分。我們可以將大型網站的不同功能和服務分割開,形成高內聚低耦合的功能模組(單元)。在設計初期可以做一個粗粒度的分割,將網站分割為若干個功能模組,後期還可以進一步對每個模組進行細粒度的分割,這樣一方面有助於軟體的開發和維護,另一方面有助於分散式的部署,提供網站的併發處理能力和功能的擴充套件。
- 分散式:除了上面提到的內容,網站的靜態資源(JavaScript、CSS、圖片等)也可以採用獨立分散式部署並採用獨立的域名,這樣可以減輕應用伺服器的負載壓力,也使得瀏覽器對資源的載入更快。資料的存取也應該是分散式的,傳統的商業級關係型資料庫產品基本上都支援分散式部署,而新生的NoSQL產品幾乎都是分散式的。當然,網站後臺的業務處理也要使用分散式技術,例如查詢索引的構建、資料分析等,這些業務計算規模龐大,可以使用Hadoop以及MapReduce分散式計算框架來處理。
- 叢集:叢集使得有更多的伺服器提供相同的服務,可以更好的提供對併發的支援。
- 快取:所謂快取就是用空間換取時間的技術,將資料儘可能放在距離計算最近的位置。使用快取是網站優化的第一定律。我們通常說的CDN、反向代理、熱點資料都是對快取技術的使用。
- 非同步:非同步是實現軟體實體之間解耦合的又一重要手段。非同步架構是典型的生產者消費者模式,二者之間沒有直接的呼叫關係,只要保持資料結構不變,彼此功能實現可以隨意變化而不互相影響,這對網站的擴充套件非常有利。使用非同步處理還可以提高系統可用性,加快網站的響應速度(用Ajax載入資料就是一種非同步技術),同時還可以起到削峰作用(應對瞬時高併發)。&quot;能推遲處理的都要推遲處理"是網站優化的第二定律,而非同步是踐行網站優化第二定律的重要手段。
- 冗餘:各種伺服器都要提供相應的冗餘伺服器以便在某臺或某些伺服器當機時還能保證網站可以正常工作,同時也提供了災難恢復的可能性。冗餘是網站高可用性的重要保證。

166、你用過的網站前端優化的技術有哪些?
答:
① 瀏覽器訪問優化:
- 減少HTTP請求數量:合併CSS、合併JavaScript、合併圖片(CSS Sprite)
- 使用瀏覽器快取:通過設定HTTP響應頭中的Cache-Control和Expires屬性,將CSS、JavaScript、圖片等在瀏覽器中快取,當這些靜態資源需要更新時,可以更新HTML檔案中的引用來讓瀏覽器重新請求新的資源
- 啟用壓縮
- CSS前置,JavaScript後置
- 減少Cookie傳輸
② CDN加速:CDN(Content Distribute Network)的本質仍然是快取,將資料快取在離使用者最近的地方,CDN通常部署在網路運營商的機房,不僅可以提升響應速度,還可以減少應用伺服器的壓力。當然,CDN快取的通常都是靜態資源。
③ 反向代理:反向代理相當於應用伺服器的一個門面,可以保護網站的安全性,也可以實現負載均衡的功能,當然最重要的是它快取了使用者訪問的熱點資源,可以直接從反向代理將某些內容返回給使用者瀏覽器。

167、你使用過的應用伺服器優化技術有哪些?
答:
① 分散式快取:快取的本質就是記憶體中的雜湊表,如果設計一個優質的雜湊函式,那麼理論上雜湊表讀寫的漸近時間複雜度為O(1)。快取主要用來存放那些讀寫比很高、變化很少的資料,這樣應用程式讀取資料時先到快取中讀取,如果沒有或者資料已經失效再去訪問資料庫或檔案系統,並根據擬定的規則將資料寫入快取。對網站資料的訪問也符合二八定律(Pareto分佈,冪律分佈),即80%的訪問都集中在20%的資料上,如果能夠將這20%的資料快取起來,那麼系統的效能將得到顯著的改善。當然,使用快取需要解決以下幾個問題:
- 頻繁修改的資料;
- 資料不一致與髒讀;
- 快取雪崩(可以採用分散式快取伺服器叢集加以解決,memcached是廣泛採用的解決方案);
- 快取預熱;
- 快取穿透(惡意持續請求不存在的資料)。
② 非同步操作:可以使用訊息佇列將呼叫非同步化,通過非同步處理將短時間高併發產生的事件訊息儲存在訊息佇列中,從而起到削峰作用。電商網站在進行促銷活動時,可以將使用者的訂單請求存入訊息佇列,這樣可以抵禦大量的併發訂單請求對系統和資料庫的衝擊。目前,絕大多數的電商網站即便不進行促銷活動,訂單系統都採用了訊息佇列來處理。
③ 使用叢集。
④ 程式碼優化:
- 多執行緒:基於Java的Web開發基本上都通過多執行緒的方式響應使用者的併發請求,使用多執行緒技術在程式設計上要解決執行緒安全問題,主要可以考慮以下幾個方面:A. 將物件設計為無狀態物件(這和麵向物件的程式設計觀點是矛盾的,在物件導向的世界中被視為不良設計),這樣就不會存在併發訪問時物件狀態不一致的問題。B. 在方法內部建立物件,這樣物件由進入方法的執行緒建立,不會出現多個執行緒訪問同一物件的問題。使用ThreadLocal將物件與執行緒繫結也是很好的做法,這一點在前面已經探討過了。C. 對資源進行併發訪問時應當使用合理的鎖機制。
- 非阻塞I/O: 使用單執行緒和非阻塞I/O是目前公認的比多執行緒的方式更能充分發揮伺服器效能的應用模式,基於Node.js構建的伺服器就採用了這樣的方式。Java在JDK 1.4中就引入了NIO(Non-blocking I/O),在Servlet 3規範中又引入了非同步Servlet的概念,這些都為在伺服器端採用非阻塞I/O提供了必要的基礎。
- 資源複用:資源複用主要有兩種方式,一是單例,二是物件池,我們使用的資料庫連線池、執行緒池都是物件池化技術,這是典型的用空間換取時間的策略,另一方面也實現對資源的複用,從而避免了不必要的建立和釋放資源所帶來的開銷。

168、什麼是XSS攻擊?什麼是SQL隱碼攻擊?什麼是CSRF攻擊?
答:
- XSS(Cross Site Script,跨站指令碼攻擊)是向網頁中注入惡意指令碼在使用者瀏覽網頁時在使用者瀏覽器中執行惡意指令碼的攻擊方式。跨站指令碼攻擊分有兩種形式:反射型攻擊(誘使使用者點選一個嵌入惡意指令碼的連結以達到攻擊的目標,目前有很多攻擊者利用論壇、微博釋出含有惡意指令碼的URL就屬於這種方式)和持久型攻擊(將惡意指令碼提交到被攻擊網站的資料庫中,使用者瀏覽網頁時,惡意指令碼從資料庫中被載入到頁面執行,QQ郵箱的早期版本就曾經被利用作為持久型跨站指令碼攻擊的平臺)。XSS雖然不是什麼新鮮玩意,但是攻擊的手法卻不斷翻新,防範XSS主要有兩方面:消毒(對危險字元進行轉義)和HttpOnly(防範XSS攻擊者竊取Cookie資料)。
- SQL隱碼攻擊是注入攻擊最常見的形式(此外還有OS注入攻擊(Struts 2的高危漏洞就是通過OGNL實施OS注入攻擊導致的)),當伺服器使用請求引數構造SQL語句時,惡意的SQL被嵌入到SQL中交給資料庫執行。SQL隱碼攻擊需要攻擊者對資料庫結構有所瞭解才能進行,攻擊者想要獲得表結構有多種方式:(1)如果使用開源系統搭建網站,資料庫結構也是公開的(目前有很多現成的系統可以直接搭建論壇,電商網站,雖然方便快捷但是風險是必須要認真評估的);(2)錯誤回顯(如果將伺服器的錯誤資訊直接顯示在頁面上,攻擊者可以通過非法引數引發頁面錯誤從而通過錯誤資訊瞭解資料庫結構,Web應用應當設定友好的錯誤頁,一方面符合最小驚訝原則,一方面遮蔽掉可能給系統帶來危險的錯誤回顯資訊);(3)盲注。防範SQL隱碼攻擊也可以採用消毒的方式,通過正規表示式對請求引數進行驗證,此外,引數繫結也是很好的手段,這樣惡意的SQL會被當做SQL的引數而不是命令被執行,JDBC中的PreparedStatement就是支援引數繫結的語句物件,從效能和安全性上都明顯優於Statement。
- CSRF攻擊(Cross Site Request Forgery,跨站請求偽造)是攻擊者通過跨站請求,以合法的使用者身份進行非法操作(如轉賬或發帖等)。CSRF的原理是利用瀏覽器的Cookie或伺服器的Session,盜取使用者身份,其原理如下圖所示。防範CSRF的主要手段是識別請求者的身份,主要有以下幾種方式:(1)在表單中新增令牌(token);(2)驗證碼;(3)檢查請求頭中的Referer(前面提到防圖片盜連結也是用的這種方式)。令牌和驗證都具有一次消費性的特徵,因此在原理上一致的,但是驗證碼是一種糟糕的使用者體驗,不是必要的情況下不要輕易使用驗證碼,目前很多網站的做法是如果在短時間內多次提交一個表單未獲得成功後才要求提供驗證碼,這樣會獲得較好的使用者體驗。

這裡寫圖片描述

補充:防火牆的架設是Web安全的重要保障,ModSecurity是開源的Web防火牆中的佼佼者。企業級防火牆的架設應當有兩級防火牆,Web伺服器和部分應用伺服器可以架設在兩級防火牆之間的DMZ,而資料和資源伺服器應當架設在第二級防火牆之後。

169. 什麼是領域模型(domain model)?貧血模型(anaemic domain model)和充血模型(rich domain model)有什麼區別?
答:領域模型是領域內的概念類或現實世界中物件的視覺化表示,又稱為概念模型或分析物件模型,它專注於分析問題領域本身,發掘重要的業務領域概念,並建立業務領域概念之間的關係。貧血模型是指使用的領域物件中只有setter和getter方法(POJO),所有的業務邏輯都不包含在領域物件中而是放在業務邏輯層。有人將我們這裡說的貧血模型進一步劃分成失血模型(領域物件完全沒有業務邏輯)和貧血模型(領域物件有少量的業務邏輯),我們這裡就不對此加以區分了。充血模型將大多數業務邏輯和持久化放在領域物件中,業務邏輯(業務門面)只是完成對業務邏輯的封裝、事務和許可權等的處理。下面兩張圖分別展示了貧血模型和充血模型的分層架構。

貧血模型
這裡寫圖片描述

充血模型
這裡寫圖片描述

貧血模型下組織領域邏輯通常使用事務指令碼模式,讓每個過程對應使用者可能要做的一個動作,每個動作由一個過程來驅動。也就是說在設計業務邏輯介面的時候,每個方法對應著使用者的一個操作,這種模式有以下幾個有點:
- 它是一個大多數開發者都能夠理解的簡單過程模型(適合國內的絕大多數開發者)。
- 它能夠與一個使用行資料入口或表資料入口的簡單資料訪問層很好的協作。
- 事務邊界的顯而易見,一個事務開始於指令碼的開始,終止於指令碼的結束,很容易通過代理(或切面)實現宣告式事務。
然而,事務指令碼模式的缺點也是很多的,隨著領域邏輯複雜性的增加,系統的複雜性將迅速增加,程式結構將變得極度混亂。開源中國社群上有一篇很好的譯文《貧血領域模型是如何導致糟糕的軟體產生》對這個問題做了比較細緻的闡述。

170. 談一談測試驅動開發(TDD)的好處以及你的理解。
答:TDD是指在編寫真正的功能實現程式碼之前先寫測試程式碼,然後根據需要重構實現程式碼。在JUnit的作者Kent Beck的大作《測試驅動開發:實戰與模式解析》(Test-Driven Development: by Example)一書中有這麼一段內容:“消除恐懼和不確定性是編寫測試驅動程式碼的重要原因”。因為編寫程式碼時的恐懼會讓你小心試探,讓你迴避溝通,讓你羞於得到反饋,讓你變得焦躁不安,而TDD是消除恐懼、讓Java開發者更加自信更加樂於溝通的重要手段。TDD會帶來的好處可能不會馬上呈現,但是你在某個時候一定會發現,這些好處包括:
- 更清晰的程式碼 — 只寫需要的程式碼
- 更好的設計
- 更出色的靈活性 — 鼓勵程式設計師面向介面程式設計
- 更快速的反饋 — 不會到系統上線時才知道bug的存在

補充:敏捷軟體開發的概念已經有很多年了,而且也部分的改變了軟體開發這個行業,TDD也是敏捷開發所倡導的。

TDD可以在多個層級上應用,包括單元測試(測試一個類中的程式碼)、整合測試(測試類之間的互動)、系統測試(測試執行的系統)和系統整合測試(測試執行的系統包括使用的第三方元件)。TDD的實施步驟是:紅(失敗測試)- 綠(通過測試) - 重構。關於實施TDD的詳細步驟請參考另一篇文章《測試驅動開發之初窺門徑》
在使用TDD開發時,經常會遇到需要被測物件需要依賴其他子系統的情況,但是你希望將測試程式碼跟依賴項隔離,以保證測試程式碼僅僅針對當前被測物件或方法展開,這時候你需要的是測試替身。測試替身可以分為四類:
- 虛設替身:只傳遞但是不會使用到的物件,一般用於填充方法的引數列表
- 存根替身:總是返回相同的預設響應,其中可能包括一些虛設狀態
- 偽裝替身:可以取代真實版本的可用版本(比真實版本還是會差很多)
- 模擬替身:可以表示一系列期望值的物件,並且可以提供預設響應
Java世界中實現模擬替身的第三方工具非常多,包括EasyMock、Mockito、jMock等。

相關文章