引言
此篇文章結合各路大神文章以及自己的一些拙見,為更好的學習和之後的深入了解而作,不足之處還望提出。
開始學習前建議自己建立一個簡單的spring項目。
Eclipse搭建一個最簡單的Spring架構Maven項目附帶XML顯式配置裝載demo
JavaEE體系結構包括四層,從上到下分别是應用層、Web層、業務層、持久層。Struts和SpringMVC是Web層的架構,Spring是業務層的架構,Hibernate和MyBatis是持久層的架構。
Spring
概念
Spring是一個開源的容器架構,使用基本的JavaBean代替EJB。它主要是為了解決企業應用開發的複雜性而誕生的。簡單來說,Spring是一個輕量級的控制反轉(IOC)和面向切面(AOP)的容器架構。
Spring的優點
-
AOP和IOC。
下面做詳細介紹
-
低侵入式設計,代碼污染度很低。
基于Spring開發的應用一般不依賴于spring的類。
-
DI機制将對象之間的依賴關系交給Spring控制管理,友善解耦。
高内聚低耦合
-
聲明式事務的支援。
隻需要通過配置就可以完成對事務的管理,而無需手動程式設計
-
支援內建各種優秀的架構。
如Hibernate,Struts2,Mybatis等。
Spring的缺點
- 使用XML配置複雜繁瑣容易出錯。
- 粘性大,架構內建後不容易拆分。
組成Spring架構的七大子產品
- 結構圖。
深入剖析實戰Spring引言SpringSpringBeanSpring事務管理Spring原理常問面試題 - 核心容器(Spring Core)
- 提供 Spring架構的基本功能;
- 主要元件是 BeanFactory(Spring的核心類),負責産生和管理Bean,它是工廠模式的經典實作;
- 提供控制反轉 (IOC)和依賴注入(DI)特性,将應用的配置和依賴性規範與實際的應用程式代碼分開。
- Spring 上下文(Spring Context)
- 是一個配置檔案,向 Spring架構提供上下文資訊。
- SpringContext子產品繼承BeanFactory類,添加了事件處理、國際化、資源裝載、透明裝載、以及資料校驗等功能;
- 還提供了架構式的Bean的通路方式和企業級的功能,如JNDI通路,支援EJB、遠端調用、繼承模闆架構、Email和定時任務排程等。
- 面向切面程式設計(Spring AOP)
- 直接将面向方面的程式設計功能內建到了Spring架構中,使 Spring架構管理的任何對象支援 AOP;
- 為基于 Spring 的應用程式中的對象提供了事務管理服務;
- 通過使用 Spring AOP,不用依賴 EJB 元件,就可以将聲明性事務管理內建到應用程式中。
- JDBC和DAO子產品(Spring DAO)
- DAO(DataAccessObject)模式思想是将業務邏輯代碼與資料庫互動代碼分離,降低兩者耦合;
- 抽象層提供了有意義的異常層次結構,可用該結構來管理異常處理和不同資料庫供應商抛出的錯誤消息;
- 異常層次結構簡化了錯誤處理,并且極大地降低了需要編寫的異常代碼數量(例如打開和關閉連接配接)。
- 對象關系映射(Spring ORM)
- Spring架構插入了若幹個 ORM 架構;
- 提供了 ORM 的對象關系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map,所有的這些都遵從Spring的通用事務和DAO異常層次結構;
- 注意這裡Spring是提供各類的接口(support),目前比較流行的下層資料庫封閉映射架構,如ibatis,Hibernate等。
- Spring Web
- 此子產品建立在SpringContext基礎之上,提供了Servlet監聽器的Context和Web應用的上下文;
- 對現有的Web架構,如JSF、Tapestry、Structs等提供了內建;
- SpringWeb子產品還簡化了處理多部分請求以及将請求參數綁定到域對象的工作。
- Spring Web MVC
- 建立在Spring核心功能之上,擁有Spring架構的所有特性,能夠适應多種多視圖、模闆技術、國際化和驗證服務,實作控制邏輯和業務邏輯的清晰分離;
- 通過政策接口,MVC 架構變成為高度可配置的,MVC 容納了大量視圖技術,其中包括 JSP、Velocity、Tiles、iText 和 POI;
- MVC模型待完善(将會新寫一篇關于Spring Mvc架構的總結)。
IOC(控制反轉)/ DI(依賴注入)
IoC和DI有什麼關系呢?其實它們是同一個概念的不同角度描述,由于控制反轉概念比較含糊(可能隻是了解為容器控制對象這一個層面,很難讓人想到誰來維護對象關系),是以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴注入”,相對IoC 而言,“依賴注入”明确描述了“被注入對象依賴IoC容器配置依賴對象”。
定義了解何為IOC
IOC主要是為了解決對象與對象之間的複雜的依賴關系,借助于“第三方”實作具有依賴關系的對象之間的解耦。IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯系,這就是有人把IOC容器比喻成“粘合劑”的由來。
獲得依賴對象的過程由自身管理變為了由IOC容器主動注入。
在未引入IOC之前對象之間的依賴關系通過自身的相關調用,這是一個主動行為;在引入IOC之後容器将對象之間的依賴關系解耦,此時對象之間失去了直接的聯系,擷取依賴的過程從主動行為被成了被動行為,控制權颠倒。
對象A依賴于對象B,當對象 A需要用到對象B的時候,IOC容器就會立即建立一個對象B送給對象A。IOC容器就是一個對象制造工廠,你需要什麼,它會給你送去,你直接使用就行了,而再也不用去關心你所用的東西是如何制成的,也不用關心最後是怎麼被銷毀的,這一切全部由IOC容器包辦。
架構師之路(39)—IoC架構
Inversion of Control Containers and the Dependency Injection pattern
深度了解依賴注入(Dependence Injection)
Inside ObjectBuilder Part1
DI注入的幾種方式
建立應用對象之間協作關系的行為通常稱為裝配( wiring ),這也是依賴注入( DI )的本質。
- 在 XML 中進行顯式配置。
- 在 Java 中進行顯式配置
- 隐式的 bean 發現機制和自動裝配
在 XML 中進行顯式配置
1、spring依靠xml檔案進行顯式裝配,xml檔案的根節點是,包含多個節點
2、xml檔案中每個bean節點可以指定對應元件的類型,名稱
3、xml檔案中,通過bean的property子節點實作屬性初始化或依賴注入
4、xml檔案中,通過bean的constructor-arg子節點實作構造器參數初始化或依賴注入
5、xml檔案中,通過property或constructor-arg的子節點或實作清單的注入
6、所有通過xml顯式配置的bean,在代碼中通過上一篇文章所描述的應用上下文實作ClassPathXmlApplicationContext從容器中擷取實體
《Spring實戰》-第二章:Bean的裝配(1)-XML顯式裝配
PS:constructor-arg注入時使用name value屬性可能會報錯
Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 8 in XML document from class path resource [gz.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 8; columnNumber: 49; cvc-complex-type.3.2.2: 元素 'constructor-arg' 中不允許出現屬性 'vaule'。
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:399)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:181)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:217)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:252)
at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:127)
at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:93)
at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:613)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:514)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
at gztest.Test.main(Test.java:8)
Caused by: org.xml.sax.SAXParseException; lineNumber: 8; columnNumber: 49; cvc-complex-type.3.2.2: 元素 'constructor-arg' 中不允許出現屬性 'vaule'。
解決辦法:
1、更改spring版本,下面代碼最後一行的最右邊改成spring-beans-4.0.xsd。
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
2、使用index方式注入
<bean id="person" class="gztest.Person">
<constructor-arg index="0">
<value>daniel</value>
</constructor-arg>
<constructor-arg index="1">
<value>18</value>
</constructor-arg>
</bean>
在 Java 中進行顯式配置
1、JavaConfig顯式配置相對于XML顯式配置來說,具有配置簡單,維護友善的優點
2、JavaConfig顯式配置主要依賴于@Bean注解和@Configuration兩個注解實作,其中@Configuration注解标明目前類為配置類,而@Bean使用在配置類中的方法内,定義一個裝配類名為方法名的Bean
3、Spring使用@Autowired進行依賴注入
4、Spring中我們可以通過兩種方式友善的對javaConfig顯式配置進行測試,一種是結合SpringJUnit4ClassRunner模拟裝配環境進行測試,一種是使用應用上下文AnnotationConfigApplicationContext進行測試
《Spring實戰》-第二章:Bean的裝配(2)-JavaConfig顯式裝配
模拟裝配環境示例
@RunWith(SpringJUnit4ClassRunner.class)//使用Spring提供的測試包進行測試,主要幫助實作bean的裝載環境
@ContextConfiguration(loader = AnnotationConfigContextLoader.class,classes = {CDConfig.class})//配置類指向CDConfig
public class AppTest
{
//使用注解自動注入
@Autowired
private CDPlayer cdPlayer;
/**
* Rigorous Test :-)
*/
@Test
public void play()
{
this.cdPlayer.playCD();
}
}
隐式的 bean 發現機制和自動裝配
1、自動化裝配使用全注解方式替代XML和JavaConfig顯式裝配方式,友善簡潔
2、自動化裝配依賴于幾個特殊注解:@Autowired,@Component,@ComponentScan和@Configuration
3、@Component注解可以将類定義為裝配的元件,同時可以為改元件另起别名(ID)
4、@Configuration将使用該注解的目前類标注為配置類,@ComponentScan開啟自動掃描,預設掃描目前配置類所在的包及其子包中含有或使用了@Component注解的類,可通過屬性指定掃描範圍
5、@Autowired注解可以用在屬性,構造函數,setter以及任何一個普通方法中,是Spring依賴注入的核心注解
《Spring實戰》-第二章:Bean的裝配(3)-自動化裝配
指定掃描包的幾種方式
//@ComponentScan(basePackages = {"com.my.spring","com.my.test"})//指定掃描com.my.spring包和com.my.test包及其子包下的元件
//@ComponentScan(basePackageClasses = {App.class})//指定掃描App.class所在包及其子包下的元件
@ComponentScan//僅能掃描自身所在包及其子包的元件
@Configuration
public class CDConfig {}
//注:通常,為了防止因包路徑更改以及業務實體類更改等因耦合而産生的問題,我們通常會使用一個不具備任何業務意義的空接口作為掃描包的類
混合導入裝配機制
最常用的方式,将多個javaConfig或多個XML配置統一到一個檔案中進行管理的技術,這樣有利于進行配置管理(UCM,既統一配置檔案管理,也就是所有配置檔案統一加載、統一讀取。)
《Spring實戰》-第二章:Bean的裝配(4)-混合導入裝配機制
AOP(面向切面程式設計)
概念
在運作時,動态地将代碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計。
程式在執行具體業務邏輯前、後,可能需要加入一些額外的東西,譬如:日志,事務,安全等。在很多地方都需要加入這些額外的元素,但是這些方面和具體的業務邏輯并沒有直接的關系。于是乎,就将這些額外的元素抽出來,加以封裝成一個特殊的切面類(aspect),在特定的位置橫切到方法的業務邏輯前後等位置。将業務和非業務的分開,極大程度解耦。
舉個例子,當你的程式寫好後,需要在所有的業務操作中添加一條日志,傳統的做法是直接改造每個方法,但是這樣勢必讓代碼變糟糕,要是以後擴充起來那就更亂了。而AOP的思想就讓你能從一個切面的角度來看待這些插入問題,AOP允許你以一種統一的方式在運作時期在想要的地方插入這些邏輯。
複雜性模型
如果将日志,事務,安全這些關注點分散到多個元件中去,你的代碼将會帶來雙重的複雜性。
實作系統關注點功能的代碼将會重複出現在多個元件中。這意味着如果你要改變這些關注點的邏輯,必須修改各個子產品中的相關實作。
即使你把這些關注點抽象為一個獨立的子產品,其他子產品隻是調用它的方法,但方法的調用還是會重複出現在各個子產品中。
元件會因為那些與自身核心業務無關的代碼而變得混亂。一個向位址簿增加位址條目的方法應該隻關注如何添加位址,而不應該關注它是不是安全的或者是否需要支援事務
上圖展示了這種複雜性。左邊的業務對象與系統級服務結合得過于緊密。每個對象不但要知道它需要記日志、進行安全控制和參與事務,還要親自執行這些服務。
AOP的作用
在整個系統内,關注點(例如日志、安全和事務)的調用經常散布到各個子產品中,而這些關注點并不是子產品的核心業務。
AOP能夠使這些服務子產品化,并以聲明的方式将它們應用到它們需要影響的元件中去。所造成的結果就是這些元件會具有更高的内聚性并且會更加關注自身的業務,完全不需要了解涉及系統服務所帶來複雜性。總之,AOP能夠確定POJO的簡單性。
如下圖所示,我們可以把切面想象為覆寫在很多元件之上的一個外殼。應用是由那些實作各自業務功能的子產品組成的。借助AOP,可以使用各種功能層去包裹核心業務層。這些層以聲明的方式靈活地應用到系統中,你的核心應用甚至根本不知道它們的存在。這是一個非常強大的理念,可以将安全、事務和日志關注點與核心業務邏輯相分離。
利用AOP, 系統範圍内的關注點覆寫在它們所影響元件之上
AOP術語
①通知( Advice ):切面的工作被稱為通知。通知描述切面的工作,同時決定切面何時工作【定義了切面工作做什麼,什麼時候做】
- 前置通知( Before ):在目标方法被調用之前調用通知功能;
- 後置通知( After ):在目标方法完成之後調用通知,此時不會關心方法的輸出是什麼;
- 傳回通知( After-returning ):在目标方法成功執行之後調用通知;
- 異常通知( After-throwing ):在目标方法抛出異常後調用通知;
- 環繞通知( Around ):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行為
②連接配接點( Join point ):觸發切面工作的點,比如方法執行,異常抛出等行為
③切點( Poincut ):決定切面工作的地方,比如某個方法等【定義了切面工作在哪裡做】
④切面( Aspect ):通知和切點的結合【面】
⑤引入( Introduction ):允許我們向現有的類添加新方法或屬性
⑥織入( Weaving ):把切面應用到目标對象并建立新的代理對象的過程【切面在指定的連接配接點被織入到目标對象中】
通知包含了需要用于多個應用對象的橫切行為;連接配接點是程式執行過程中能夠應用通知的所有點;切點定義了通知被應用的具體位置(在哪些連接配接點)。其中關鍵的概念是切點定義了哪些連接配接點會得到通知
Spring對AOP的支援(包括代理模式以及示例demo)
- 基于代理的經典 Spring AOP ;
- 純 POJO 切面;
- @AspectJ 注解驅動的切面;
- 注入式 AspectJ 切面(适用于 Spring 各版本)。
-
靜态代理
AspectJ是靜态代理的增強,所謂靜态代理,就是AOP架構會在編譯階段生成AOP代理類,是以也稱為編譯時增強,他會在編譯階段将AspectJ(切面)織入到Java位元組碼中,運作的時候就是增強之後的AOP對象。
為其他對象提供一種代理以控制對這個對象的通路。在某些情況下,一個對象不适合或者不能直接引用另一個對象,而代理對象可以在用戶端和目标對象之間起到中介的作用。
比如,有兩個有錢人張三和李四在杭州有一套房子要租出去,隻有找到租客才能收租金,李四找了中介幫他,張三自己找人,其中的中介就起到代理的作用,李四可以不再去管租房的事情而是每個月收收房租做個快樂的房東,張三就不一樣了,他必須自己找到租客。
下面demo有關于靜态代理的代碼實作。
-
動态代理
Spring AOP使用的動态代理,所謂的動态代理就是說AOP架構不會去修改位元組碼,而是每次運作時在記憶體中臨時為方法生成一個AOP對象,這個AOP對象包含了目标對象的全部方法,并且在特定的切點做了增強處理,并回調原對象的方法。
-
JDK動态代理
面向接口生成代理,原理就是類加載器根據接口,在虛拟機内部建立接口實作類
Proxy.newProxyInstance(classloader,interfaces[], invocationhandler );
invocationHandler 通過invoke()方法反射來調用目标類中的代碼。
1.建立目标業務對象的引用;
2.使用目标業務對象類加載器和接口,在記憶體中建立代理對象;
3.實作InvocationHandler接口。
Java動态代理InvocationHandler和Proxy學習筆記
-
CGLIB動态代理
使用範圍:對于不适用接口的業務類,無法使用JDK動态代理。
原理:CGLIB采用非常底層的位元組碼技術,可以為一個類建立子類,解決無接口類的代理類問題。
通過實作MethodInterceptor接口重寫intercept方法實作攔截與代理增加;通過invokeSuper()方法反射調用目标類中的代碼。
1.使用cglib自帶的位元組碼增強器Enhancer建立真實業務類的子類;
2.将委托類設定成父類;
3.設定回調函數、攔截器。
cglib源碼分析
-
-
具體實作demo:
Spring對AOP切面支援實作及示例demo(代理模式實作、注解驅動、注入式)
Spring JDBC
概念
Spring通過抽象JDBC通路并提供一緻的API來簡化JDBC程式設計的工作量。我們隻需要聲明SQL、調用合适的Spring JDBC架構API、處理結果集即可。
與傳統JDBC的差別
傳統JDBC | Spring JDBC |
---|---|
擷取JDBC連接配接 | 擷取JDBC連接配接 |
聲明sql | 聲明sql |
預編譯sql | 預編譯sql |
執行sql | 執行sql |
處理結果集 | 處理結果集 |
釋放結果集 | 釋放結果集 |
釋放Statement | 釋放Statement |
送出事務 | 送出事務 |
處理異常并復原事務 | 處理異常并復原事務 |
釋放JDBC連接配接 | 釋放JDBC連接配接 |
------------------------------------------- | ------------------------------------------- |
缺點 | 優點 |
1.冗長,重複 | 1.簡單、簡潔 |
2.顯示事務控制 | 2.Spring事務控制 |
3.每個步驟不可擷取 | 3.隻做需要做的 |
4.顯示處理受檢查異常 | 4.一緻的非檢查異常體系 |
Spring JDBC的三種工作模式
- JDBC模闆方式: Spring JDBC架構提供以下幾種模闆類來簡化JDBC程式設計,實作GoF模闆設計模式,将可變部分和非可變部分分離,可變部分采用回調接口方式由使用者來實作:如JdbcTemplate、NamedParameterJdbcTemplate、SimpleJdbcTemplate。
- 關系資料庫操作對象化方式: Spring JDBC架構提供了将關系資料庫操作對象化的表示形式,進而使使用者可以采用面向對象程式設計來完成對資料庫的通路;如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等類。這些類的實作一旦建立即可重用并且是線程安全的。
- SimpleJdbc方式: Spring JDBC架構還提供了SimpleJdbc方式來簡化JDBC程式設計,SimpleJdbcInsert 、 SimpleJdbcCall用來簡化資料庫表插入、存儲過程或函數通路。
Spring JDBC還提供了一些強大的工具類,如DataSourceUtils來在必要的時候手工擷取資料庫連接配接等。
Spring JDBC子產品組成
- support包: 提供将JDBC異常轉換為DAO非檢查異常轉換類、一些工具類如JdbcUtils等。
- datasource包: 提供簡化通路JDBC 資料源(javax.sql.DataSource實作)工具類,并提供了一些DataSource簡單實作類進而能使從這些DataSource擷取的連接配接能自動得到Spring管理事務支援。
- core包: 提供JDBC模闆類實作及可變部分的回調接口,還提供SimpleJdbcInsert等簡單輔助類。
- object包: 提供關系資料庫的對象表示形式,如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等類,該包是基于core包JDBC模闆類實作。
Spring JDBC模闆實戰
- 引入jar包,也可以自己選擇其他的連接配接池和資料庫驅動比如DBCP,C3P0等。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- 阿裡巴巴資料源包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- mysql驅動包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
- 建立db.properties資料庫屬性檔案,注意此處配置隻适用與Mysql5.7及以下版本,Mysql5.8版本配置與之前的版本有差別
validationQuery=SELECT 1 FROM DUAL
jdbc_driverClassName=com.mysql.jdbc.Driver
jdbc_url=jdbc\:mysql\://192.168.1.15\:3311/test?rewriteBatchedStatements\=true&useUnicode\=true&characterEncoding\=utf-8
jdbc_username=root
jdbc_password=123456
- 建立springjdbc.xml配置檔案
<?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:context="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-4.1.xsd">
<!-- 屬性檔案的注入方法:Bean方法 -->
<!-- <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:db.properties"></property>
</bean> -->
<!-- 屬性檔案的注入方法:Context方法 -->
<context:property-placeholder location="classpath:db.properties" />
<bean id="log-filter" class="com.alibaba.druid.filter.logging.Log4jFilter">
<property name="statementExecutableSqlLogEnable" value="false" />
<property name="resultSetLogEnabled" value="false" />
<property name="statementPrepareAfterLogEnabled" value="false" />
</bean>
<bean id="dataSourceDefault" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc_driverClassName}" />
<property name="url" value="${jdbc_url}" />
<property name="username" value="${jdbc_username}" />
<property name="password" value="${jdbc_password}" />
<!-- 初始化連接配接大小 -->
<property name="initialSize" value="2" />
<!-- 連接配接池最大使用連接配接數量 -->
<property name="maxActive" value="60" />
<!-- 連接配接池最小空閑 -->
<property name="minIdle" value="0" />
<!-- 擷取連接配接最大等待時間 -->
<property name="maxWait" value="60000" />
<property name="validationQuery" value="${validationQuery}" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接配接,機關是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個連接配接在池中最小生存的時間,機關是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分鐘 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 關閉abanded連接配接時輸出錯誤日志 -->
<property name="logAbandoned" value="true" />
<!-- 列印日志 -->
<property name="proxyFilters">
<list>
<ref bean="log-filter" />
</list>
</property>
</bean>
<!-- Spring提供的jdbcTemplate模闆 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSourceDefault"></property>
</bean>
</beans>
- 在目标資料庫建立表
create table jdbc_test(
id varchar(20) NOT NULL PRIMARY KEY,
text varchar(100) DEFAULT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8
- 建立測試類
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
public class JdbcTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("/springjdbc.xml");
JdbcTemplate jdbc = ac.getBean(JdbcTemplate.class);
String sql = "insert into jdbc_test values(?,?)";
for (int i = 0; i < 20; i++) {
//建立
jdbc.update(sql,i,"test"+i);
}
}
}
SpringBean
Bean的生命周期
可以簡單描述bean的生命周期:
- 執行個體化Bean
- 依賴注入
- 處理Aware接口
- BeanPostProcessor(自定義處理)
- init-method(初始化)
- DisposableBean(清理階段)
- destroy-method(銷毀)
若容器實作了流程圖中涉及的接口,程式将按照以上流程進行。需要我們注意的是,這些接口并不是必須實作的,可根據自己開發中的需要靈活地進行選擇,沒有實作相關接口時,将略去流程圖中的相關步驟。
分類類型 | 所包含方法 |
---|---|
Bean自身的方法 | 配置檔案中的init-method和destroy-method配置的方法、Bean對象自己調用的方法 |
Bean級生命周期接口方法 | BeanNameAware、BeanFactoryAware、InitializingBean、DiposableBean等接口中的方法 |
容器級生命周期接口方法 | InstantiationAwareBeanPostProcessor、BeanPostProcessor等後置處理器實作類中重寫的方法 |
SpringBean生命周期詳解
提供後初始化和預破壞方法的三種方式
建立實體類
public class Employee {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@ PostConstruct,@ PreDestroy注釋
将定義的init-method用@ PostConstruct注釋,destroy-method用@PreDestroy注釋
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class MyService {
private Employee employee;
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
@PostConstruct
public void init(){
System.out.println("MyService init method called");
if(employee.getName() == null){
employee.setName("woc");
}
}
public MyService(){
System.out.println("MyService no-args constructor called");
}
@PreDestroy
public void destory(){
System.out.println("MyService destroy method called");
}
}
配置檔案
<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor" />
<bean name="myService" class="beanLifeCycle.MyService" >
<property name="employee" ref="employee"></property>
</bean>
InitializingBean,DisposableBean接口
實作post-init和pre-destroy方法的接口
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class EmployeeService implements InitializingBean,DisposableBean{
private Employee employee;
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public EmployeeService(){
System.out.println("EmployeeService no-args constructor called");
}
public void destroy() throws Exception {
System.out.println("EmployeeService Closing resources");
}
public void afterPropertiesSet() throws Exception {
System.out.println("EmployeeService initializing to dummy value");
if(employee.getName() == null){
employee.setName("66666666");
}
}
}
配置檔案
<bean name="employeeService" class="beanLifeCycle.EmployeeService">
<property name="employee" ref="employee"></property>
</bean>
自定義post-init,pre-destroy
自定義方法在配置檔案中配置
public class MyEmployeeService{
private Employee employee;
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public MyEmployeeService(){
System.out.println("MyEmployeeService no-args constructor called");
}
//pre-destroy method
public void destroy() throws Exception {
System.out.println("MyEmployeeService Closing resources");
}
//post-init method
public void init() throws Exception {
System.out.println("MyEmployeeService initializing to dummy value");
if(employee.getName() == null){
employee.setName("daniel");
}
}
}
配置檔案
<bean name="myEmployeeService" class="beanLifeCycle.MyEmployeeService"
init-method="init" destroy-method="destroy">
<property name="employee" ref="employee"></property>
</bean>
注意點
- bean初始化的順序與spring bean配置檔案中定義的順序相同。
- Spring Context首先調用no-args構造函數來初始化bean對象,然後調用post-init方法。
- 僅當使用post-init方法執行正确初始化所有spring bean時才傳回上下文。
- 當上下文被關閉時,bean按照它們被初始化的相反順序被銷毀,即以LIFO(後進先出)順序。
Aware接口
Spring Aware接口類似于具有回調方法和實作觀察者設計模式的servlet監聽器。
一些重要的Aware接口是:
- ApplicationContextAware - 注入ApplicationContext對象,示例用法是擷取bean定義名稱的數組。
- BeanFactoryAware - 注入BeanFactory對象,示例用法是檢查bean的範圍。
- BeanNameAware - 知道配置檔案中定義的bean名稱。
- ResourceLoaderAware - 要注入ResourceLoader對象,示例用法是擷取類路徑中檔案的輸入流。
- ServletContextAware - 在MVC應用程式中注入ServletContext對象,示例用法是讀取上下文參數和屬性。
- ServletConfigAware - 在MVC應用程式中注入ServletConfig對象,示例用法是擷取servlet配置參數。
import java.util.Arrays;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
public class MyAwareService implements ApplicationContextAware,
ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware,
BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware {
@Override
public void setApplicationContext(ApplicationContext ctx)
throws BeansException {
System.out.println("setApplicationContext called");
System.out.println("setApplicationContext:: Bean Definition Names="
+ Arrays.toString(ctx.getBeanDefinitionNames()));
}
@Override
public void setBeanName(String beanName) {
System.out.println("setBeanName called");
System.out.println("setBeanName:: Bean Name defined in context="
+ beanName);
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("setBeanClassLoader called");
System.out.println("setBeanClassLoader:: ClassLoader Name="
+ classLoader.getClass().getName());
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
System.out.println("setResourceLoader called");
Resource resource = resourceLoader.getResource("classpath:spring.xml");
System.out.println("setResourceLoader:: Resource File Name="
+ resource.getFilename());
}
@Override
public void setImportMetadata(AnnotationMetadata annotationMetadata) {
System.out.println("setImportMetadata called");
}
@Override
public void setEnvironment(Environment env) {
System.out.println("setEnvironment called");
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("setBeanFactory called");
System.out.println("setBeanFactory:: employee bean singleton="
+ beanFactory.isSingleton("employee"));
}
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
System.out.println("setApplicationEventPublisher called");
}
}
Bean的作用域
概念
什麼是作用域呢?即“scope”,在面向對象程式設計中一般指對象或變量之間的可見範圍。而在Spring容器中是指其建立的Bean對象相對于其他Bean對象的請求可見範圍。
Spring提供“singleton”和“prototype”兩種基本作用域,另外提供“request”、“session”、“global session”三種web作用域;Spring還允許使用者定制自己的作用域。
預設情況下,Spring容器裝配的Bean都是單例的,也就是說,不管什麼情況下,在同一應用中通過Spring容器擷取的都是同一個對象,也就導緻了這個對象攜帶了很多可變的屬性,有時候會很不友善。
比如:我們通過ApplicationContext先後擷取BaseBean進行設值和取值,可以看到他們是同一個對象
@Component
public class BaseBean {
private String name="BaseBean";
}
public class ScopeTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentConfig.class);
//從Spring容器擷取BaseBean并進行屬性設定
BaseBean baseBean = applicationContext.getBean(BaseBean.class);
baseBean.setName("My Name");
//從Spring容器擷取BaseBean并進行屬性比對
BaseBean baseBean1 = applicationContext.getBean(BaseBean.class);
System.out.println(baseBean.equals(baseBean1));
System.out.println(baseBean1.getName());
}
}
測試結果:
true
My Name
分類
- singleton:指“singleton”作用域的Bean隻會在每個Spring IoC容器中存在一個執行個體,而且其完整生命周期完全由Spring容器管理。對于所有擷取該Bean的操作Spring容器将隻傳回同一個Bean。
GoF單例設計模式指“保證一個類僅有一個執行個體,并提供一個通路它的全局通路點”,
介紹了兩種實作:通過在類上定義靜态屬性保持該執行個體和通過系統資料庫方式。
- prototype:即原型,指每次向Spring容器請求擷取Bean都傳回一個全新的Bean,相對于“singleton”來說就是不緩存Bean,每次都是一個根據Bean定義建立的全新Bean。
GoF原型設計模式,指用原型執行個體指定建立對象的種類,并且通過拷貝這些原型建立新的對象。
-
Web應用中的作用域
配置方式和基本的作用域相同,隻是必須要有web環境支援,并配置相應的容器監聽器或攔截器進而能應用這些作用域。
- request作用域:表示每個請求需要容器建立一個全新Bean。比如送出表單的資料必須是對每次請求建立一個Bean來保持這些表單資料,請求結束釋放這些資料。
- session作用域:表示每個會話需要容器建立一個全新Bean。比如對于每個使用者一般會有一個會話,該使用者的使用者資訊需要存儲到會話中,此時可以将該Bean配置為web作用域。
- globalSession:類似于session作用域,隻是其用于portlet環境的web應用。如果在非portlet環境将視為session作用域。
- 自定義作用域
【第三章】 DI 之 3.4 Bean的作用域 ——跟我學spring3
限制Bean的作用域
Ⅰ、在自動化配置中,我們通過@Scope結合@Component注解限制被注解Bean的作用域
如下我們将BaseBean的作用域設定為原型作用域,即每次擷取都會重新建立一個新的BaseBean
@Component
@Scope("prototype")
public class BaseBean {
private String name="BaseBean";
}
//執行ScopeTest測試結果如下:
false //baseBean和baseBean1不是同一個BaseBean
BaseBean
其實,Spring将Bean的作用域常量封裝在ConfigurableBeanFactory和WebApplicationContext兩個類中,其代表常量如下:
- ConfigurableBeanFactory.SCOPE_SINGLETON =“singleton”;
- ConfigurableBeanFactory.SCOPE_PROTOTYPE =“prototype”;
- WebApplicationContext.SCOPE_SESSION = “session”;
- WebApplicationContext.SCOPE_REQUEST = “request”;
Ⅱ、在JavaConfig顯式配置中,我們通過@Scope注解結合@Bean注解限制被注解Bean的作用域
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public BaseBean baseBean{
return new BaseBean();
}
Ⅲ、在XML顯式配置中,我們通過限制bean節點的scope屬性限制裝配bean的作用域
<bean id="baseBean" class="com.my.spring.bean.BaseBean" scope="prototype">
<constructor-arg name="name" value="baseBeanOne"></constructor-arg>
</bean>
Spring事務管理
概述
事務是一系列操作組成的工作單元,該工作單元内的操作是不可分割的,即要麼所有操作都
做,要麼所有操作都不做,這就是事務。
舉個例子:
比如你去ATM機取1000塊錢,大體有兩個步驟:首先輸入密碼金額,銀行卡扣掉1000元錢;然後ATM出1000元錢。這兩個步驟必須是要麼都執行要麼都不執行。如果銀行卡扣除了1000塊但是ATM出錢失敗的話,你将會損失1000元;如果銀行卡扣錢失敗但是ATM卻出了1000塊,那麼銀行将損失1000元。是以,如果一個步驟成功另一個步驟失敗對雙方都不是好事,如果不管哪一個步驟失敗了以後,整個取錢過程都能復原,也就是完全取消所有操作的話,這對雙方都是極好的。
事務就是用來解決類似問題的。事務是一系列的動作,它們綜合在一起才是一個完整的工作單元,這些動作必須全部完成,如果有一個失敗的話,那麼事務就會復原到最開始的狀态,仿佛什麼都沒發生過一樣。
在企業級應用程式開發中,事務管理必不可少的技術,用來確定資料的完整性和一緻性。
事務的四個特性:ACID
- 原子性(Atomicity):即事務是不可分割的最小工作單元,事務内的操作要麼全做,要麼全不做;
- 一緻性(Consistency):在事務執行前資料庫的資料處于正确的狀态,而事務執行完成後資料庫的資料還是處于正确的狀态,即資料完整性限制沒有被破壞;如銀行轉帳,A轉帳給B,必須保證A的錢一定轉給B,一定不會出現A的錢轉了但B沒收到,否則資料庫的資料就處于不一緻(不正确)的狀态。
- 隔離性(Isolation):并發事務執行之間無影響,在一個事務内部的操作對其他事務是不産生影響,這需要事務隔離級别來指定隔離性;
- 持久性(Durability):事務一旦執行成功,它對資料庫的資料的改變必須是永久的,不會因比如遇到系統故障或斷電造成資料不一緻或丢失。
隔離級别與并發問題
隔離級别越高,資料庫事務并發執行性能越差,能處理的操作越少。是以在實際項目開發中為了考慮并發性能一般使用送出讀隔離級别,它能避免丢失更新和髒讀,盡管不可重複讀和幻讀不能避免,但可以在可能出現的場合使用悲觀鎖或樂觀鎖來解決這些問題。
隔離級别 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
未送出讀(Read uncommitted) | 可能 | 可能 | 可能 |
已送出讀(Read committed) | 不可能 | 可能 | 可能 |
可重複讀(Repeatable read) | 不可能 | 不可能 | 可能 |
序列化(Serializable) | 不可能 | 不可能 | 不可能 |
并發問題
當有多個事務并發執行時,并發執行就可能遇到問題,目前常見的問題如下:
- 丢失更新:兩個事務同時更新一行資料,最後一個事務的更新會覆寫掉第一個事務的更新,進而導緻第一個事務更新的資料丢失,這是由于沒有加鎖造成的;
- 髒讀:一個事務看到了另一個事務未送出的更新資料;
- 不可重複讀:在同一事務中,多次讀取同一資料卻傳回不同的結果;也就是有其他事務更改了這些資料;
- 幻讀:一個事務在執行過程中讀取到了另一個事務已送出的插入資料;即在第一個事務開始時讀取到一批資料,但此後另一個事務又插入了新資料并送出,此時第一個事務又讀取這批資料但發現多了一條,即好像發生幻覺一樣。
ps:不可重複讀與幻讀的差別
不可重複讀的重點是修改: 同樣的條件, 你讀取過的資料, 再次讀取出來發現值不一樣了 。
幻讀的重點在于新增或者删除: 同樣的條件, 第1次和第2次讀出來的記錄數不一樣 。
隔離級别
- 未送出讀(Read Uncommitted):最低隔離級别,一個事務能讀取到别的事務未送出的更新資料,很不安全,可能出現丢失更新、髒讀、不可重複讀、幻讀;
- 送出讀(Read Committed):隻能讀取到已經送出的資料。Oracle等多數資料庫預設都是該級别 (不重複讀);
- 可重複讀(Repeatable Read):保證同一事務中先後執行的多次查詢将傳回同一結果,不受其他事務影響,InnoDB預設級别,在SQL标準中,該隔離級别消除了不可重複讀,但是還存在幻象讀,但是innoDB解決了幻讀;
- 序列化(Serializable):最高隔離級别,性能最差,不允許事務并發執行,而必須串行化執行,最安全,每次讀都需要獲得表級共享鎖,讀寫互相都會阻塞。
ps:InnoDB解決幻讀的方法:基于MVCC(多版本并發控制)
在InnoDB中,會在每行資料後添加兩個額外的隐藏的值來實作MVCC,
這兩個值一個記錄這行資料何時被建立,另外一個記錄這行資料何時過期(或者被删除)。
Spring中七種事務傳播行為
事務傳播行為類型 | 說明 |
---|---|
保證多個操作在同一個事務中 | --------------------------------------------------------------------------------- |
PROPAGATION_REQUIRED | 如果目前沒有事務,就建立一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。 |
PROPAGATION_SUPPORTS | 支援目前事務,如果目前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY | 使用目前的事務,如果目前沒有事務,就抛出異常。 |
保證多個操作不在同一個事務中 | --------------------------------------------------------------------------------- |
PROPAGATION_REQUIRES_NEW | 建立事務,如果目前存在事務,把目前事務挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事務方式執行操作,如果目前存在事務,就把目前事務挂起。 |
PROPAGATION_NEVER | 以非事務方式執行,如果目前存在事務,則抛出異常。 |
嵌套式事務 | --------------------------------------------------------------------------------- |
PROPAGATION_NESTED | 如果目前存在事務,則在嵌套事務内執行。如果目前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。 |
事務傳播行為代碼驗證
Spring事務管理(詳解+執行個體)
Spring原理
轉自:Spring架構的基本原理分析
示例
講原理前,先看個例子:
小汽車類Car
package com.example;
public class Car {
private String name;
private double price;
// 省略getter,setter
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
public void carInfo() {
System.out.println("i have this car: " + name);
}
}
汽車擁有者類:Person
package com.example;
public class Person {
private String name;
private int age;
private Car car;
// 省略getter,setter
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", car=" + car +
'}';
}
}
Spring配置檔案spring-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="car" class="com.example.Car">
<property name="name" value="hongqi"/>
<property name="price" value="1000000.0"/>
</bean>
<bean id="person" class="com.example.Person">
<property name="name" value="zhaangsan"/>
<property name="age" value="27"/>
<property name="car" ref="car"/>
</bean>
</beans>
測試類SpringMain
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringMain {
public static void main(String[] args) {
// 建立IOC容器
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
// 通過getBean擷取Car的執行個體,這裡利用了反射
Car car = (Car) context.getBean("car");
car.carInfo();
System.out.println(car.toString());
Person person = (Person) context.getBean("person");
System.out.println(person.toString());
}
}
執行結果:
i have this car: hongqi
Car{name='hongqi', price=1000000.0}
Person{name='zhaangsan', age=27, car=Car{name='hongqi', price=1000000.0}}
原理分析
當執行new ClassPathXmlApplicationContext(“spring-config.xml”)這個動作時,Spring容器也即IOC容器随即建立。容器建立時,加載了儲存所有bean資訊的配置檔案spring-config.xml,此時bean被建立,并儲存到了記憶體中;随即通過context.getBean(“car”);擷取相應的bean的執行個體,這裡的car是配置檔案中< bean id=”car” …>。
在執行:
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
時,就已經将對應的屬性值設定到對象中了,這裡調用了setter方法或者構造器等。
這裡我們來簡單分析下,如何利用反射擷取到了執行個體。
在Spring配置檔案spring-config.xml中:
<bean id="car" class="com.example.Car">
<property name="name" value="hongqi"/>
<property name="price" value="1000000.0"/>
</bean>
首先容器讀取到了< bean … />節點,
1.讀取id屬性值,得到字元串“car”:
String idStr = "car";
2. 讀取class屬性,得到全限定名字元串“com.example.Car”:
String classStr = "com.example.Car";
3. 利用反射,通過全限定名擷取Class對象:
Class<?> clz = Class.forName(classStr);
4. 接着執行個體化對象
Object obj = clz.newInstance();
5. 加入到Spring容器中
// springContainer --> Map
springContainer.put(idStr, obj);
而若一個類需要另一個類時,如下:
<bean id="person" class="com.example.Person">
...
<property name="car" ref="car"/>
</bean>
解析< property …/>元素
1. 擷取name屬性值car:
String nameStr = "car";
2. 擷取ref屬性值car:
String refStr = "car";
3. 生成将要調用setter方法名 :
String setterName = "set" + nameStr.substring(0, 1).toUpperCase() + nameStr.substring(1);
4. 擷取Spring容器中名為refStr的Bean:
Object carBean = springContainer.get(refStr);
5. 擷取setter方法的Method類,此處的clz是前面執行個體通過Class.forName()獲得的類對象
下面是Person類中的setCar(Car)方法
public void setCar(Car car) {
this.car = car;
}
通過反射,第一個參數是方法名,第二個參數是方法參數類型
Method setter = clz.getMethod(setterName, carBean.getClass());
6. 調用invoke()方法,此處的obj是剛才反射代碼得到的Object對象
public Object invoke(Object obj, Object... args){...}
反射方法invoke中的參數,
obj:從中調用底層方法的對象(簡單的說就是調用誰的方法用誰的對象);
args:用于方法調用的參數,是以:
setter.invoke(obj, carBean);
到此,Spring架構原理基本介紹完畢。
常問面試題
Spring面試底層原理的那些問題,你是不是真的懂Spring?
Spring常見面試題總結(超詳細回答)