(源碼鏈接:https://pan.baidu.com/s/1i-afXQ2tBGded6t8Psea8g 提取碼:bh0r)
一、What is the Spring?
Spring框架是由於軟件開發的複雜性而創建的,換句話說:Spring是爲簡化軟件開發而存在的。Spring主要的作用可以概況爲:簡化開發,最大程度的松耦合。
爲什麼可以松耦合?這個要從下面的模型講起:
什麼是耦合?比如對象A的在實現其功能時需要用到對象B,那就說A依賴B,A和B之間就有耦合關係。耦合又會帶來啥?那就是如果修改其中一方,另一方就需要同步修改,如上圖Object A、Object B、Object C就是互相耦合的,3個齒輪環環相扣,缺一不可,若其中一個齒輪出現故障或改動,會直接影響其它2個齒輪的運轉。耦合度高的項目很不利於自身的更新、擴展、維護、部署,舉個不利於部署的例子:如果客戶只需要某個很簡單的功能,但因爲你的項目耦合度太高無法獨立劃分出來部署,那你就需要部署維護一整套資源,成本就太高了。其實,松耦合是我們一直追求的目標。
Spring實際上就是一種松耦合的方案,它作爲第三方,爲其他各自獨立的對象建立聯繫。如上圖中間的圖,Object A、Object B、Object C彼此獨立,他們的聯繫是由第三方Spring維繫的,這種模式的好處就是Object A、Object B、Object C無論哪個改變都不會影響其它的兩個。下面從代碼的角度來說明:
如果不使用Spring,按照原來的模式,service層調用DAO層,需要在service層new一個peopleDAO對象,再去調用該對象的方法,如下:
public class PeopleDAO {
public void add(People p){}
}
public class PeopleService {
public void insert(People p){
PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.add(p);
}
}
很顯然,若這樣處理service層和DAO層就有了很強的耦合關係,且service的每個需要調用DAO層對象的方法都需要new一個這個對象,那你將看到service層到處是new DAO層對象的語句。
若使用了Spring,那麼DAO層的對象只需要一次注入即可到處使用,這個對象交由Spring管理,如下:
public class PeopleService {
private PeopleDAO peopleDAO;
public void add(People p){
peopleDAO.add(p);
}
}
這裏Spring作爲第三方實現瞭解耦:
若未使用spring進行管理,service層在使用DAO層的對象時就是傳統的new這個對象,一旦DAO中的這個對象發生改變,比如構造方法等改變,那service中所有使用new這個構造方法的代碼都需要同步修改,這樣耦合性很大;若使用了spring,則service中要使用的對象是由spring注入的,即使DAO層對象改變,service層的代碼不需要修改,就實現瞭解耦。
二、Spring 架構
Spring框架是一種一站式框架,封裝了Web應用所需的所有層面。尤其是Spring對Bean對象,Dao組件對象,Service組件對象等管理能力是它的核心功能(雖然也具有處理持久層和web層的功能,但是一般交由更加專項的如SpringMVC、Mybatis等框架處理)。
spring也是一種容器,專門裝對象,貫穿web層、service層和dao層的,創建struts2中的action、springMVC中的controller、service中的javaBean、dao層hibernate中的session等。
三、Spring中的核心概念
1、IOC
(Inversion of Control),即控制反轉。簡單的來說,控制反轉就是反轉了對象的創建方式,由我們自己new創建反轉給了Spring。控制的方式有兩種:配置文件或註解,本文後面會詳細介紹這兩種實現IOC的方式。IOC是Spring的核心,貫穿始終。
2、DI
(Dependency Injection),即依賴注入。IOC時被調用對象交由Spring來創建,被調用對象注入調用者中的過程就是DI。在理解上DI不應該和IOC區別的太遠,兩者可以理解成一個概念,只是IOC偏重於原理,DI偏重於實現,或者說DI是實現IOC的方式。IOC的一個重點是在系統運行中,動態的向某個對象提供它所需要的其他對象,這一點是通過DI(Dependency Injection,依賴注入)來實現的。
所以這兩個概念可以理解成是一回事,IOC是目的,DI是實現方式。
3、AOP
(Aspect Oriented Programming),即面向切面編程。聽起來很抽象,但不要被這個嚇壞,通俗的講,比如:一個組件A,不關心其他常用的服務組件B,但是這個組件A使用組件B的時候,不是組件A自身調用,而是通過配置等其他方式,比如Spring中通過xml配置文件。這樣就使得A壓根就不需要知道服務組件B是怎樣的。A只關係自己的業務邏輯,具體A使用B的時候,配置文件去做,與具體的A組件無關。AOP就是實現上面所述過程的方式。
實際上Spring主要有兩個層面的動態:一是動態創建所需的對象,二是動態爲某個類去動態添加一些方法。第一種就是IOC,而第二種就是AOP。AOP的主要作用就是:不修改目標類的前提下,使用動態代理技術,去增強目標類的功能。
例如:你要在類A的某個方法被執行時添加事務的啓動與提交功能。若不使用AOP方式,唯一的實現方式就是在這個方法中加入事務功能代碼,這樣肯定能實現功能,但如果項目中有很多類似的方法都要添加一樣的事務功能,你就需要每個方法中都加上類似的事務相關代碼,項目中會出現大量重複代碼並且增加了開發工作量。
這時你會想,要是像添加事務、添加日誌這類頻繁出現的功能不要一遍一遍重複的在業務代碼裏面寫就好了,而是某個類的某個方法需要的時候就動態自動添加,這樣這個業務類就和事務等實現瞭解耦,業務類只關心業務。
AOP就是來幹這樣的事的,可以不用修改這個類,就能爲這個類增加額外的功能。AOP專門用於處理系統中分佈於各個模塊中的交叉關注問題,或者說來處理具有橫切性質的系統級服務,如事務管理、安全檢查、日誌記錄、緩存、對象池管理等。利用AOP可以對業務邏輯的各個部分進行隔離,使得業務邏輯各個部分之間的耦合度降低。
本文後面會詳細介紹AOP是如何實現這樣的過程的,這裏只需要知道AOP能幹什麼就行了。
以上IOC和AOP可以通過xml配置文件實現,也可以通過註解實現。
四、Spring創建bean的過程
Spring中有兩種容器對象:ApplicationContext和BeanFactory
BeanFactory已經過時,且ApplicationContext已經覆蓋BeanFactory所有接口,因此實際開發時,ApplicationContext來創建bean對象。
下面通過xml配置文件的形式演示下流程:
- 將要創建bean對象的實體類寫進.xml文件
- 根據配置創建Spring容器對象,從容器對象中取得需要的bean對象
- 結果
Spring裝配Bean有三種方式:
1.在XML中顯示配置()
2.在Java的接口和類中實現配置
3.隱式Bean的發現機制和自動裝配原則
往往是三種自由配合使用,優先使用隱式Bean的發現機制和自動裝配原則,因爲基於約定優於配置的原則,第三種可以減少繁重的配置;其次可以選擇在Java的接口和類中實現配置,目的也是減少XML配置文件;在無法前面兩種時可以選擇XML去配置Ioc,比如你要配置的類的bean不是自己工程內的就可以使用XML配置。
4.1 Bean的作用域
1.單例(singleton):默認選項,整個過程中Spring只爲其生成一個Bean實例;
2.原型(prototype):每次注入或者獲取Bean時,Spring都會爲其創建新的Bean實例;
3.會話(session):在Web應用中使用,在會話中只創建一個實例;
4.請求(request):在Web應用中使用,每請求一次就創建一個實例。
五、DI(依賴注入)的方式
Sping的注入方式可以分爲三種:
1.set注入
- 基本數據類型
- 引用類型(對象)
- 複雜類型(數組,List,Map,Properties)
2.構造方法注入
- 基本數據類型
- 引用類型(對象)
- 複雜類型(數組,List,Map,Properties)
3.接口注入
在類中,對於依賴的接口,通過具體的實現類名,動態的加載並強制轉換成其接口類型。接口注入不常用,只有在有些特定環境下使用,比如數據庫鏈接資源不在項目內,比如配置在Tomcat中時,就可以通過JNDI的形式去獲取。接口注入模式因爲歷史較爲悠久,在很多容器中都已經得到應用。但由於其在靈活性、易用性上不如其他兩種注入模式,因而在 IOC 的專題世界內並不被看好。不是重點,不需去研究。
注意:構造器注入和setter注入都是通過java的反射技術得以實現的。
1、set注入
set注入是spring中最主流的注入方式, setter注入是通過setter方法注入,首先將構造方法設置爲無參的構造方法,然後利用setter注入爲其設置新的值,實際就是利用java的反射技術實現的。
1)基本類型注入
ApplicationContext.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">
<!--
name屬性: 填寫你要注入的字段名稱
value屬性:你要注入的字段名稱對應的值
-->
<bean name="people" class="com.wo.domain.People">
<property name="name" value="熊大"></property>
<property name="age" value="18"></property>
</bean>
</beans>
public class DemoTest {
@Test
public void test2() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
People people = (People) context.getBean("people");
System.out.println(people);
}
}
People{age=18, name='熊大'}
注意:set注入要求這個屬性set方法要存在不然會。報錯,如下把People類中name屬性的set方法註釋之後,ApplicationContext.xml就會報錯:0
public class People {
private int age;
private String name;
private Ball ball;
public String getName() {
return name;
}
// public void setName(String name) {
// this.name = name;
// }
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Ball getBall() {
return ball;
}
public void setBall(Ball ball) {
this.ball = ball;
}
@Override
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
", ball=" + ball +
'}';
}
}
2)引用類型注入
總體來說分爲兩步:
- 需要先將要注入的引用對象使用Spring容器創建出來;
- 再將該引用對象注入進來,使用ref
public class Ball {
private String name;
private int size;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public String toString() {
return "Ball{" + "name='" + name + '\'' + ", size=" + size + '}';
}
}
public class People {
private int age;
private String name;
private Ball ball;
public Ball getBall() {
return ball;
}
public void setBall(Ball ball) {
this.ball = ball;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "People{" + "age=" + age + ", name='" + name + '\'' + '}';
}
}
3)複雜類型注入(數組、List、Map、Properties)
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Feng, Ge 2020/2/19 19:59
*/
public class CollectionDemo {
private Object[] arr;
private List list;
private Map map;
private Properties properties;
public Object[] getArr() {
return arr;
}
public void setArr(Object[] arr) {
this.arr = arr;
}
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "CollectionDemo{" +
"arr=" + Arrays.toString(arr) +
", list=" + list +
", map=" + map +
", properties=" + properties +
'}';
}
}
xml文件:
<bean name="collection" class="com.wo.domain.CollectionDemo">
<property name="arr">
<array>
<value>你大爺</value>
<value>你二大爺</value>
</array>
</property>
<property name="list">
<list>
<value>DOTA</value>
<value>war</value>
<ref bean="myBall"></ref>
</list>
</property>
<property name="map">
<map>
<entry key="price" value="9.9"></entry>
<entry key="address" value="地球"></entry>
</map>
</property>
<property name="properties">
<props>
<prop key="id">ksodsodkosdosodko</prop>
</props>
</property>
</bean>
@Test
public void test() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
CollectionDemo collectionDemo = (CollectionDemo) context.getBean("collection");
System.out.println(collectionDemo);
}
CollectionDemo{arr=[你大爺, 你二大爺], list=[DOTA, war, Ball{name='FootBall', size=12}], map={price=9.9, address=地球}, properties={id=ksodsodkosdosodko}}
注意:Properties本質就是Map
2、構造方法注入
構造器注入是通過構造方法注入。不需要被注入類實現set方法,只需要有構造方法,下面展示基本類型、引用類型、複雜類型的構造器注入過程:
import java.util.List;
/**
* Feng, Ge 2020/2/19 20:36
*/
public class ConStructorDemo {
private String name;
private List list;
private Ball ball;
public ConStructorDemo(String name, List list, Ball ball) {
this.name = name;
this.list = list;
this.ball = ball;
}
@Override
public String toString() {
return "ConStructorDemo{" +
"name='" + name + '\'' +
", list=" + list +
", ball=" + ball +
'}';
}
}
以上並未實現set方法,只有構造方法。
<bean name="construction" class="com.wo.domain.ConStructorDemo">
<constructor-arg index="0" type="java.lang.String" value="構造器注入"></constructor-arg>
<constructor-arg index="1" type="java.util.List">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</constructor-arg>
<constructor-arg index="2" type="com.wo.domain.Ball" ref="myBall"></constructor-arg>
</bean>
@Test
public void testConstruction() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
ConStructorDemo conStructorDemo = (ConStructorDemo) context.getBean("construction");
System.out.println(conStructorDemo);
}
ConStructorDemo{name='構造器注入', list=[1, 2, 3], ball=Ball{name='FootBall', size=12}}
注意:這裏index指定構造方法參數順序,要和構造方法定義一致。
set注入和構造方法注入的優缺點比較:
-
使用set注入的理由:
- 1.若Bean有很多的依賴,那麼構造方法的參數列表會變的很長
- 2.若一個對象有多種構造方法,構造方法會造成代碼量增加
- 3.若構造方法中有兩個以上的參數類型相同,那麼將很難確定參數的用途
-
使用構造方法注入的理由:
- 1.構造方法注入使用強依賴規定,如果不給足夠的參數,對象則無法創建
- 2.對於依賴關係無須變化的Bean,構造注入更有用處;因爲沒有setter方法,所有的依賴關係全部在構造器內設定,因此,不用擔心後續代碼對依賴關係的破壞。
建議:
這兩種依賴注入的方式,並沒有絕對的好壞,只是適應的場景有所不同。但Spring官方更推薦使用set注入,即採用以設值注入爲主,構造注入爲輔的注入策略。對於依賴關係無需變化的注入,儘量採用構造注入;而其他的依賴關係的注入,則考慮採用設值注入。
六、AOP的7個專業術語
前面已經介紹過AOP的概念,AOP的主要作用就是在不修改目標類的基礎上,使用動態代理技術,去增強目標類的功能 。
這樣直接說可能不能很快的去理解,下面看一個需求場景:
假如現在有一個類,要求你在這個執行這個類裏的每個方法的前後都加上事務開啓與事務提交。例如下面的類:
/**
* Feng, Ge 2020/2/19 21:32
*/
public class AopDemo {
public void add(){
System.out.println("執行增加!");
}
}
現在要求你在執行add()方法前開啓事務,執行add()後提交事務,當然你可以如下面的方式來實現:
public class AopDemo {
public void add(){
System.out.println("開啓事務!");
System.out.println("執行增加!");
System.out.println("提交事務!");
}
}
以上雖然可以完成我們的需求,但是仔細考慮就會有問題:(1)現在AopDemo類只有一個add()方法,假如AopDemo類有很多方法或者別的類的很多方法也要實現同樣的添加事務的功能,像這樣在每個方法裏面寫"開啓事務!"和"提交事務!"的方式會造成大量重複代碼,帶來很大的工作量;(2)直接在AopDemo中編寫代碼破壞了AopDemo的獨立性,使得這個類與事務相關的類產生耦合,一方改動會影響另一方。
基於以上分析,我們想要是在不修改AopDemo類代碼的前提下,就能給AopDemo類增加事務功能就好了,既不影響AopDemo類的業務,使其保持獨立,又能添加想要的功能。AOP就是來幹這件事的,AOP把上面的場景分成三方:目標類、增強(通知)、代理類
上面場景中的AopDemo類就是目標類(Target),這裏事務相關的代碼就是增強,代理類由Spring提供。AOP中重要的專業術語如下:
- 目標類(Target):增強邏輯的織入目標類,例如:AopDemo類
- 連接點(Joinpoint):可能被Spring攔截的點(方法),例如:AopDemo類中的update()方法
- 切入點(Pointcut):已經被增強的連接點(方法),例如:AopDemo類中的add()方法
- 增強/通知(Advice):那些增強的代碼,例如:AdviceDemo類中的方法
- 織入(Weaving):把增強advice應用到目標類target來創建新的代理對象procxy的過程
- 代理(Proxy):一個類被AOP織入增強後,就產出了一個結果類,這個結果類就是融合了原類和增強邏輯的代理類
- 切面(Aspect):切入點和增強的結合。通知說明了幹什麼和什麼時候幹(什麼時候通過方法名中的befor,after,around等就能知道),切入點說明了在哪幹(指定到底是哪個方法),這就是一個完整的切面定義。
七、Spring實現AOP的過程
1、 編寫目標類
/**
- 目標類
- Feng, Ge 2020/2/19 21:32
*/
public class AopDemo {
public void add() {
System.out.println("執行增加!");
}
public void update() {
System.out.println("執行修改!");
}
public void delete() {
System.out.println("刪除!");
}
}
2、 編寫增強
/**
- 增強
- Feng, Ge 2020/2/19 21:57
*/
public class AdviceDemo {
public void before(){
System.out.println("開啓事務!");
}
public void after(){
System.out.println("提交事務!");
}
}
3、配置xml
結果:
開啓事務!
執行增加!
提交事務!
注意:
1、使用AOP功能時,ApplicationContext.xml頭文件要增加Aop相關規範及資源
- xmlns XML NameSpace的縮寫,初始化bean的格式文件地址,來區分此xml文件不同於別的xml
- xmlns:xsi 指定了xml所使用的Schema(模式)定義的語言及需要遵循的規範
- xmlns:aop 使用spring框架的aop 面向切面編程時,在xml文件中引入aop資源
- xsi:context:關於spring上下文,包括加載資源文件
- xmlns:tx spring 事務配置
- xsi:schemaLocation 引入所有xml本文檔需要遵循的規範 解析器在需要的情況下對該屬性引入的文檔進行校驗,第一個值表示命名空間,第二個值表示該命名空間的模式文檔的具體位置,其中命名空間和對應的 xsd 文件的路徑,必須成對出現。比如用到的 、、 等命名空間,都需要在這裏聲明其 xsd 文件地址。
2、需要導入aspectjweaver.jar包
3、AOP的切入點表達式
<aop:pointcut expression=“execution(表達式)” id=“pCut”/>
- public void com.wo.domain.AopDemo.add() ==》具體的
- public void com.wo.domain.AopDemo.add(…) ==》切入點的方法參數不固定
- public void com.wo.domain.AopDemo.*add(…) ==》切入點的方法開頭不確定
在Spring中有4種實現AOP的方式:
1.使用ProxyFactoryBean和對應的接口實現AOP
2.使用XML配置AOP
3.使用@AspectJ註解驅動切面
4.使用AspectJ注入切面
八、AOP增強/通知的五種類型
- before 前置通知
目標方法運行之前調用 - after-returning 後置通知
目標方法運行之後調用,但出現異常則不會調用 - around 環繞通知
在目標方法之前和之後都調用 - after-throwing 異常攔截通知
出現異常就會調用 - after 最終通知
目標方法運行之後調用,無論有無異常都會調用
/**
* 目標類
* Feng, Ge 2020/2/19 21:32
*/
public class AopDemo {
public void add() {
// 手動產生異常
int i = 6/0;
System.out.println("執行增加!");
}
public void update() {
System.out.println("執行修改!");
}
public void delete() {
System.out.println("刪除!");
}
}
/**
* 增強
* Feng, Ge 2020/2/19 21:57
*/
public class AdviceDemo {
public void before() {
System.out.println("開啓事務!");
}
public void after() {
System.out.println("提交事務!");
}
public void afterExp() {
System.out.println("出現異常會執行我!");
}
}
@Test
public void testAop() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
AopDemo aopDemo = (AopDemo) context.getBean("aopDemo");
aopDemo.add();
}
結果:
jionpoint參數:
在每個增強方法中,都可以接收一個Joinpoint類型參數,主要包含兩個方法:
- getTarget():獲得被代理的目標對象
- getSignature():獲取被代理目標類中的目標方法
/**
* 增強
* Feng, Ge 2020/2/19 21:57
*/
public class AdviceDemo {
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getTarget());
System.out.println(joinPoint.getSignature());
System.out.println(joinPoint.getSignature().getName());
System.out.println("開啓事務!");
}
}
com.wo.domain.AopDemo@395b56bb
void com.wo.domain.AopDemo.add()
add
開啓事務!
執行增加!
提交事務!
九、Spring中的註解
利用註解也可以實現一下目標:
- IOC
- DI
- AOP
就是利用註解取代xml配置。
但是並不是xml文件就不需要了,使用註解時需要先在xml文件中添加註解相關約束加載相關資源,並開啓註解掃描,即告訴Spring要以註解的方式創建bean。
一般不推薦XML的方式裝配Bean,更多時候會使用註解的方式去裝配bean,註解不僅提供了XML的功能,也提供了自動裝配功能,採用註解遵循了“約定優於配置”的思想,開發者要做的決斷更少。
1、註解實現IOC
具體配置說明如下:
@Component(value = "people")
public class People {
private int age;
private String name;
private Ball ball;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Ball getBall() {
return ball;
}
public void setBall(Ball ball) {
this.ball = ball;
}
@Override
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
", ball=" + ball +
'}';
}
}
@Test
public void test2() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
People people = (People) context.getBean("people");
System.out.println(people);
}
People{age=0, name='null', ball=null}
這裏xml中並沒有配置標籤,但是依然可以實現bean的創建,這就是註解方式。@Componment就是把普通pojo實例化到spring容器中,相當於配置文件中的 < bean id="" class=""/>
注意:
Spring中的@Componment有三個衍生註解:
- @Controller ----> Web層
- @Service ----> 業務層
- @Repository ----> 持久層(DAO)
這3個和@Componment的作用是一樣的,這4個註解都可以完成bean的創建,但是後3個可以更清晰的展示各層級,利於項目的機構清晰和代碼可讀性,項目開發中各層最好使用各層專有的註解形式。
2、註解實現DI
註解實現DI就可以不再提供set或者構造器,分爲以下幾種:
- 普通類型注入,使用@Value
- 對象注入,使用@AutoWired(按類型自動裝配)或者@Qualifier(強制使用名稱注入);還可以使用@Resource,它是Java提供的,但是Spring也支持這種註解形式,相當於@AutoWired和@Qualifier一起使用
@Component(value = "people")
public class People {
@Value("16")
private int age;
@Value("ohou")
private String name;
@Autowired
private Ball ball;
// 不在需要set()方法
// public String getName() {
// return name;
// }
//
// public void setName(String name) {
// this.name = name;
// }
//
// public int getAge() {
// return age;
// }
//
// public void setAge(int age) {
// this.age = age;
// }
//
// public Ball getBall() {
// return ball;
// }
//
// public void setBall(Ball ball) {
// this.ball = ball;
// }
@Override
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
", ball=" + ball +
'}';
}
}
@Component
public class Ball {
@Value("好球")
private String name;
private int size;
@Override
public String toString() {
return "Ball{" + "name='" + name + '\'' + ", size=" + size + '}';
}
}
@Test
public void test2() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
People people = (People) context.getBean("people");
System.out.println(people);
}
結果:
People{age=16, name='ohou', ball=Ball{name='好球', size=0}}
3、註解實現AOP
1、首先是開啓AOP掃描:
aop:aspectj-autoproxy</aop:aspectj-autoproxy>
2、目標類添加註解(創建bean)
@Component
public class AopDemo {
public void add() {
System.out.println("執行增加!");
}
public void update() {
System.out.println("執行修改!");
}
public void delete() {
System.out.println("刪除!");
}
}
3、增強添加註解(創建bean、配置切面(配置切入點、通知類型))
@Component
@Aspect
public class AdviceDemo {
@Before(value = "execution(public void add())")
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getTarget());
System.out.println(joinPoint.getSignature());
System.out.println(joinPoint.getSignature().getName());
System.out.println("開啓事務!");
}
@After(value = "execution(public void add())")
public void after() {
System.out.println("提交事務!");
}
public void afterExp() {
System.out.println("出現異常會執行我!");
}
}
@Test
public void testAop() {
// 1、默認從src下加載.xml,根據配置文件創建 容器對象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、從容器對象中取得 需要的 bean對象
AopDemo aopDemo = (AopDemo) context.getBean("aopDemo");
aopDemo.add();
}
com.wo.domain.AopDemo@44c79f32
void com.wo.domain.AopDemo.add()
add
開啓事務!
執行增加!
提交事務!
十、Spring中的事務管理
1、事務的特性
先回顧一下事務的特性(ACID):
- 原子性(Atomicity):操作這些指令時,要麼全部執行成功,要麼全部不執行。只要其中一個指令執行失敗,所有的指令都執行失敗,數據進行回滾,回到執行指令前的數據狀態。
- 一致性(Consistency):事務的執行使數據從一個狀態轉換爲另一個狀態,但是對於整個數據的完整性保持穩定。
拿轉賬來說,
假設用戶A和用戶B兩者的錢加起來一共是20000,
那麼不管A和B之間如何轉賬,轉幾次賬,
事務結束後兩個用戶的錢相加起來應該還得是20000,
這就是事務的一致性。
- 隔離性(Isolation):隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後纔開始,這樣每個事務都感覺不到有其他事務在併發地執行。 - 持久性(Durability):當事務正確完成後,它對於數據的改變是永久性的。
例如我們在使用JDBC操作數據庫時,在提交事務方法後,
提示用戶事務操作完成,當我們程序執行完成直到看到提示後,
就可以認定事務已經正確提交,即使這時候數據庫出現了問題,
也必須要將我們的事務完全執行完成,
否則就會造成我們看到提示事務處理完畢,
但是數據庫因爲故障而沒有執行事務的重大錯誤。
如果違背以上原則就很可能引起下面的問題:
- 髒讀:髒讀是指在一個事務處理過程裏讀取了另一個未提交的事務中的數據。
小明的銀行卡餘額裏有100元。現在他打算用手機點一個外賣飲料,
需要付款10元。但是這個時候,他的女朋友看中了一件衣服95元,
她正在使用小明的銀行卡付款。於是小明在付款的時候,
程序後臺讀取到他的餘額只有5塊錢了,根本不夠10元,
所以系統拒絕了他的交易,告訴餘額不足。
但是小明的女朋友最後因爲密碼錯誤,無法進行交易。
小明非常鬱悶,明明銀行卡里還有100元,怎麼會餘額不足呢?
- 幻讀也叫虛讀:一個事務執行兩次查詢,第二次結果集包含第一次中沒有或某些行已經被刪除的數據,造成兩次結果不一致,只是另一個事務在這兩次查詢中間插入或刪除了數據造成的。幻讀是事務非獨立執行時發生的一種現象。
例如事務T1對一個表中所有的行的某個數據項做了從“1”修改爲“2”的操作,
這時事務T2又對這個表中插入了一行數據項,
而這個數據項的數值還是爲“1”並且提交給數據庫。
而操作事務T1的用戶如果再查看剛剛修改的數據,
會發現還有一行沒有修改,其實這行是從事務T2中添加的,
就好像產生幻覺一樣,這就是發生了幻讀。
- 不可重複讀:一個事務兩次讀取同一行的數據,結果得到不同狀態的結果,中間正好另一個事務更新了該數據,兩次結果相異,不可被信任。
例如事務T1在讀取某一數據,
而事務T2立馬修改了這個數據並且提交事務給數據庫,
事務T1再次讀取該數據就得到了不同的結果,發送了不可重複讀。
注意:
1.不可重複讀和髒讀的區別:髒讀是某一事務讀取了另一個事務未提交的髒數據,而不可重複讀則是讀取了前一事務提交的數據。
2.幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點就髒讀不同),所不同的是不可重複讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。
2、事務的操作步驟
1.開啓事務
2.提交事務
3.回滾事務(發生異常時)
3、事務隔離級別
1.髒讀
2.讀/寫提交
3.可重複讀
4.序列化
在互聯網應用中不僅要考慮數據庫的一致性,還要考慮系統的性能,由1到4,數據一致性是逐漸增強的,但是性能卻是下降的,所以在選擇隔離級別時要綜合考慮數據一致性和性能。一般會選擇“讀/寫提交”,可以防止髒讀又能兼顧性能。在實際工作中,使用 @Transactional註解,它在不同的數據庫中,隔離級別也是不同的,Mysql中支持這4種級別,且默認是第三種“可重複讀”級別,在Oracle中只支持讀/寫提交和序列化兩種,默認是讀/寫提交。
4、傳播行爲
傳播行爲是指方法之間調用事務策略的問題。
5、事務舉例
傳統處理事務時都是利用try…catch…finally進行分步處理:
public void saveRecord() {
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(new Configuration());
SqlSession sqlSession = null;
try {
sqlSession = defaultSqlSessionFactory.openSession();
System.out.println("處理完第一個事務");
System.out.println("處理完第二個事務");
// 多件事完成後,開始提交
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
// 遇到異常事務回滾
sqlSession.rollback();
} finally {
// 關閉資源
if (!(sqlSession == null)) {
sqlSession.close();
}
}
}
若有大量的try…catch必定會使人眼花繚亂,於是Spring給我們提供了一個處理事務的註解@Transactional,上面的代碼可以用下面的代碼替換:
@Transactional
public void saveRecord() {
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(new Configuration());
SqlSession sqlSession = defaultSqlSessionFactory.openSession();
System.out.println("處理完第一個事務");
System.out.println("處理完第二個事務");
}
這段代碼沒有任何開啓關閉提交回滾等操作,但是卻達到了上面一樣的效果,且更加簡潔易維護。實際上@Transactional就是Spring AOP的典型應用。