spring6合集——Spring6核心知識點總結
- 啟示錄
- 一、SOLID原則
- 1. 單一職責原則(SRP)
- 2. 開閉原則(OCP)
- 3. 里氏替換原則(LSP)
- 4. 接口隔離原則(ISP)
- 5. 依賴倒置原則(DIP)
- 二、控制反轉IOC(Inversion of Control)
- 1. 為什么會出現控制反轉
- 2. 依賴注入(DI)
- 2.1 set注入
- 2.2 構造方法注入
- 3. set注入專題
- 3.1 外部注入Bean
- 3.2 內部注入Bean
- 3.3 注入簡單類型的屬性
- 3.5 注入數組
- 3.6 set注入List集合
- 3.8 set注入Map集合
- 3.9 set注入Properties
- 3.10 set注入null和空串
- 3.11 set注入特殊字符串
- 4. p命名空間注入
- 5. c命名空間注入
- 6. util命名空間
- 7. 自動裝配
- 7.1 根據名稱自動裝配
- 7.2 根據類型自動裝配
- 8. Spring引入外部屬性配置文件
- 三、Bean的作用域
- 1. 單例模式(singleton)
- 2. 原型作用域(property)
- 3. 其它作用域
- 四、GoF之工廠模式
- 1. 工廠模式的三種形態
- 2. 簡單工廠模式
- 3. 工廠方法模式
- 5. 抽象工廠模式
- 6. 三種工廠模式對比(重點:exclamation:)
- **對比總結**
- 五、Bean的實例化方式
- 1. 構造方法實例化
- 2、、通過簡單工廠模式實例化
- 3. 通過factory-bean實例化
- 4. 通過FactoryBean接口實例化
- 5. BeanFactory和FactoryBean的區別
- 6. 為什么要學習Bean的不同創建方式
- 六. Bean的生命周期
- 1. Bean生命周期之5步
- 2. Bean生命周期之7步
- 3. Bean生命周期之10步
- 七、Bean的循環依賴
- 1. 什么是循環依賴
- 2. singleton下的set注入
- 3. property下的set注入
- 4. singleton+構造注入
- 5. Spring解決循環依賴的原理和實現
- 6. 總結
- 八、注解開發
- 1. 核心組件注解
- 2. 依賴注入相關注解
- 九、GOF之代理模式
- 1. 靜態代理
- 2. 動態代理(Proxy類)
- 2.1 JDK動態代理
- 2.2 CgLib動態代理
- 十、Aop切面編程
- 1. Aop介紹
- 2. Aop的七大術語
- 2.1 連接點( Joinpoint)
- 2.2 切點(Pointcut)
- 2.3 通知(Advice)
- 2.4 切面(Aspect)
- 2.5 織入(Weaving)
- 2.6 代理對象(Proxy)
- 2.7 目標對象(Target)
- 3. 切點表達式
- 4. 使用Spring的Aop編程
- 4.1 準備工作
- 4.2 核心步驟
- 4.3 通知類型
- 4.4 切面的執行順序
- 4.5 簡化切點表達式
- 十一、事務
- 1. 事務概述
- 2. Spring對事務的支持(@Transactional)
- 3. 事務的屬性
- 3.1 屬性的分類
- 3.2 事務傳播行為
- Spring事務傳播行為選擇指南
- 使用說明:
- 3.3 事務的隔離級別
- 事務隔離級別詳解
- 隔離級別選擇建議:
- 在Spring中使用事務的隔離性
- 3.4 事務超時
- 3.5 只讀事務
- 3.6 哪些異常回滾事務
- 3.7 哪些異常不會滾事務
- 下期預告
啟示錄
???同志們大家好,以及親愛的作者你好,今天2025年6月19日開啟spring6的新篇章,由于之前已經系統學習過spring6,但是礙于沒有做筆記,只是跟著寫了代碼,對于很多的知識點又忘記啦。因此今天開始全面記錄和學習spring6,加深記憶, gogogo出發嘍!🎉🎉🎉
???本文主要參考資料為動力節點老杜的spring6
課程,B站可以直接搜到課程,非常的nice,同時也有筆記,我的思路是跟著筆記挑出重點進行總結和復習
,出發點可能略有不同,以下是筆記的地址。
https://www.yuque.com/dujubin/ltckqu/kipzgd?singleDoc#p1WjS
一、SOLID原則
???SOLID是面向對象設計
(Object-Oriented Design, OOD)的五大基本原則的縮寫,這些原則旨在提高軟件的?可維護性、?可擴展性、?可復用性和?靈活性。
???有點子抽象,一直再說OCP、IOC,是不是還不知道這是五大設計原則中的成員啊!
縮寫 | 全稱 | 中文含義 |
---|---|---|
S | Single Responsibility Principle | 單一職責原則 |
O | Open/Closed Principle | 開閉原則 |
L | Liskov Substitution Principle | 里氏替換原則 |
I | Interface Segregation Principle | 接口隔離原則 |
D | Dependency Inversion Principle | 依賴倒置原則 |
1. 單一職責原則(SRP)
一個類只能有一個引起它變化的原因,或者說一個類只負責一項職責,只干一類工作
含義 🔍
- 一個類或方法只干一類事情
- 每個模塊或組件只負責一類事情
示例 ?
public class UserController {public User getUserInfo();public List<User> getUserInfo();}
public class DeptController {public Dept getDeptInfo();public List<Dept> getDeptInfo();}
錯誤示例 ?
public class UserController {public User getUserInfo();public Dept getDeptInfo();}
優點 💡
- 提高代碼可讀性
- 降低耦合度
- 易于維護和測試
???上面的例子很清晰,針對于類來說,SRP原則比較容易實行,在正式的編碼中對于不同的業務都有不同的Java類
進行實現,當然也有為了方便,在一個類中存在多個類型的業務功能。我只能說,盡量盡量避免,一個優秀的程序員不能這樣干。
???但是在方法中,這個例子就不太好舉了,因為可能我們的業務需求就需要整合多項數據并分析得出結論
,那SRP原則不就沖突了嗎?我們根據它的設計初衷進行分析,就是為了降低代碼耦合度啊和可讀性這些,其實在方法中,我們謹記,如果有重復的代碼,提出來成為一個方法
,減少耦合度,同時對于有規律的代碼塊或者說目標明確的代碼塊
,同時又比較臃腫,這類代碼我們大多數情況下是為了得出一個結論或者一批數據
,過程和其它代碼沒有關系,那這個方法就可以提出來,雖然它僅你調用,這樣可讀性就會很高,后面定位問題也非常好定位。
2. 開閉原則(OCP)
對擴展開放,對修改關閉
含義 🔍
- 類、函數、模塊應通過擴展來支持或增加新行為,而不是通過修改代碼。
? 示例:
interface Shape {double area();
}class Rectangle implements Shape {public double area() { return width * height; }
}class Circle implements Shape {public double area() { return Math.PI * radius * radius; }
}
新增圖形只需添加新的子類,無需修改原有邏輯
💡 優點:
- 提高系統可擴展性
- 避免因修改導致的副作用
???對于OCP原則來說核心就是:別改,你就新增。
???其實OCP原則才是我們在開發中最容易忽略的點,你一旦動了別人的代碼,或者說你自己曾經寫過的代碼,這就意味了需要全面測試
,不要說你有絕對的自信,你永遠不會知道線上會因為什么報錯,這都是經驗之談。所以在后面的代碼中,能盡量將新的方法嵌入到之前的代碼
或者說直接新增加一個方法
(前提是一個全新的功能,避免耦合度過高),如果情況所迫,其實我們還是得在原有基礎上進行修改,對于你不知道或者沒有完全把握的代碼,不要去動,在下面寫就行了。
3. 里氏替換原則(LSP)
子類型必須能夠替換其基類型。
🔍 含義:
- 所有引用基類的地方必須能夠透明的使用其子類的對象。
- 子類不能違反父類的行為契約。
📌 關鍵約束
-
子類方法參數類型應比父類更寬松
-
子類返回值類型應比父類更嚴格
-
子類不應拋出父類未聲明的異常
💡 優點:
- 提高代碼的健壯性和可重用性
- 支持多態和接口編程
錯誤示例?
class Rectangle {protected int width, height;void setWidth(int w) { width = w; }void setHeight(int h) { height = h; }
}class Square extends Rectangle {// 破壞父類行為約束void setWidth(int w) { width = height = w; }
}
正確示例?
interface Shape {int getArea();
}class Rectangle implements Shape { /*...*/ }
class Square implements Shape { /*...*/ }
???對于LSP原則我的理解是,在正常的程序開發中,我們盡量使用接口繼承的方式,定義一個通用的接口,讓業務組件都實現這個接口,從而達到LSP原則。但是這個原則我們其實用到的并不多,大多數體現在Java的源碼中,或者第三方框架的源碼中,就比如Map,HashMap,LinkedHashMap
這種,父子類有嚴格的要求,HashMap的功能更加復雜,返回值更加詳細,但又沒有突破父級的限制,這就是典型的LSP原則。
4. 接口隔離原則(ISP)
客戶端不應該被迫依賴于他們不使用的接口
🔍 含義:
- 大而全的接口應該拆分為
更小、更具體
的接口。 - 客戶端只需知道它們
實際使用的方法
。 - 減少不必要的依賴和耦合
💡 優點:
- 提高代碼的可讀性
???對于ISP原則在最新的springCloud
項目中非常的典型,我們使用的都是Controller、Service、ServiceImpl、Mapper、Dao
等不同的業務包,一般客戶端使用的都是Service中的接口,不需要關注內部實現,同時Service下面如果調用數據庫的話,也只需要調用Mapper中的接口即可,不需要知道其內部實現
,這樣的拆分非常的清晰,在大公司中是分Java開發和數據庫開發人員的,可能Java開發不需要知道數據庫內部怎么實現,只需要調用對應的接口即可。
???同時也需要注意,如果你的一個接口需要多個實現類繼承,但是其中只有某些接口是共有的,這時候雙方可能會有多余的方法繼承,這時候可以使用抽象類,也就是public abstract interface
,可以選擇性繼承。
錯誤示例?
// 違反ISP
interface Worker {void work();void eat();void sleep();
}class Robot implements Worker {void work() { /*...*/ }void eat() { /* 機器人不需要吃飯 */ }void sleep() { /* 機器人不需要睡覺 */ }
}// 遵循ISP
interface Workable {void work();
}interface Eatable {void eat();
}interface Sleepable {void sleep();
}class Human implements Workable, Eatable, Sleepable {// 實現所有方法
}class Robot implements Workable {// 只實現需要的方法
}
5. 依賴倒置原則(DIP)
高層模塊不應該依賴于底層模塊,兩者都應該依賴于抽象,抽象不應該依賴于細節,細節應該依賴于抽象
???上面聽起來花里胡哨的,其實核心就一句話:面向接口編程
。
🔍 含義:
-
通過抽象(接口或抽象類)進行解耦
-
減少類之間的直接依賴
-
提高代碼的可測試性和靈活性
???就是這張圖,這就是DIP原則告訴我們應該做的。
???嘿,到這里五個原則就講完了,那這五個原則其實就是為了給IOC原則做鋪墊,注意:IOC是spring中的核心原則,上面SOLID是軟件設計需要遵循的五大原則,而spring做到了,實現的原理就是IOC控制反轉
。
二、控制反轉IOC(Inversion of Control)
1. 為什么會出現控制反轉
???Spring的核心就是IOC控制反轉,那么為什么要出現這個控制反轉,直接通過一個例子來說明。
public class UserService {private userDao;public UserService () {// 一旦替換數據庫,這里就要變化this.userDao= new UserDaoForMysql();} ... 具體的業務實現}
???上面的例子是一個簡單的數據庫查詢,但是如果有一天甲方要求mysql數據庫不安全,我要替換為其它數據庫,如果按照上面那種舊版寫法,那一個一個改去吧,很累的,因為耦合度太高了。因為我們沒有辦法直接new接口,只能new具體的實現類
,而為了解耦合,Spring就出現了IOC控制反轉,將userDao的創建和關系維護交給容器(Spring)去做,這樣代碼的改動就很小了,而且在編碼中也非常的簡潔。
2. 依賴注入(DI)
???注意,上面說的IOC是Spring中非常重要的一種思想,而實現這種思想的手段是DI依賴注入💡。
依賴:指的是對象A和B之間的關系,也就是UserService和UserServiceImpl之間的關系。
注入:注入是一種手段,通過這種手段,可以讓對象A和B對象產生關系
???一般我們說的注入手段其實就是怎么給對象賦值
,我們要使用userService就是new,那么注入實際上就是把new的這個過程交出去了,有兩種常見的注入方式。
- set注入:執行對象的set方法給屬性賦值。
- 構造方法注入:使用有參的構造方法給對象賦值。
2.1 set注入
📌 以下的例子來自老杜的筆記。
package com.powernode.spring6.dao;/*** @author 動力節點* @version 1.0* @className UserDao* @since 1.0**/
public class UserDao {public void insert(){System.out.println("正在保存用戶數據。");}
}
package com.powernode.spring6.service;import com.powernode.spring6.dao.UserDao;/*** @author 動力節點* @version 1.0* @className UserService* @since 1.0**/
public class UserService {private UserDao userDao;// 使用set方式注入,必須提供set方法。// 反射機制要調用這個方法給屬性賦值的。public void setUserDao(UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/><bean id="userServiceBean" class="com.powernode.spring6.service.UserService"><property name="userDao" ref="userDaoBean"/></bean></beans>
📌 實現原理:
- 通過property標簽獲取到屬性名
- 通過屬性名推斷出set方法名
- 通過反射機制調用set方法給屬性賦值
?? 需要注意的是:
- 對象的set方法一定要存在。
- ref屬性是要注入的bean對象的ID,而且不能重復
💡 核心
- 通過反射機制調用bean的set方法,讓兩個對象之間產生關系
2.2 構造方法注入
package com.powernode.spring6.dao;/*** @author 動力節點* @version 1.0* @className OrderDao* @since 1.0**/
public class OrderDao {public void deleteById(){System.out.println("正在刪除訂單。。。");}
}
package com.powernode.spring6.service;import com.powernode.spring6.dao.OrderDao;/*** @author 動力節點* @version 1.0* @className OrderService* @since 1.0**/
public class OrderService {private OrderDao orderDao;// 通過反射機制調用構造方法給屬性賦值public OrderService(OrderDao orderDao) {this.orderDao = orderDao;}public void delete(){orderDao.deleteById();}
}
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--index="0"表示構造方法的第一個參數,將orderDaoBean對象傳遞給構造方法的第一個參數。--><constructor-arg index="0" ref="orderDaoBean"/>
</bean>
不使用參數的下標,使用參數的名字也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/><bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--這里使用了構造方法上參數的名字--><constructor-arg name="orderDao" ref="orderDaoBean"/><constructor-arg name="userDao" ref="userDaoBean"/>
</bean><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
不指定下標,不指定名字,也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--沒有指定下標,也沒有指定參數名字--><constructor-arg ref="orderDaoBean"/><constructor-arg ref="userDaoBean"/>
</bean><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
當然,不指定順序,也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/><bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--順序已經和構造方法的參數順序不同了--><constructor-arg ref="userDaoBean"/><constructor-arg ref="orderDaoBean"/>
</bean><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
3. set注入專題
3.1 外部注入Bean
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/><bean id="userServiceBean" class="com.powernode.spring6.service.UserService"><property name="userDao" ref="userDaoBean"/></bean></beans>
📌 特點
- bean定義到外面,在property標簽中使用ref屬性進行注入,是最常用的方式
3.2 內部注入Bean
<?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="userServiceBean" class="com.powernode.spring6.service.UserService"><property name="userDao"><bean class="com.powernode.spring6.dao.UserDao"/></property></bean></beans>
3.3 注入簡單類型的屬性
package com.powernode.spring6.beans;/*** @author 動力節點* @version 1.0* @className User* @since 1.0**/
public class User {private int age;public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"age=" + age +'}';}
}
<?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="userBean" class="com.powernode.spring6.beans.User"><!--如果像這種int類型的屬性,我們稱為簡單類型,這種簡單類型在注入的時候要使用value屬性,不能使用ref--><!--<property name="age" value="20"/>--><property name="age"><value>20</value></property></bean>
</beans>
?? ?? ??
???有人說這種注入是為了什么,有什么作用,其實這就是@Value注解的前身
。我們經常將一些固定的配置放在yml或者properties文件中,如果在程序中想要使用,一般都是使用@Value注解,而上面的代碼就是在@Value注解沒有出現之前的替代方案。
@Component
public class DatabaseConfig {@Value("${db.url}")private String url;@Value("${db.username}")private String username;@Value("${db.password}")private String password;@Value("${db.pool.size:10}") // 默認值10private int poolSize;// getters...
}
db.url=jdbc:mysql://localhost:3306/mydb
db.username=admin
db.password=secret
db.pool.size=20
set注入簡單數據類型如下
● 基本數據類型
● 基本數據類型對應的包裝類
● String或其他的CharSequence子類
● Number子類
● Date子類
● Enum子類
● URI
● URL
● Temporal子類
● Locale
● Class
● 還包括以上簡單值類型對應的數組類型。
3.5 注入數組
📌當注入數組類型是簡單類型時
<?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="person" class="com.powernode.spring6.beans.Person"><property name="favariteFoods"><array><value>雞排</value><value>漢堡</value><value>鵝肝</value></array></property></bean>
</beans>
📌 當數組中是非簡單類型(對象)時使用ref注入
<?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="goods1" class="com.powernode.spring6.beans.Goods"><property name="name" value="西瓜"/></bean><bean id="goods2" class="com.powernode.spring6.beans.Goods"><property name="name" value="蘋果"/></bean><bean id="order" class="com.powernode.spring6.beans.Order"><property name="goods"><array><!--這里使用ref標簽即可--><ref bean="goods1"/><ref bean="goods2"/></array></property></bean></beans>
💡 要點
- 如果數組中是簡單類型,使用
value
標簽。 - 如果數組中是非簡單類型,使用
ref
標簽。
3.6 set注入List集合
<?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="peopleBean" class="com.powernode.spring6.beans.People"><property name="phones"><set><!--非簡單類型可以使用ref,簡單類型使用value--><value>110</value><value>110</value><value>120</value><value>120</value><value>119</value><value>119</value></set></property></bean>
</beans>
💡 要點:
- 使用
set
標簽 - set集合中元素是簡單類型的使用
value標簽,反之使用ref標簽
。
3.8 set注入Map集合
<?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="peopleBean" class="com.powernode.spring6.beans.People"><property name="addrs"><map><!--如果key不是簡單類型,使用 key-ref 屬性--><!--如果value不是簡單類型,使用 value-ref 屬性--><entry key="1" value="北京大興區"/><entry key="2" value="上海浦東區"/><entry key="3" value="深圳寶安區"/></map></property></bean>
</beans>
💡 要點
- 使用
map標簽
- 如果key是簡單類型,使用
key
屬性,反之使用key-ref
屬性。 - 如果value是簡單類型,使用
value
屬性,反之使用value-ref
屬性。
3.9 set注入Properties
📌 Properties繼承java.util.Hashtable,所以Properties也是一個Map集合
<?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="peopleBean" class="com.powernode.spring6.beans.People"><property name="properties"><props><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></props></property></bean>
</beans>
💡 要點
- 使用標簽嵌套標簽完成。
3.10 set注入null和空串
📌 注入空串的兩種方式
<?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="vipBean" class="com.powernode.spring6.beans.Vip"><!--空串的第一種方式--><!--<property name="email" value=""/>--><!--空串的第二種方式--><property name="email"><value/></property></bean></beans>
📌 注入null的兩種方式
<?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="vipBean" class="com.powernode.spring6.beans.Vip" /></beans><?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="vipBean" class="com.powernode.spring6.beans.Vip"><property name="email"><null/></property></bean></beans>
💡 要點
- 注入空字符串可以使用
<value/>和value=''
- 注入null可以使用
<null/>標簽或者不給屬性賦值
3.11 set注入特殊字符串
📌 xml中的五個特殊字符串 <、>、'、"、&
💡第一種解決方案,使用特殊符號轉義
特殊字符 | 轉義字符 |
---|---|
> | > |
< | < |
’ | ' |
" | " |
& | & |
💡 第二種解決方案,將含有特殊符號的字符串放到:<![CDATA[]]>
當中。因為放在CDATA區中的數據不會被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="mathBean" class="com.powernode.spring6.beans.Math"><property name="result"><!--只能使用value標簽--><value><![CDATA[2 < 3]]></value></property></bean></beans>
4. p命名空間注入
📌 目的:簡化配置
📌 前提條件
- 添加p命名空間的配置信息:xmlns:p=“http://www.springframework.org/schema/p”
- p命名空間是基于setter方法注入的,需要對應的setter方法
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:p="http://www.springframework.org/schema/p"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="customerBean" class="com.powernode.spring6.beans.Customer" p:name="zhangsan" p:age="20"/></beans>
5. c命名空間注入
📌 目的:簡化配置
,不過這里簡化的是構造方法的注入。
📌 前提條件
- 添加p命名空間的配置信息:xmlns:c=“http://www.springframework.org/schema/c”
- 需要提供構造方法
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:c="http://www.springframework.org/schema/c"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="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:year="1970" c:month="1" c:day="1"/>--><bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/></beans>
6. util命名空間
📌 目的:配置復用,也就是定義的對象可以使用在多個Bean中。
📌 前提條件
<?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:util="http://www.springframework.org/schema/util"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"><util:properties id="prop"><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></util:properties><bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1"><property name="properties" ref="prop"/></bean><bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2"><property name="properties" ref="prop"/></bean>
</beans>
7. 自動裝配
7.1 根據名稱自動裝配
<?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="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/><bean id="aaa" class="com.powernode.spring6.dao.UserDao"/></beans>
📌 關鍵點:
- 添加屬性:autowire=“byName”
- UserSerivice類中有一個對象aaa,必須擁有aaa對象的set方法,同時名稱和配置文件中的Bean的id相同。
7.2 根據類型自動裝配
<?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="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/><bean id="x" class="com.powernode.spring6.dao.AccountDao"/><bean id="y" class="com.powernode.spring6.dao.AccountDao"/></beans>
📌 要點:
- 添加屬性:autowire=“byType”
- 底層同樣基于set方法注入,但是要注意,同一個配置文件中
不能同時擁有兩個相同類型的Bean
。
8. Spring引入外部屬性配置文件
<?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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder location="jdbc.properties"/><bean id="dataSource" class="com.powernode.spring6.beans.MyDataSource"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></bean>
</beans>
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123
📌 要點
- 引入依賴:xmlns:context=“http://www.springframework.org/schema/context”
- 使用context標簽引入對應的文件:<context:property-placeholder location=“jdbc.properties”/>
- 使用value屬性,用${}符號獲取對應的值。
三、Bean的作用域
1. 單例模式(singleton)
???默認情況下,Spring的Ioc容器創建的Bean對象是單例的,這個單例的含義是:每個Spring容器中,這個Bean對應一個唯一的實例,也就是內存地址相同。
? 為什么默認采用單例?
-
性能考慮:
減少對象創建和銷毀的開銷
-
資源共享:適合無狀態的Bean共享使用
-
設計合理性:大多數情況下,服務類對象不需要多個實例
2. 原型作用域(property)
???原型作用域與單例模式相反,每次獲取Bean實例的時候都會創建新的對象,代碼中實現如下:
<?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="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" /></beans>
?? ?? ?? 注意,如果要使用原型作用域,必須了解以下幾點:
- Spring會在每次調用時創建新的對象,但是
不會主動去銷毀!
- 原型作用域適用的情況是
需要線程安全的場景
,或者說對象中的屬性是隔離的
,不同方法不能互相影響。
// 假設有以下Bean類
public class SpringBean {// 這個屬性是隔離的,不同方法不能互相影響private int counter;public void increment() {counter++;}public int getCount() {return counter;}
}
? 怎么手動銷毀原型作用域
- 實現 DisposableBean 接口
public class SpringBean implements DisposableBean {private int counter;public void increment() {counter++;}public int getCount() {return counter;}@Overridepublic void destroy() throws Exception {System.out.println("SpringBean 實例正在被銷毀,執行清理工作...");// 在這里釋放資源,如關閉文件、數據庫連接等}
}
- xml中配置destroy-method
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" destroy-method="customDestroy"/>
💡 真實使用場景
public class PrototypeBeanTest {public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");// 獲取 prototype beanSpringBean bean1 = context.getBean("sb", SpringBean.class);SpringBean bean2 = context.getBean("sb", SpringBean.class);// 使用bean...// 手動觸發銷毀(Spring不會自動調用)// 需要根據實際實現選擇調用方式if (bean1 instanceof DisposableBean) {try {((DisposableBean) bean1).destroy();} catch (Exception e) {e.printStackTrace();}}// 或者如果使用destroy-method方式// 可以通過反射調用指定方法try {Method destroyMethod = bean2.getClass().getMethod("customDestroy");destroyMethod.invoke(bean2);} catch (Exception e) {e.printStackTrace();}context.close();}
}
3. 其它作用域
● singleton:默認的,單例。
● prototype:原型。每調用一次getBean()方法則獲取一個新的Bean對象。或每次注入的時候都是新對象。
● request:一個請求對應一個Bean。僅限于在WEB應用中使用。
● session:一個會話對應一個Bean。僅限于在WEB應用中使用。
● global session:portlet應用中專用的。如果在Servlet的WEB應用中使用global session的話,和session一個效果。(portlet和servlet都是規范。servlet運行在servlet容器中,例如Tomcat。portlet運行在portlet容器中。)
● application:一個應用對應一個Bean。僅限于在WEB應用中使用。
● websocket:一個websocket生命周期對應一個Bean。僅限于在WEB應用中使用。
● 自定義scope:很少使用。
四、GoF之工廠模式
📌 設計模式:一種可以被重復利用的解決方案
。
📌 GoF(Gang of Four),中文名——四人組。
《Design Patterns: Elements of Reusable Object-Oriented Software》(即《設計模式》一書),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。這幾位作者常被稱為"四人組(Gang of Four)"。
📌 該書中描述了23種設計模式。我們平常所說的設計模式就是指這23種設計模式。不過除了GoF23種設計模式之外,還有其它的設計模式,比如:JavaEE的設計模式(DAO模式、MVC模式等)。
📌 GoF23種設計模式可分為三大類:
- 創建型(5個):解決對象創建問題。
■單例模式
■工廠方法模式
■抽象工廠模式
■ 建造者模式
■原型模式
- 結構型(7個):一些類或對象組合在一起的經典結構。
■ 代理模式
■ 裝飾模式
■ 適配器模式
■ 組合模式
■ 享元模式
■ 外觀模式
■ 橋接模式 - 行為型(11個):解決類或對象之間的交互問題。
■ 策略模式
■ 模板方法模式
■ 責任鏈模式
■ 觀察者模式
■ 迭代子模式
■ 命令模式
■ 備忘錄模式
■ 狀態模式
■ 訪問者模式
■ 中介者模式
■ 解釋器模式
1. 工廠模式的三種形態
- 簡單工廠模式:不屬于23中設計模式,又叫做
靜態工廠方法模式
,實際就是工廠模式的特殊實現(簡化版)。 - 工廠方法模式:23種設計模式之一。
- 抽象工廠模式:23種設計模式之一。
2. 簡單工廠模式
📌 三個主要角色
- 抽象產品:某一類產品的統一父類,比如美食。
- 具體產品:某一類產品的具體代表,比如火雞面。
- 工廠類:根據類型可以生產處具體的美食。
📌 抽象產品角色
package com.powernode.factory;/*** 武器(抽象產品角色)* @author 動力節點* @version 1.0* @className Weapon* @since 1.0**/
public abstract class Weapon {/*** 所有的武器都有攻擊行為*/public abstract void attack();
}
📌 具體產品角色
package com.powernode.factory;/*** 坦克(具體產品角色)* @author 動力節點* @version 1.0* @className Tank* @since 1.0**/
public class Tank extends Weapon{@Overridepublic void attack() {System.out.println("坦克開炮!");}
}
📌 工廠類角色
package com.powernode.factory;/*** 工廠類角色* @author 動力節點* @version 1.0* @className WeaponFactory* @since 1.0**/
public class WeaponFactory {/*** 根據不同的武器類型生產武器* @param weaponType 武器類型* @return 武器對象*/public static Weapon get(String weaponType){if (weaponType == null || weaponType.trim().length() == 0) {return null;}Weapon weapon = null;if ("TANK".equals(weaponType)) {weapon = new Tank();} else if ("FIGHTER".equals(weaponType)) {weapon = new Fighter();} else if ("DAGGER".equals(weaponType)) {weapon = new Dagger();} else {throw new RuntimeException("不支持該武器!");}return weapon;}
}
📌 客戶端角色
package com.powernode.factory;/*** @author 動力節點* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {Weapon weapon1 = WeaponFactory.get("TANK");weapon1.attack();Weapon weapon2 = WeaponFactory.get("FIGHTER");weapon2.attack();Weapon weapon3 = WeaponFactory.get("DAGGER");weapon3.attack();}
}
📌 簡單工廠模式的優點:
- 客戶端不需要關注細節,直接根據參數類型得到對應的對象,初步實現了
責任的分離
。客戶端只負責“消費”,工廠負責“生產”,生產和消費分離
。
📌 簡單工廠模式的缺點:
- 工廠類集中了所有產品的生產邏輯,可以稱之為上帝類,
一旦出現問題則系統癱瘓
。 不符合OCP開閉原則
,進行系統擴展時需要修改工廠類,而不是新增。
3. 工廠方法模式
📌 角色:
抽象工廠
:申明工廠方法,返回抽象產品類型,就是工廠的父類或者父接口。具體工廠
:重寫抽象工廠方法,返回具體的產品實例。- 抽象產品
- 具體產品
📌 抽象產品角色
package com.powernode.factory;/*** 武器類(抽象產品角色)* @author 動力節點* @version 1.0* @className Weapon* @since 1.0**/
public abstract class Weapon {/*** 所有武器都有攻擊行為*/public abstract void attack();
}
📌 具體產品角色
package com.powernode.factory;/*** 具體產品角色* @author 動力節點* @version 1.0* @className Gun* @since 1.0**/
public class Gun extends Weapon{@Overridepublic void attack() {System.out.println("開槍射擊!");}
}
📌 抽象工廠角色
package com.powernode.factory;/*** 武器工廠接口(抽象工廠角色)* @author 動力節點* @version 1.0* @className WeaponFactory* @since 1.0**/
public interface WeaponFactory {Weapon get();
}
📌 具體工廠角色
package com.powernode.factory;/*** 具體工廠角色* @author 動力節點* @version 1.0* @className GunFactory* @since 1.0**/
public class GunFactory implements WeaponFactory{@Overridepublic Weapon get() {return new Gun();}
}
📌 客戶端
package com.powernode.factory;/*** @author 動力節點* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {WeaponFactory factory = new GunFactory();Weapon weapon = factory.get();weapon.attack();WeaponFactory factory1 = new FighterFactory();Weapon weapon1 = factory1.get();weapon1.attack();}
}
💡 工廠方法模式就是為了解決簡單工廠的弊端:不符合OCP開閉原則的問題,在上面的例子中,如果后期要擴展的話,我們只需要增加對應的產品和抽象工廠以及具體工廠
,沒有修改原來的代碼,在其基礎上進行擴展。
💡 優點:
- 客戶端可以直接根據名稱創建對象。
- 擴展性高,只需要擴展工廠類即可。
- 屏蔽產品的具體實現,只需要關心產品的接口。
? 缺點:
- 類爆炸,需要不斷的擴展工廠,增加了系統的復雜度。
5. 抽象工廠模式
📌 角色:
抽象工廠
:申明工廠方法,返回抽象產品類型,就是工廠的父類或者父接口。具體工廠
:重寫抽象工廠方法,返回具體的產品實例。- 抽象產品
- 具體產品
武器產品族
package com.powernode.product;/*** 武器產品族* @author 動力節點* @version 1.0* @className Weapon* @since 1.0**/
public abstract class Weapon {public abstract void attack();
}
package com.powernode.product;/*** 武器產品族中的產品等級1* @author 動力節點* @version 1.0* @className Gun* @since 1.0**/
public class Gun extends Weapon{@Overridepublic void attack() {System.out.println("開槍射擊!");}
}
package com.powernode.product;/*** 武器產品族中的產品等級2* @author 動力節點* @version 1.0* @className Dagger* @since 1.0**/
public class Dagger extends Weapon{@Overridepublic void attack() {System.out.println("砍丫的!");}
}
水果產品族
package com.powernode.product;/*** 水果產品族* @author 動力節點* @version 1.0* @className Fruit* @since 1.0**/
public abstract class Fruit {/*** 所有果實都有一個成熟周期。*/public abstract void ripeCycle();
}
package com.powernode.product;/*** 水果產品族中的產品等級1* @author 動力節點* @version 1.0* @className Orange* @since 1.0**/
public class Orange extends Fruit{@Overridepublic void ripeCycle() {System.out.println("橘子的成熟周期是10個月");}
}
package com.powernode.product;/*** 水果產品族中的產品等級1* @author 動力節點* @version 1.0* @className Orange* @since 1.0**/
public class Orange extends Fruit{@Overridepublic void ripeCycle() {System.out.println("橘子的成熟周期是10個月");}
}
package com.powernode.product;/*** 水果產品族中的產品等級2* @author 動力節點* @version 1.0* @className Apple* @since 1.0**/
public class Apple extends Fruit{@Overridepublic void ripeCycle() {System.out.println("蘋果的成熟周期是8個月");}
}
抽象工廠類
package com.powernode.factory;import com.powernode.product.Fruit;
import com.powernode.product.Weapon;/*** 抽象工廠* @author 動力節點* @version 1.0* @className AbstractFactory* @since 1.0**/
public abstract class AbstractFactory {public abstract Weapon getWeapon(String type);public abstract Fruit getFruit(String type);
}
具體工廠類
package com.powernode.factory;import com.powernode.product.Dagger;
import com.powernode.product.Fruit;
import com.powernode.product.Gun;
import com.powernode.product.Weapon;/*** 武器族工廠* @author 動力節點* @version 1.0* @className WeaponFactory* @since 1.0**/
public class WeaponFactory extends AbstractFactory{public Weapon getWeapon(String type){if (type == null || type.trim().length() == 0) {return null;}if ("Gun".equals(type)) {return new Gun();} else if ("Dagger".equals(type)) {return new Dagger();} else {throw new RuntimeException("無法生產該武器");}}@Overridepublic Fruit getFruit(String type) {return null;}
}
package com.powernode.factory;import com.powernode.product.*;/*** 水果族工廠* @author 動力節點* @version 1.0* @className FruitFactory* @since 1.0**/
public class FruitFactory extends AbstractFactory{@Overridepublic Weapon getWeapon(String type) {return null;}public Fruit getFruit(String type){if (type == null || type.trim().length() == 0) {return null;}if ("Orange".equals(type)) {return new Orange();} else if ("Apple".equals(type)) {return new Apple();} else {throw new RuntimeException("我家果園不產這種水果");}}
}
package com.powernode.client;import com.powernode.factory.AbstractFactory;
import com.powernode.factory.FruitFactory;
import com.powernode.factory.WeaponFactory;
import com.powernode.product.Fruit;
import com.powernode.product.Weapon;/*** @author 動力節點* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {// 客戶端調用方法時只面向AbstractFactory調用方法。AbstractFactory factory = new WeaponFactory(); // 注意:這里的new WeaponFactory()可以采用 簡單工廠模式 進行隱藏。Weapon gun = factory.getWeapon("Gun");Weapon dagger = factory.getWeapon("Dagger");gun.attack();dagger.attack();AbstractFactory factory1 = new FruitFactory(); // 注意:這里的new FruitFactory()可以采用 簡單工廠模式 進行隱藏。Fruit orange = factory1.getFruit("Orange");Fruit apple = factory1.getFruit("Apple");orange.ripeCycle();apple.ripeCycle();}
}
? 優點:當一個產品族中的多個對象被設計成一起工作時,它能保證客戶端始終只使用同一個產品族中的對象
。
? 缺點:產品族擴展非常困難
,要增加一個系列的某一產品,既要在AbstractFactory里加代碼,又要在具體的里面加代碼。
6. 三種工廠模式對比(重點??)
模式 | 核心區別 | 適用場景 |
---|---|---|
簡單工廠 | 一個工廠類通過條件判斷創建所有產品。 | 產品種類少,邏輯簡單。 |
工廠方法 | 一個工廠類只生產一種產品,通過子類擴展。 | 需要靈活擴展單一產品類型。 |
抽象工廠 | 一個工廠類生產一個產品族(多個相關產品)。 | 需要保證一組產品的一致性(如跨平臺)。 |
對比總結
- 簡單工廠:集中式管理,但違反開閉原則(新增產品需修改工廠類)。
- 工廠方法:單一職責,支持擴展,但每個產品需對應一個工廠類。
- 抽象工廠:解決產品族協同問題,但擴展新產品類型需修改接口。
???注意了,如果在反復閱讀之后還是不懂上面三種工廠模式的區別,不妨聽我用大白話敘述一下,用我們生活中常見的例子進行說明。
- 簡單工廠:小商店
- 工廠方法:超市
- 抽象工廠:商超
???拿簡單工廠來說,因為其只有一個核心的工廠類(上帝類),所以并不太適合復雜的場景,因為其并不符合OCP開閉原則,一旦業務復雜,擴展將會很復雜,但是如果我們的業務比較簡單,事實上可以使用簡單工廠,這和小商店是一樣的,里面有很多種類的東西,但其實類型并不是很復雜,一個售貨員就可以管理和出售
,如果規模大,可以多雇幾個,但實際上內部的實現邏輯是比較簡單的。
???接上,如果小商店擴展擴展發現,管理逐漸費勁,而且效率不夠高的時候,他就需要進行拆分,進化為超市,也就是工廠方法模式
,對于不同種類的商品進行分類,并且招聘對應的售貨員(工廠類),如果后期需要擴展,那就增加對應商品的分類,同時招收售貨員。這樣一來管理就變得方便了,在一定承受范圍內可以繼續擴展。
???繼續,在不斷發展之下,這個老板又想繼續擴展,而且想要增加不同類型和系列的產品比如娛樂、休息和衣服等模塊,所以他成立了一個商超,也就是抽象工廠模式
。到這一步普通的管理就顯得微不足道了,如果不斷的擴展是很麻煩的,所以他成立了一個管理部門(抽象工廠),由這個管理部門去吸納不同模塊的管理人才,然后不同模塊的管理者在根據職責實現對應產品的創建、宣傳、售賣等等,但是如果要繼續擴展則需要從管理層開始,不斷的一層一層維護,直到最后的具體商品
,同時也需要改變原有的管理層架構。這樣做的好處就是不同的但又類型統一的模塊都在一起
,用戶可以根據需求直接面向抽象工廠,由抽象工廠對應到具體的工廠,最后得到用戶的產品。
???在這種設計模式下,商超的魅力就在于解耦
,用戶不需要直面具體的產品,直接面向銷售或者大廳管理者,由他們帶領用戶去到指定的模塊,再由對應模塊的負責人對接,然后拿到具體的產品。解耦解耦,無非就是將一個需求實現的過程拆分,每一步都有清晰的職責,雖然過程復雜,但是實現簡單有效
。這樣的痛點就是怎樣維護這個過程,而這個過程對于用戶來說是隱藏的。
???再到我們的互聯網公司,越大的公司,每一個員工的職責越詳細,板塊越清晰
,但需要一些特殊的人才來將其管理和匯總,我們可以理解為中樞。
???其實到頭來不過一句話,沒有什么是加一層解決不了的
,仔細想想這一步一步的過程不就是多加了一層嗎?
五、Bean的實例化方式
1. 構造方法實例化
<?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="userBean" class="com.powernode.spring6.bean.User"/></beans>
2、、通過簡單工廠模式實例化
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className Vip* @since 1.0**/
public class Vip {
}
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className VipFactory* @since 1.0**/
public class VipFactory {public static Vip get(){return new Vip();}
}
<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>
@Test
public void testSimpleFactory(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Vip vip = applicationContext.getBean("vipBean", Vip.class);System.out.println(vip);
}
3. 通過factory-bean實例化
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className Order* @since 1.0**/
public class Order {
}
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className OrderFactory* @since 1.0**/
public class OrderFactory {public Order get(){return new Order();}
}
<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/>
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>
@Test
public void testSelfFactoryBean(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Order orderBean = applicationContext.getBean("orderBean", Order.class);System.out.println(orderBean);
}
實質就是通過工廠方法模式創建。
4. 通過FactoryBean接口實例化
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className Person* @since 1.0**/
public class Person {
}
package com.powernode.spring6.bean;import org.springframework.beans.factory.FactoryBean;/*** @author 動力節點* @version 1.0* @className PersonFactoryBean* @since 1.0**/
public class PersonFactoryBean implements FactoryBean<Person> {@Overridepublic Person getObject() throws Exception {return new Person();}@Overridepublic Class<?> getObjectType() {return null;}@Overridepublic boolean isSingleton() {// true表示單例// false表示原型return true;}
}
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>
@Test
public void testFactoryBean(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Person personBean = applicationContext.getBean("personBean", Person.class);System.out.println(personBean);Person personBean2 = applicationContext.getBean("personBean", Person.class);System.out.println(personBean2);
}
FactoryBean在Spring中是一個接口。被稱為“工廠Bean”。“工廠Bean”是一種特殊的Bean。所有的“工廠Bean”都是用來協助Spring框架來創建其他Bean對象的。
5. BeanFactory和FactoryBean的區別
BeanFactory是一個工廠
,可以用來創建Bean對象。FactoryBean是一種Bean
,他是Spring中用來輔助創建其它Bean對象的一種特殊Bean。
6. 為什么要學習Bean的不同創建方式
📌 其主要目的是讓我們更好的理解Spring的底層哲學,就是最重要的Ioc控制反轉思想的實現,主要體現在:
- 解耦
- 擴展性
📌 為什么我們在看這一章的時候感覺很懵逼,沒什么用,因為在正常開發中是用不到的,大多數使用在框架的開發和程序中特殊業務下需要統一提供的功能
,這些都是架構師來做的。比如簡單工廠模式,我們可以用在不同數據源的切換上。
<bean id="dataSource" class="com.example.DataSourceFactory" factory-method="createDataSource"/>
public class DataSourceFactory {public static DataSource createDataSource() {return new HikariDataSource(); // 根據配置返回不同實現}
}
📌 或者說在mybatis和shiro中也是常用的,不能每一次數據庫請求都需要創建一次連接吧,用戶每次登陸的統一信息管理只存一份就行了吧,也好管理,類似與這種,有興趣的伙伴可以看源碼,看看他們的底層是怎么實現的。
六. Bean的生命周期
💡 Spring其實就是一個管理Bean對象的工廠
,它負責對象的創建和銷毀,Bean的生命周期實際上就是對象從創建到銷毀的整個過程,比如:
- 什么時候創建Bean?
- 創建Bean前后會調用什么方法?
- Bean對象什么時候銷毀?
- Bean對象銷毀前后會調用什么方法?
💡 所以其本質可以總結為:我們需要知道在哪一步調用了什么方法,以便我們可以添加自己的邏輯!
1. Bean生命周期之5步
- 實例化Bean
- Bean屬性賦值
- 初始化Bean
- 使用Bean
- 銷毀Bean
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className User* @since 1.0**/
public class User {private String name;public User() {System.out.println("1.實例化Bean");}public void setName(String name) {this.name = name;System.out.println("2.Bean屬性賦值");}public void initBean(){System.out.println("3.初始化Bean");}public void destroyBean(){System.out.println("5.銷毀Bean");}}
<?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"><!--init-method屬性指定初始化方法。destroy-method屬性指定銷毀方法。--><bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean"><property name="name" value="zhangsan"/></bean></beans>
package com.powernode.spring6.test;import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;/*** @author 動力節點* @version 1.0* @className BeanLifecycleTest* @since 1.0**/
public class BeanLifecycleTest {@Testpublic void testLifecycle(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");User userBean = applicationContext.getBean("userBean", User.class);System.out.println("4.使用Bean");// 只有正常關閉spring容器才會執行銷毀方法ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;context.close();}
}
?? 需要注意的是:
- 只有正常關閉Spring容器的時候,bean的銷毀方法才會被調用
- ClassPathXmlApplicationContext類才有close()方法
- 配置文件中的
init-method
指定初始化方法。destroy-method
指定銷毀方法
2. Bean生命周期之7步
💡 這里相比于上面多了兩步,分別是Bean初始化前和初始化后
package com.powernode.spring6.bean;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;/*** @author 動力節點* @version 1.0* @className LogBeanPostProcessor* @since 1.0**/
public class LogBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {System.out.println("Bean后處理器的before方法執行,即將開始初始化");return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {System.out.println("Bean后處理器的after方法執行,已完成初始化");return bean;}
}
💡 配置Bean后處理器
<!--配置Bean后處理器。這個后處理器將作用于當前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>
3. Bean生命周期之10步
Aware相關的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
- 當Bean實現了BeanNameAware,Spring會將Bean的名字傳遞給Bean。
- 當Bean實現了BeanClassLoaderAware,Spring會將加載該Bean的類加載器傳遞給Bean。
- 當Bean實現了BeanFactoryAware,Spring會將Bean工廠對象傳遞給Bean。
package com.powernode.spring6.bean;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;/*** @author 動力節點* @version 1.0* @className User* @since 1.0**/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {private String name;public User() {System.out.println("1.實例化Bean");}public void setName(String name) {this.name = name;System.out.println("2.Bean屬性賦值");}public void initBean(){System.out.println("6.初始化Bean");}public void destroyBean(){System.out.println("10.銷毀Bean");}@Overridepublic void setBeanClassLoader(ClassLoader classLoader) {System.out.println("3.類加載器:" + classLoader);}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {System.out.println("3.Bean工廠:" + beanFactory);}@Overridepublic void setBeanName(String name) {System.out.println("3.bean名字:" + name);}@Overridepublic void destroy() throws Exception {System.out.println("9.DisposableBean destroy");}@Overridepublic void afterPropertiesSet() throws Exception {System.out.println("5.afterPropertiesSet執行");}
}
通過測試可以看出來:
- InitializingBean的方法早于init-method的執行。
- DisposableBean的方法早于destroy-method的執行。
七、Bean的循環依賴
1. 什么是循環依賴
A對象中有B屬性。B對象中有A屬性。這就是循環依賴。我依賴你,你也依賴我
準備兩個對象,演示在spring中什么情況下會出現循環依賴問題
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className Husband* @since 1.0**/
public class Husband {private String name;private Wife wife;public void setName(String name) {this.name = name;}public String getName() {return name;}public void setWife(Wife wife) {this.wife = wife;}// toString()方法重寫時需要注意:不能直接輸出wife,輸出wife.getName()。要不然會出現遞歸導致的棧內存溢出錯誤。@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}
}
package com.powernode.spring6.bean;/*** @author 動力節點* @version 1.0* @className Wife* @since 1.0**/
public class Wife {private String name;private Husband husband;public void setName(String name) {this.name = name;}public String getName() {return name;}public void setHusband(Husband husband) {this.husband = husband;}// toString()方法重寫時需要注意:不能直接輸出husband,輸出husband.getName()。要不然會出現遞歸導致的棧內存溢出錯誤。@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}
2. singleton下的set注入
<?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="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton"><property name="name" value="張三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean>
</beans>
package com.powernode.spring6.test;import com.powernode.spring6.bean.Husband;
import com.powernode.spring6.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;/*** @author 動力節點* @version 1.0* @className CircularDependencyTest* @since 1.0**/
public class CircularDependencyTest {@Testpublic void testSingletonAndSet(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);System.out.println(husbandBean);System.out.println(wifeBean);}
}
💡 在singleton + set注入的情況下,循環依賴是沒有問題的。Spring可以解決這個問題。
3. property下的set注入
<?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="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype"><property name="name" value="張三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean>
</beans>
執行測試程序:發生了異常,異常信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException
: Error creating bean with name ‘husbandBean’: Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
… 44 more
翻譯為:創建名為“husbandBean”的bean時出錯:請求的bean當前正在創建中:是否存在無法解析的循環引用?
📌 經過測試,只有都為property的情況下才會出現,只要其中有一個是singleton就可以避免這個問題
,主要原因如下:
4. singleton+構造注入
<?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="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton"><constructor-arg name="name" value="張三"/><constructor-arg name="wife" ref="wBean"/></bean><bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton"><constructor-arg name="name" value="小花"/><constructor-arg name="husband" ref="hBean"/></bean>
</beans>
📌 現象和原因
依舊會產生和上面一樣的循環依賴問題,主要原因是因為通過構造方法注入導致的。因為構造方法注入會導致實例化對象的過程和對象屬性賦值的過程沒有分離開
,必須在一起完成導致的。
5. Spring解決循環依賴的原理和實現
📌 前提條件
set + singleton模式
📌 根本原因
根本的原因在于:這種方式可以做到將“實例化Bean”和“給Bean屬性賦值”這兩個動作分開去完成。
- 實例化Bean的時候:調用無參數構造方法來完成。此時可以
先不給屬性賦值
,可以提前將該Bean對象“曝光”給外界
。 - 給Bean屬性賦值的時候:調用setter方法來完成。
- 兩個步驟是完全可以分離開去完成的,并且這
兩步不要求在同一個時間點上完成
。 - 也就是說,Bean都是單例的,我們可以先把所有的單例Bean實例化出來,放到一個集合當中(我們可以稱之為
緩存
),所有的單例Bean全部實例化完成之后,以后我們再慢慢的調用setter方法給屬性賦值
。這樣就解決了循環依賴的問題。
📌 代碼實現
- Cache of singleton objects: bean name to bean instance. 單例對象的緩存:key存儲bean名稱,value存儲Bean對象【一級緩存】
- Cache of early singleton objects: bean name to bean instance. 早期單例對象的緩存:key存儲bean名稱,value存儲早期的Bean對象【二級緩存】
- Cache of singleton factories: bean name to ObjectFactory. 單例工廠緩存:key存儲bean名稱,value存儲該Bean對應的ObjectFactory對象【三級緩存】
這三個緩存其實本質上是三個Map集合。
💡 提前曝光的核心:addSingletonFactory
💡 獲取曝光對象的步驟
💡 從源碼中可以看到,spring會先從一級緩存
中獲取Bean,如果獲取不到,則從二級緩存
中獲取Bean,如果二級緩存還是獲取不到,則從三級緩存
中獲取之前曝光的ObjectFactory對象,通過ObjectFactory對象獲取Bean實例,這樣就解決了循環依賴的問題。
6. 總結
💡 Spring只能解決setter方法注入的單例bean之間的循環依賴
。ClassA依賴ClassB,ClassB又依賴ClassA,形成依賴閉環。Spring在創建ClassA對象后,不需要等給屬性賦值,直接將其曝光到bean緩存
當中。在解析ClassA的屬性時,又發現依賴于ClassB,再次去獲取ClassB,當解析ClassB的屬性時,又發現需要ClassA的屬性,但此時的ClassA已經被提前曝光加入了正在創建的bean的緩存中,則無需創建新的的ClassA的實例,直接從緩存中獲取即可。從而解決循環依賴問題。
八、注解開發
??? 上面都是基于配置文件進行Bean的裝配的,當然我們后面都是基于注解開發的,這大大減少了我們的工作量,不過還是得需要了解其中的底層工作原理,下面介紹一下我們常用的一些Spring注解。
1. 核心組件注解
- @Component
-
作用:通用的組件注解,標識一個類為Spring組件
-
使用場景:當不確定一個類屬于哪一層時使用
- @Controller
-
作用:標識一個類為Spring MVC控制器
-
特點:是@Component的特殊化,主要用于處理HTTP請求
- @Service
-
作用:標識一個類為業務服務層組件
-
特點:是@Component的特殊化,用于業務邏輯層
- @Repository
-
作用:標識一個類為數據訪問層組件
-
特點:是@Component的特殊化,具有將數據庫操作拋出的原生異常轉換為Spring的持久化異常的功能
四個注解的關系:@Controller、@Service、@Repository都是@Component的特殊化,從功能上可以互相替換,但使用特定注解能更好地表達類的用途,并且某些特定注解會有額外的功能(如@Repository的異常轉換)。
2. 依賴注入相關注解
- @Autowired
-
作用:自動裝配依賴對象
-
特點:
1.1 默認按類型匹配
1.2 可以用在構造器、方法、字段和參數上
1.3 是Spring提供的注解
- @Resource
-
作用:自動裝配依賴對象
-
特點:
2.1 默認按名稱匹配,名稱可通過name屬性指定
2.2 是JSR-250標準注解,不屬于Spring
2.3 可以用在字段和方法上
- @Qualifier
-
作用:當有多個相同類型的bean時,用于指定具體的bean
-
常與@Autowired一起使用
- @Value
-
作用:注入屬性值,支持SpEL表達式
-
使用場景:
4.1 注入簡單值
4.2 注入配置文件中的屬性
4.3 使用SpEL表達式
注解 | 來源 | 主要用途 | 特點 |
---|---|---|---|
@Component | Spring | 通用組件聲明 | 最基礎的組件注解 |
@Controller | Spring | MVC控制器 | 處理HTTP請求,@Component的特殊化 |
@Service | Spring | 業務服務層 | 業務邏輯處理,@Component的特殊化 |
@Repository | Spring | 數據訪問層 | 異常自動轉換,@Component的特殊化 |
@Autowired | Spring | 依賴注入 | 默認按類型匹配,支持構造器/方法/字段/參數 |
@Resource | JSR-250 | 依賴注入 | 默認按名稱匹配,支持name屬性指定 |
@Qualifier | Spring | 限定注入 | 配合@Autowired解決多個同類型bean的歧義 |
@Value | Spring | 屬性注入 | 支持直接值注入、配置文件屬性注入(${})和SpEL表達式注入(#{})) |
九、GOF之代理模式
??? 代理模式是GOF中23種設計模式之一,核心就是兩個字“代理”,簡單點來說就是中間件(在生活中就是中介和銷售的例子)
,代理模式屬于結構型設計模式
,主要特點如下:
- 保護目標對象,不會直接接觸到目標對象,使用代理類。
- 簡化代碼,增加靈活性。
- 代理類擁有和目標類一樣的功能。
??? 代理模式的角色:
- 代理類(代理主題)
- 目標類(真實主題)
- 共用接口(代理類和目標類共同實現的接口)
💡 可以聯想一下Shiro,可以判斷當前的登陸狀態,也可以獲取到當前登錄的用戶信息。
1. 靜態代理
package com.powernode.mall.service.impl;import com.powernode.mall.service.OrderService;/*** @author 動力節點* @version 1.0* @className OrderServiceImpl* @since 1.0**/
public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("訂單已生成");}@Overridepublic void detail() {try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("訂單信息如下:******");}@Overridepublic void modify() {try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("訂單已修改");}
}
???現在的場景是有一個類中有三個方法,需要統計這三個方法的用時,正常有三種解決辦法。
📌 第一種:直接在源碼上添加統計時間的代碼,弊端是違反OCP原則,優勢是實現簡單。
package com.powernode.mall.service.impl;import com.powernode.mall.service.OrderService;/*** @author 動力節點* @version 1.0* @className OrderServiceImpl* @since 1.0**/
public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {long begin = System.currentTimeMillis();try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("訂單已生成");long end = System.currentTimeMillis();System.out.println("耗費時長"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("訂單信息如下:******");long end = System.currentTimeMillis();System.out.println("耗費時長"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("訂單已修改");long end = System.currentTimeMillis();System.out.println("耗費時長"+(end - begin)+"毫秒");}
}
📌 第二種,編寫一個子類繼承上面的目標類,在子類中統計對應的方法耗時,優勢是符合OCP原則,缺點是存在類爆炸和重復代碼的問題,增加了代碼的耦合度。
package com.powernode.mall.service.impl;/*** @author 動力節點* @version 1.0* @className OrderServiceImplSub* @since 1.0**/
public class OrderServiceImplSub extends OrderServiceImpl{@Overridepublic void generate() {long begin = System.currentTimeMillis();super.generate();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();super.detail();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();super.modify();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}
}
📌 第三種:使用靜態代理
package com.powernode.mall.service;/*** @author 動力節點* @version 1.0* @className OrderServiceProxy* @since 1.0**/
public class OrderServiceProxy implements OrderService{ // 代理對象// 目標對象private OrderService orderService;// 通過構造方法將目標對象傳遞給代理對象public OrderServiceProxy(OrderService orderService) {this.orderService = orderService;}@Overridepublic void generate() {long begin = System.currentTimeMillis();// 執行目標對象的目標方法orderService.generate();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();// 執行目標對象的目標方法orderService.detail();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();// 執行目標對象的目標方法orderService.modify();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}
}
?? 注意,這里還是創建了一個新的類,但是我們是實現了對應的Service接口,相當于最開始說的代理類和目標類實現同一個接口,代理類擁有和目標類相同的功能,但是在代理類中我們可以增加個性化功能,不影響目標類。
?? 弊端:無法解決類爆炸的問題。
2. 動態代理(Proxy類)
??? 動態代理主要是為了解決靜態代理的類爆炸
問題,原理就是在內存中動態的生成對應的字節碼文件
,用完之后銷毀,代碼中只需要一份邏輯代碼。
2.1 JDK動態代理
??? 只能代理接口
。
📌 創建一個統計時間的類
package com.powernode.mall.service;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;/*** @author 動力節點* @version 1.0* @className TimerInvocationHandler* @since 1.0**/
public class TimerInvocationHandler implements InvocationHandler {// 目標對象private Object target;// 通過構造方法來傳目標對象public TimerInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 目標執行之前增強。long begin = System.currentTimeMillis();// 調用目標對象的目標方法Object retValue = method.invoke(target, args);// 目標執行之后增強。long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");// 一定要記得返回哦。return retValue;}
}
package com.powernode.mall;import com.powernode.mall.service.OrderService;
import com.powernode.mall.service.TimerInvocationHandler;
import com.powernode.mall.service.impl.OrderServiceImpl;import java.lang.reflect.Proxy;/*** @author 動力節點* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {// 創建目標對象OrderService target = new OrderServiceImpl();// 創建代理對象OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new TimerInvocationHandler(target));// 調用代理對象的代理方法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}
InvocationHandler接口中有一個方法invoke,這個invoke方法上有三個參數:
- 第一個參數:
Object proxy。代理對象
。設計這個參數只是為了后期的方便,如果想在invoke方法中使用代理對象的話,盡管通過這個參數來使用。 - 第二個參數:
Method method。目標方法
。 - 第三個參數:
Object[] args。目標方法調用時要傳的參數
。
??? 可以看到JDK的動態代理完美的解決了類爆炸的問題,而且大幅度的簡化了代碼,只需要改動調用的地方即可,符合OCP原則(沒有改動業務代碼)。
2.2 CgLib動態代理
??? 既可以代理接口,也可以代理類
。
📌 創建代理類
package com.powernode.mall.service;import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** @author 動力節點* @version 1.0* @className TimerMethodInterceptor* @since 1.0**/
public class TimerMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 前增強long begin = System.currentTimeMillis();// 調用目標Object retValue = methodProxy.invokeSuper(target, objects);// 后增強long end = System.currentTimeMillis();System.out.println("耗時" + (end - begin) + "毫秒");// 一定要返回return retValue;}
}
📌 客戶端使用
package com.powernode.mall;import com.powernode.mall.service.TimerMethodInterceptor;
import com.powernode.mall.service.UserService;
import net.sf.cglib.proxy.Enhancer;/*** @author 動力節點* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {// 創建字節碼增強器Enhancer enhancer = new Enhancer();// 告訴cglib要繼承哪個類enhancer.setSuperclass(UserService.class);// 設置回調接口enhancer.setCallback(new TimerMethodInterceptor());// 生成源碼,編譯class,加載到JVM,并創建代理對象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();userServiceProxy.logout();}
}
??? CgLib的動態代理實現起來更加的舒服和簡潔,個人比較推薦CgLib,其中有字節碼增強器,效率也會更快一些。
??? 對于高版本的jdk需要增加兩個參數。
● --add-opens java.base/java.lang=ALL-UNNAMED
● --add-opens java.base/java.lang=ALL-UNNAMED
十、Aop切面編程
📌 Aop是一種編程技術
;
📌 Aop底層是通過JDK和CjLib動態
代理實現的,接口使用JDK,類使用CjLib,也可以強制使用CjLib
;
📌 Aop技術通常使用在日志和全局的某些通用功能處理上,比如上面的時間分析
,又或者是行為日志
等
1. Aop介紹
??? Aop大部分的使用場景是交叉業務
,所謂交叉業務指的是系統中的一些通用服務例如:安全、日志、事務管理等
。
📌 為什么會出現交叉業務?
- 類似日志和安全這種問題在代碼中大多數都是重復代碼,提出來單獨處理可以減少重復代碼,并且方便管理和擴展。
- 交叉業務出現可以讓程序員更加專注業務代碼,增加開發效率。
📌 為什么要學習Aop?
- 這是一種重要的編程思想,能增加我們在開發中的靈活性。
- 重點:普通程序猿轉向高級程序猴的重要一步,操作系統的架構。
📌 總結:將與核心業務無關的代碼抽出來形成一個單獨的組件,然后以橫向交叉的方式應用到業務中的過程被稱作Aop編程。
📌 Aop的優點
- 代碼復用性高。
- 靈活性強,好擴展
- 讓開發者更加關注業務代碼
2. Aop的七大術語
2.1 連接點( Joinpoint)
📌 在程序的整個執行流程中,可以織入切面的位置
。例如方法的執行前后,異常拋出之后的位置等。
2.2 切點(Pointcut)
📌 在程序執行過程織入切面的方法
。(一個切點對應多個連接點)
2.3 通知(Advice)
📌 通知又叫做增強,就是具體織入的代碼,可分為以下幾種:
- 前置通知
- 后置通知
- 環繞通知
- 異常通知
- 最終通知
2.4 切面(Aspect)
📌 切點+通知組成切面。
2.5 織入(Weaving)
📌 把通知應用到目標對象上的過程。
2.6 代理對象(Proxy)
📌 一個目標對象被織入通知后產生的新對象。
2.7 目標對象(Target)
📌 被織入通知的對象。
3. 切點表達式
切點表達式用來定義通知(Advice)往哪些方法上切入。
切入點表達式語法格式:
execution([訪問控制權限修飾符] 返回值類型 [全限定類名]方法名(形式參數列表) [異常])
訪問控制權限修飾符:
● 可選項。
● 沒寫,就是4個權限都包括。
● 寫public就表示只包括公開的方法。
返回值類型:
● 必填項。
● * 表示返回值類型任意。
全限定類名:
● 可選項。
● 兩個點“…”代表當前包以及子包下的所有類。
● 省略時表示所有的類。
方法名:
● 必填項。
● 表示所有方法。
● set表示所有的set方法。
形式參數列表:
● 必填項
● () 表示沒有參數的方法
● (…) 參數類型和個數隨意的方法
● () 只有一個參數的方法
● (, String) 第一個參數類型隨意,第二個參數是String的。
異常:
● 可選項。
● 省略時表示任意異常類型。
service包下所有的類中以delete開始的所有方法execution(public * com.powernode.mall.service.*.delete*(..))
mall包下所有的類的所有的方法execution(* com.powernode.mall..*(..))
所有類的所有方法execution(* *(..))
4. 使用Spring的Aop編程
- 第一種方式:
Spring框架結合AspectJ框架實現的AOP,基于注解方式
。(常用) - 第二種方式:
Spring框架結合AspectJ框架實現的AOP,基于XML方式
。 - 第三種方式:Spring框架自己實現的AOP,基于XML配置方式。
? 什么是AspectJ?(Eclipse組織的一個支持AOP的框架。AspectJ框架是獨立于Spring框架之外的一個框架,Spring框架用了AspectJ)
AspectJ項目起源于帕洛阿爾托(Palo Alto)研究中心(縮寫為PARC)。該中心由Xerox集團資助,Gregor Kiczales領導,從1997年開始致力于AspectJ的開發,1998年第一次發布給外部用戶,2001年發布1.0 release。為了推動AspectJ技術和社團的發展,PARC在2003年3月正式將AspectJ項目移交給了Eclipse組織,因為AspectJ的發展和受關注程度大大超出了PARC的預期,他們已經無力繼續維持它的發展。
4.1 準備工作
使用Spring+AspectJ的AOP需要引入的依賴如下
<!--spring context依賴-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.0-M2</version>
</dependency>
<!--spring aop依賴-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.0.0-M2</version>
</dependency>
<!--spring aspects依賴-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.0.0-M2</version>
</dependency>
<?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"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
4.2 核心步驟
📌 第一步:目標對象必須被Spring管理,也就是添加@Component注解
。
package com.powernode.spring6.service;// 目標類
@Component
public class OrderService {// 目標方法public void generate(){System.out.println("訂單已生成!");}
}
📌 第二步:開啟組件掃描
和自動代理
<?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"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--開啟組件掃描--><context:component-scan base-package="com.powernode.spring6.service"/><!--開啟自動代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
<aop:aspectj-autoproxy proxy-target-class=“true”/> 開啟自動代理之后,凡事帶有@Aspect注解的bean都會生成代理對象。
proxy-target-class=“true” 表示采用cglib動態代理。
proxy-target-class=“false” 表示采用jdk動態代理。默認值是false。即使寫成false,當沒有接口的時候,也會自動選擇cglib生成代理類。
📌 第三步:添加切面類和通知,并配置切點表達式
package com.powernode.spring6.service;import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;// 切面類
@Aspect
@Component
public class MyAspect {// 切點表達式@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")// 這就是需要增強的代碼(通知)public void advice(){System.out.println("我是一個通知");}
}
??? 到這里一個簡單的例子就完成了,Aop編程在有些公司中非常的普遍,如果要更好的使用Aop編程,對于我們程序猴的要求也是比較高的,比如說嚴格遵循SOLID五大原則
,尤其是ISP接口隔離
和SRP單一職責原則
,這樣可以更好的織入通知,嵌入交叉業務。更有甚者對于方法名也有嚴格的要求
,現在知道為什么了吧,都是為了更好的實現Aop。
4.3 通知類型
- 前置通知:@Before 目標方法執行之前的通知
- 后置通知:@AfterReturning 目標方法執行之后的通知
- 環繞通知:@Around 目標方法之前添加通知,同時目標方法執行之后添加通知。
- 異常通知:@AfterThrowing 發生異常之后執行的通知
- 最終通知:@After 放在finally語句塊中的通知
💡 這個順序就代表了在程序中通知的執行順序!
package com.powernode.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;// 切面類
@Component
@Aspect
public class MyAspect {@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("環繞通知開始");// 執行目標方法。proceedingJoinPoint.proceed();System.out.println("環繞通知結束");}@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("異常通知");}@After("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterAdvice(){System.out.println("最終通知");}}
?? 注意:
- 最終通知在發生異常后也是會執行的。
- 發生異常后,環繞通知的結束部分不會執行,后置通知也不會執行。
4.4 切面的執行順序
🚀 我們知道,業務流程當中不一定只有一個切面,可能有的切面控制事務,有的記錄日志,有的進行安全控制,如果多個切面的話,順序如何控制:可以使用@Order注解來標識切面類,為@Order注解的value指定一個整數型的數字,數字越小,優先級越高
。
package com.powernode.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Aspect
@Component
@Order(1) //設置優先級
public class YourAspect {@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("YourAspect環繞通知開始");// 執行目標方法。proceedingJoinPoint.proceed();System.out.println("YourAspect環繞通知結束");}@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("YourAspect前置通知");}@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("YourAspect后置通知");}@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("YourAspect異常通知");}@After("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterAdvice(){System.out.println("YourAspect最終通知");}
}
4.5 簡化切點表達式
💡 使用@Pointcut注解定義切點表達式,方法名可以被其它注解作為切點表達式的key。
package com.powernode.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;// 切面類
@Component
@Aspect
@Order(2)
public class MyAspect {@Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")public void pointcut(){}@Around("pointcut()")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("環繞通知開始");// 執行目標方法。proceedingJoinPoint.proceed();System.out.println("環繞通知結束");}@Before("pointcut()")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("pointcut()")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("pointcut()")public void afterThrowingAdvice(){System.out.println("異常通知");}@After("pointcut()")public void afterAdvice(){System.out.println("最終通知");}}
十一、事務
1. 事務概述
📌 在一個業務流程中,多條DML語句要么全部成功要么全部失敗,這就叫做事務。
📌事務的處理過程
- 第一步:開啟事務
- 第二步:執行業務代碼
- 第三步:提交事務(commit)
- 第四步:回滾事務(rollback)(如果出現異常的情況下)
📌事務的四個特性
- 原子性:事務是最小工作單元,不可分割
- 一致性:要么同時成功,要么同時失敗
- 持久性:持久性是事務結束的標識
- 隔離性:不同事務之間不能互相影響
2. Spring對事務的支持(@Transactional)
前提:xmlns:tx="http://www.springframework.org/schema/tx"開啟事務支持
<tx:annotation-driven transaction-manager="transactionManager"/>
📌 首先開啟事務的支持,后面我們直接使用@Transactional
注解即可,添加在類和方法上都可,底層還是使用Aop實現的,原理和我們手動提交是一樣的,只不過使用Aop進行了簡化。
package com.powernode.bank.service.impl;import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;/*** @author 動力節點* @version 1.0* @className AccountServiceImpl* @since 1.0**/
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {@Resource(name = "accountDao")private AccountDao accountDao;@Overridepublic void transfer(String fromActno, String toActno, double money) {// 查詢賬戶余額是否充足Account fromAct = accountDao.selectByActno(fromActno);if (fromAct.getBalance() < money) {throw new RuntimeException("賬戶余額不足");}// 余額充足,開始轉賬Account toAct = accountDao.selectByActno(toActno);fromAct.setBalance(fromAct.getBalance() - money);toAct.setBalance(toAct.getBalance() + money);int count = accountDao.update(fromAct);// 模擬異常String s = null;s.toString();count += accountDao.update(toAct);if (count != 2) {throw new RuntimeException("轉賬失敗,請聯系銀行");}}
}
3. 事務的屬性
3.1 屬性的分類
事務中的重點屬性:
- 事務傳播行為
- 事務隔離級別
- 事務超時
- 只讀事務
- 設置出現哪些異常回滾事務
- 設置出現哪些異常不回滾事務
3.2 事務傳播行為
📌 在Service中,方法a和方法b都有事務,那這兩個事務是單獨的還是會合并為一個呢?這就是事務的傳播行為。
@Transactional(propagation = Propagation.REQUIRED)
📌 一共有七種傳播行為:
- REQUIRED:支持當前事務,如果不存在就新建一個(默認)
【沒有就新建,有就加入】
- SUPPORTS:支持當前事務,如果當前沒有事務,就以非事務方式執行
【有就加入,沒有就不管了】
- MANDATORY:必須運行在一個事務中,如果當前沒有事務正在發生,將拋出一個異常
【有就加入,沒有就拋異常】
- REQUIRES_NEW:開啟一個新的事務,如果一個事務已經存在,則將這個存在的事務掛起
【不管有沒有,直接開啟一個新事務,開啟的新事務和之前的事務不存在嵌套關系,之前事務被掛起】
- NOT_SUPPORTED:以非事務方式運行,如果有事務存在,掛起當前事務
【不支持事務,存在就掛起】
- NEVER:以非事務方式運行,如果有事務存在,拋出異常
【不支持事務,存在就拋異常】
- NESTED:如果當前正有一個事務在進行中,則該方法應當運行在一個嵌套式事務中。被嵌套的事務可以獨立于外層事務進行提交或回滾。如果外層事務不存在,行為就像REQUIRED一樣。
【有事務的話,就在這個事務里再嵌套一個完全獨立的事務,嵌套的事務可以獨立的提交和回滾。沒有事務就和REQUIRED一樣。】
Spring事務傳播行為選擇指南
傳播行為 | 當前有事務 | 當前無事務 | 適用場景 |
---|---|---|---|
REQUIRED | 加入當前事務 | 新建一個事務 | 大多數業務方法(默認行為) |
REQUIRES_NEW | 新建事務并掛起當前事務 | 新建一個事務 | 需要獨立事務的操作(如日志記錄) |
SUPPORTS | 加入當前事務 | 非事務方式執行 | 查詢方法,支持但不要求事務 |
NOT_SUPPORTED | 掛起當前事務,非事務執行 | 非事務方式執行 | 不需要事務支持的操作 |
MANDATORY | 加入當前事務 | 拋出異常 | 必須被事務方法調用的操作 |
NEVER | 拋出異常 | 非事務方式執行 | 禁止在事務中執行的操作 |
NESTED | 創建嵌套事務(保存點) | 新建一個事務 | 需要部分回滾的復雜業務 |
使用說明:
- REQUIRED:最常用,適用于大多數業務場景
- REQUIRES_NEW:用于需要獨立提交/回滾的操作
- NESTED:提供部分回滾能力,但并非所有數據庫都支持
- SUPPORTS/NOT_SUPPORTED:根據是否需要事務支持選擇
- MANDATORY/NEVER:用于強制/禁止事務環境
3.3 事務的隔離級別
📌 三大讀問題
- 臟讀:讀取到沒有提交到數據庫中的數據。
- 不可重復讀:同一個事務中兩次讀寫的數據不一致。
- 幻讀:讀取的數據是假的。
📌 四個隔離級別
- 讀未提交 READ_UNCOMMITTED
- 讀提交 READ_COMMITTED
- 可重復度 REPEATABLE_READ
- 序列化 SERIALIZABLE
事務隔離級別詳解
隔離級別 | 臟讀(Dirty Read) | 不可重復讀(Non-repeatable Read) | 幻讀(Phantom Read) | 典型實現方式 | 適用場景 |
---|---|---|---|---|---|
讀未提交 (Read Uncommitted) | ? 可能 | ? 可能 | ? 可能 | 讀不加鎖,寫加排他鎖 | 對一致性要求極低,追求最高性能 |
讀已提交 (Read Committed) | ? 避免 | ? 可能 | ? 可能 | 讀時加共享鎖(立即釋放),寫加排他鎖 | 多數數據庫默認級別,平衡一致性與性能 |
可重復讀 (Repeatable Read) | ? 避免 | ? 避免 | ? 可能* | 讀時加共享鎖(事務結束釋放),寫加排他鎖 | 需要事務內多次讀取一致的場景 |
串行化 (Serializable) | ? 避免 | ? 避免 | ? 避免 | 范圍鎖,完全串行化執行 | 最高一致性要求,如金融交易 |
*注:MySQL的InnoDB引擎在"可重復讀"級別通過MVCC機制避免了大部分幻讀問題
隔離級別選擇建議:
- 優先使用數據庫默認級別(通常為Read Committed)
- 對數據一致性要求高的場景使用Repeatable Read
- 只有特別敏感的業務(如資金結算)才考慮Serializable
- 幾乎不應該使用Read Uncommitted
在Spring中使用事務的隔離性
@Transactional(isolation = Isolation.READ_COMMITTED)
3.4 事務超時
@Transactional(timeout = 10)
📌 這里的超時時間指的是事務中最后一條DML語句之前的執行時間,超時了就回滾。
不計入超時時間
@Transactional(timeout = 10) // 設置事務超時時間為10秒。
public void save(Account act) {accountDao.insert(act);// 睡眠一會try {Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}
}
計入超時時間
@Transactional(timeout = 10) // 設置事務超時時間為10秒。
public void save(Account act) {// 睡眠一會try {Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}accountDao.insert(act);
}
3.5 只讀事務
@Transactional(readOnly = true)
📌 作用:啟用Spring的優化策略,提高查詢語句的效率,其余增刪改DML無法執行。
3.6 哪些異常回滾事務
@Transactional(rollbackFor = RuntimeException.class)
3.7 哪些異常不會滾事務
@Transactional(noRollbackFor = NullPointerException.class)
下期預告
老杜的筆記最后是有說明Spring的八大設計模式的,雖然在課程中詳細的學習了單例、原型鏈、簡單工廠、工廠模式、抽象工廠、代理模式、靜態代理、動態代理等,如果在加上其它的例如裝飾器模式
,說實話還是有一點混,所以決定重開一章單獨學習23種設計模式。