1 設計模式概述 軟體設計模式(Software Design Pattern),俗稱設計模式,設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結。它描述了在軟體設計過程中的一些不斷重覆發生的問題,以及該問題的解決方案。也就是說,它是解決特定問題的一系列套路,是前輩們的 ...
1 設計模式概述
軟體設計模式(Software Design Pattern),俗稱設計模式,設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結。它描述了在軟體設計過程中的一些不斷重覆發生的問題,以及該問題的解決方案。也就是說,它是解決特定問題的一系列套路,是前輩們的代碼設計經驗的總結,具有一定的普遍性,可以反覆使用。使用設計模式的目的是為了代碼重用、讓代碼更容易被他人理解、保證代碼可靠性。
設計模式:
設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結。它描述了在軟體設計過程中一些不斷重覆發生的問題,以及該問題的解決方案。
設計模式使用場景:
1、在程式設計上會使用到設計模式(巨集觀)
2、在軟體架構設計上會使用到設計模式(程式中的體現)
設計模式的目的:
1、提高代碼的可重用性
2、提高代碼的可讀性
3、保障代碼的可靠性
GOF
《Design Patterns: Elements of Reusable Object-Oriented Software》(即後述《設計模式》一書),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。這幾位作者常被稱為"四人組(Gang of Four)",而這本書也就被稱為"四人組(或 GOF)"書。
在《設計模式》這本書的最大部分是一個目錄,該目錄列舉並描述了 23 種設計模式。
GOF中共提到了23種設計模式不是孤立存在的,很多模式之間存在一定的關聯關係,在大的系統開發中常常同時使用多種設計模式。這23種設計模式根據功能作用來劃分,可以劃分為3類:
(1)創建型模式:用於描述“怎樣創建對象”,它的主要特點是“將對象的創建與使用分離”,單例、原型、工廠方法、抽象工廠、建造者5種設計模式屬於創建型模式。
(2)結構型模式:用於描述如何將類或對象按某種佈局組成更大的結構,代理、適配器、橋接、裝飾、外觀、享元、組合7種設計模式屬於結構型模式。
(3)行為型模式:用於描述類或對象之間怎樣相互協作共同完成單個對象都無法單獨完成的任務,以及怎樣分配職責。模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄、解釋器11種設計模式屬於行為型模式。
GOF的23種設計模式:
1、單例(Singleton)模式:某個類只能生成一個實例,該類提供了一個全局訪問點供外部獲取該實例,其拓展是有限多例模式。
2、原型(Prototype)模式:將一個對象作為原型,通過對其進行複製而克隆出多個和原型類似的新實例。
3、工廠方法(Factory Method)模式:定義一個用於創建產品的介面,由子類決定生產什麼產品。
4、抽象工廠(AbstractFactory)模式:提供一個創建產品族的介面,其每個子類可以生產一系列相關的產品。
5、建造者(Builder)模式:將一個複雜對象分解成多個相對簡單的部分,然後根據不同需要分別創建它們,最後構建成該複雜對象。
6、代理(Proxy)模式:為某對象提供一種代理以控制對該對象的訪問。即客戶端通過代理間接地訪問該對象,從而限制、增強或修改該對象的一些特性。
7、適配器(Adapter)模式:將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類能一起工作。
8、橋接(Bridge)模式:將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現,從而降低了抽象和實現這兩個可變維度的耦合度。
9、裝飾(Decorator)模式:動態的給對象增加一些職責,即增加其額外的功能。
10、外觀(Facade)模式:為多個複雜的子系統提供一個一致的介面,使這些子系統更加容易被訪問。
11、享元(Flyweight)模式:運用共用技術來有效地支持大量細粒度對象的復用。
12、組合(Composite)模式:將對象組合成樹狀層次結構,使用戶對單個對象和組合對象具有一致的訪問性。
13、模板方法(TemplateMethod)模式:定義一個操作中的演算法骨架,而將演算法的一些步驟延遲到子類中,使得子類可以不改變該演算法結構的情況下重定義該演算法的某些特定步驟。
14、策略(Strategy)模式:定義了一系列演算法,並將每個演算法封裝起來,使它們可以相互替換,且演算法的改變不會影響使用演算法的客戶。
15、命令(Command)模式:將一個請求封裝為一個對象,使發出請求的責任和執行請求的責任分割開。
16、職責鏈(Chain of Responsibility)模式:把請求從鏈中的一個對象傳到下一個對象,直到請求被響應為止。通過這種方式去除對象之間的耦合。
17、狀態(State)模式:允許一個對象在其內部狀態發生改變時改變其行為能力。
18、觀察者(Observer)模式:多個對象間存在一對多關係,當一個對象發生改變時,把這種改變通知給其他多個對象,從而影響其他對象的行為。
19、中介者(Mediator)模式:定義一個中介對象來簡化原有對象之間的交互關係,降低系統中對象間的耦合度,使原有對象之間不必相互瞭解。
20、迭代器(Iterator)模式:提供一種方法來順序訪問聚合對象中的一系列數據,而不暴露聚合對象的內部表示。
21、訪問者(Visitor)模式:在不改變集合元素的前提下,為一個集合中的每個元素提供多種訪問方式,即每個元素有多個訪問者對象訪問。
22、備忘錄(Memento)模式:在不破壞封裝性的前提下,獲取並保存一個對象的內部狀態,以便以後恢復它。
23、解釋器(Interpreter)模式:提供如何定義語言的放法,以及對語言句子的解釋方法,即解釋器。
2 單例模式
單例模式(Singleton Pattern)是 Java 中最常見的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
單例模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。該類還提供了一種訪問它唯一對象的方式,其他類可以直接訪問該方法獲取該對象實例,而不需要實例化該類的對象。
單例模式特點:
1、單例類只能有一個實例。 A a = new A()
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
單例模式優點:
1、在記憶體里只有一個實例,減少了記憶體的開銷,尤其是頻繁的創建和銷毀實例。
2、避免對資源的多重占用(比如寫文件操作)。
單例模式真實應用場景:
1、網站的計數器
2、應用程式的日誌應用
3、資料庫連接池設計
4、多線程的線程池設計
2.1 單例模式-餓漢式
創建一個單例對象SingleModel
,SingleModel
類有它的私有構造函數和本身的一個靜態實例。
SingleModel
類提供了一個靜態方法,供外界獲取它的靜態實例。DesignTest
我們的演示類使用SingleModel
類來獲取 SingleModel
對象。
創建SingleModel
:
public class SingleModel {
//創建 SingleModel 的一個對象
private static SingleModel instance = new SingleModel();
//讓構造函數為 private,這樣該類就不會被實例化
private SingleModel(){}
//獲取唯一可用的對象
public static SingleModel getInstance(){
return instance;
}
public void useMessage(){
System.out.println("Single Model!");
}
}
單例測試:
public class DemoTest {
/****
* 單例模式測試
*/
@Test
public void testSingleModel(){
//不合法的構造函數
//編譯時錯誤:構造函數 SingleModel() 是不可見的
//SingleModel singleModel = new SingleModel();
//獲取唯一可用的對象
SingleModel singleModel1 = SingleModel.getInstance();
SingleModel singleModel2 = SingleModel.getInstance();
//顯示消息
singleModel1.useMessage();
//創建的2個對象是同一個對象
System.out.println(singleModel1 == singleModel2);
}
}
輸入結果如下:
Single Model!
true
我們測試創建10萬個對象,用單例模式創建,僅占記憶體:104
位元組,而如果用傳統方式創建10萬個對象,占記憶體大小為2826904
位元組。
2.2 多種單例模式講解
單例模式有多種創建方式,剛纔創建方式沒有特別的問題,但是程式啟動就需要創建對象,不管你用不用到對象,都會創建對象,都會消耗一定記憶體。因此在單例的創建上出現了多種方式。
懶漢式:
懶漢式有這些特點:
1、延遲載入創建,也就是用到對象的時候,才會創建
2、線程安全問題需要手動處理(不添加同步方法,線程不安全,添加了同步方法,效率低)
3、實現容易
案例如下:SingleModel1
如果在創建對象實例的方法上添加同步synchronized
,但是這種方案效率低,代碼如下:
雙重校驗鎖:SingleModel2
這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。
public class SingleModel2 {
//不實例化
private static SingleModel2 instance;
//讓構造函數為 private,這樣該類就不會被實例化
private SingleModel2(){}
//獲取唯一可用的對象
public static SingleModel2 getInstance(){
//instance為空的時候才創建對象
if(instance==null){
//同步鎖,效率比懶漢式高
synchronized (SingleModel2.class){
//這裡需要判斷第2次為空
if(instance==null){
instance = new SingleModel2();
}
}
}
return instance;
}
public void useMessage(){
System.out.println("Single Model!");
}
}
指令重排問題解決
對象創建,一般正確流程如下:
1:申請記憶體空間
2:創建對象
3:將創建的對象指向申請的記憶體空間地址
但其實在對象創建的時候,也有可能發生 指令重排問題,也就是上面流程會被打亂:
1:申請記憶體空間
2:將創建的對象指向申請的記憶體空間地址
3:創建對象
如果是這樣的話,雙檢鎖在多線程情況下也會出現問題,需要添加volatile
屬性,該屬性能防止指令重排,代碼如下:
public class SingleModel2 {
//不實例化
private static volatile SingleModel2 instance;
//讓構造函數為 private,這樣該類就不會被實例化
private SingleModel2(){}
//獲取唯一可用的對象
public static SingleModel2 getInstance(){
//instance為空的時候才創建對象
if(instance==null){
//同步鎖,效率比懶漢式高
synchronized (SingleModel2.class){
//這裡需要判斷第2次為空
if(instance==null){
instance = new SingleModel2();
}
}
}
return instance;
}
public void useMessage(){
System.out.println("Single Model!");
}
}
3 SpringAOP代理模式
Spring是一個分層的JavaSE/EE full-stack(一站式) 輕量級開源框架,非常受企業歡迎,他解決了業務邏輯層和其他各層的松耦合問題,它將面向介面的編程思想貫穿整個系統應用。在Spring源碼中擁有多個優秀的設計模式使用場景,有非常高的學習價值。
3.1 代理模式
定義:
給某對象提供一個代理對象,通過代理對象可以訪問該對象的功能。主要解決通過代理去訪問[不能直接訪問的對象],例如租房中介,你可以直接通過中介去瞭解房東的房源信息,此時中介就可以稱為代理。
優點:
1、職責清晰。
2、高擴展性。
3、智能化。
缺點:
1、由於在客戶端和真實主題之間增加了代理對象,因此有些類型的代理模式可能會造成請求的處理速度變慢。
2、實現代理模式需要額外的工作,有些代理模式的實現非常複雜。
代理實現方式:(代理實現技術方案)
基於介面的動態代理
提供者:JDK官方的Proxy類。
要求:被代理類最少實現一個介面。
基於子類的動態代理
提供者:第三方的CGLib,如果報asmxxxx異常,需要導入asm.jar。
要求:被代理類不能用final修飾的類(最終類)。
3.2 JDK動態代理
JDK動態代理要點:
1、被代理的類必須實現一個介面
2、用JDK代理,被代理的過程需要實現InvocationHandler
3、代理過程在invoke中實現
4、創建代理對象Proxy.newProxyInstance實現
我們以王五租房為例,王五通過中介直接租用戶主房屋,中介在這裡充當代理角色,戶主充當被代理角色。
創建房東介面對象:LandlordService
public interface LandlordService {
void rentingPay(String name);
}
創建房東對象:Landlord
public class Landlord implements LandlordService{
/****
* @param name
*/
@Override
public void rentingPay(String name){
System.out.println(name+" 來交租!");
}
}
創建代理處理過程對象:QFangProxy
public class QFangProxy implements InvocationHandler{
private Object instance;
public QFangProxy(Object instance) {
this.instance = instance;
}
/****
* 代理過程
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
args[0] = "中介QFang帶領租戶"+args[0];
Object result = method.invoke(instance, args);
return result;
}
}
創建代理,並通過代理調用房東方法:JdkProxyTest
public class JdkProxyTest {
public static void main(String[] args) {
//給QFang產生代理
LandlordService landlordService = new Landlord();
QFangProxy proxy = new QFangProxy(landlordService);
LandlordService landlordServiceProxy = (LandlordService) Proxy.newProxyInstance(LandlordService.class.getClassLoader(), new Class[]{LandlordService.class}, proxy);
//通過代理對象調用Landlord對象的方法
landlordServiceProxy.rentingPay("王五");
}
}
運行結果如下:
中介QFang帶領客戶 來交租!
3.3 CGLib動態代理
CGLib動態代理要點:
1、代理過程可以實現MethodInterceptor(Callback)介面中的invoke來實現
2、通過Enhancer來創建代理對象
在上面的案例基礎上,把QFangProxy
換成SFangProxy
,代碼如下:
public class SFangProxy implements MethodInterceptor {
private Object instance;
public SFangProxy(Object instance) {
this.instance = instance;
}
/***
* 代理過程
* @throws Throwable
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
args[0]="S房網帶租戶"+args[0];
return method.invoke(instance,args);
}
}
創建測試類:CGLibProxyTest
,代碼如下
public class CGLibProxyTest {
public static void main(String[] args) {
//給QFang產生代理
LandlordService landlordService = new Landlord();
SFangProxy proxy = new SFangProxy(landlordService);
LandlordService landlordServiceProxy = (LandlordService) Enhancer.create(LandlordService.class,proxy);
//通過代理對象調用Landlord對象的方法
landlordServiceProxy.rentingPay("王五");
}
}
3.4 Spring AOP-動態代理
基於SpringAOP可以實現非常強大的功能,例如聲明式事務、基於AOP的日誌管理、基於AOP的許可權管理等功能,利用AOP可以將重覆的代碼抽取,重覆利用,節省開發時間,提升開發效率。Spring的AOP其實底層就是基於動態代理而來,並且支持JDK動態代理和CGLib動態代理,動態代理的集中體現在DefaultAopProxyFactory
類中,我們來解析下DefaultAopProxyFactory
類。
如果我們在spring的配置文件中不配置<aop:config proxy-target-class="true">
,此時預設使用的將是JDK動態代理,如果配置了,則會使用CGLib動態代理。
JDK動態代理的創建JdkDynamicAopProxy
如下:
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
//創建代理對象
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//JDK動態代理過程
}
}
}
CGLib動態代理的創建ObjenesisCglibAopProxy
如下:
class ObjenesisCglibAopProxy extends CglibAopProxy {
//CGLib動態代理創建過程
@Override
@SuppressWarnings("unchecked")
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
Class<?> proxyClass = enhancer.createClass();
Object proxyInstance = null;
if (objenesis.isWorthTrying()) {
try {
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
}
catch (Throwable ex) {
logger.debug("Unable to instantiate proxy using Objenesis, " +
"falling back to regular proxy construction", ex);
}
}
if (proxyInstance == null) {
// Regular instantiation via default constructor...
try {
Constructor<?> ctor = (this.constructorArgs != null ?
proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
proxyClass.getDeclaredConstructor());
ReflectionUtils.makeAccessible(ctor);
proxyInstance = (this.constructorArgs != null ?
ctor.newInstance(this.constructorArgs) : ctor.newInstance());
}
catch (Throwable ex) {
throw new AopConfigException("Unable to instantiate proxy using Objenesis, " +
"and regular proxy instantiation via default constructor fails as well", ex);
}
}
((Factory) proxyInstance).setCallbacks(callbacks);
return proxyInstance;
}
}
3.5 代理模式-文件服務實戰
設計模式如果只是去學習他的模式,而不投入實際應用,其實無異於閉門造豬,因此我們要將設計模式投入實際開發使用才是對設計模式真正的領悟。
案例:根據文件類型,將文件存儲到不同服務
代理模式:
給一個對象創建一個代理對象,通過代理對象可以使用該對象的功能。
CGLib和JDK是代理模式實現的技術方案。
3.5.1 文件服務應用
代理模式的應用場景除了代碼級別,還可以將代理模式遷移到應用以及架構級別,如下圖文件上傳代理服務,針對一些圖片小文件,我們可以直接把文件存儲到FastDFS
服務,針對大文件,例如商品視頻介紹,我們可以把它存儲到第三方OSS
。
用戶通過文件上傳代理服務可以間接訪問OSS和本地FastDFS,這種分散式海量文件管理解決方案,這裡不僅在代碼層面充分運用了代理模式,在架構層面也充分運用了代理模式。
3.5.2 分散式文件代理伺服器實現
1)實現分析
基於代理模式,我們實現文件上傳分別路由到aliyunOSS
和FastDFS
,用例圖如下:
講解:
1、FileUpload抽象介面,定義了文件上傳方法,分別給它寫了2種實現。
2、AliyunOSSFileUpload是將文件上傳到aliyunOSS,主要上傳mp4和avi的視頻大文件。
3、FastdfsFileUpoad是將文件上傳到FastDFS,主要上傳png/jpg等圖片小文件。
4、FileUploadProxy是代理對象,供用戶訪問,調用了FileUpload的文件上傳方法,為用戶提供不同文件上傳調用。
5、FileController是控制器,用於接收用戶提交的文件,並調用代理FileUploadProxy實現文件上傳。
2)代碼實現
bootstrap.yml配置:
server:
port: 18081
logging:
level:
#root: debug開啟dubug級別
com.seckill.goods.dao: error
pattern:
console: "%msg%n"
#對應實例的id和需要處理的文件類型的映射關係
upload:
filemap:
aliyunOSSFileUpload: avi,mp4
fastdfsFileUpoad: png,jpg
#FastDFS配置
fastdfs:
url: http://192.168.211.137:28181/
#aliyun
aliyun:
oss:
endpoint: oss-cn-beijing.aliyuncs.com
accessKey: a7i6rVEjbtaJdYX2
accessKeySecret: MeSZPybPHfJtsYCRlEaUbfRtdH8gl4
bucketName: sklll
key: video/
backurl: https://sklll.oss-cn-beijing.aliyuncs.com/video/ #訪問地址配置
spring:
application:
name: seckill-goods
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
servlet:
multipart:
max-file-size: 100MB #上傳文件大小配置
FileUpload介面定義:
public interface FileUpload {
/***
* 文件上傳
* @param buffers:文件位元組數組
* @param extName:尾碼名
* @return
*/
String upload(byte[] buffers,String extName);
}
AliyunOSSFileUpload實現:
@Component(value = "aliyunOSSFileUpload")
public class AliyunOSSFileUpload implements FileUpload{
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKey}")
private String accessKey;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.key}")
private String key;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${aliyun.oss.backurl}")
private String backurl;
/****
* 文件上傳
* 文件類型如果是圖片,則上傳到本地FastDFS
* 文件類型如果是視頻,則上傳到aliyun OSS
*/
@Override
public String upload(byte[] buffers,String extName) {
String realName = UUID.randomUUID().toString()+"."+extName;
// 創建OSSClient實例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKey, accessKeySecret);
// <yourObjectName>表示上傳文件到OSS時需要指定包含文件尾碼在內的完整路徑,例如abc/efg/123.jpg。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key+realName, new ByteArrayInputStream(buffers));
// 上傳字元串。
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(FileUtil.getContentType("."+extName));
putObjectRequest.setMetadata(objectMetadata);
ossClient.putObject(putObjectRequest);
// 關閉OSSClient。
ossClient.shutdown();
return backurl+realName;
}
}
FastdfsFileUpoad實現:
@Component(value = "fastdfsFileUpoad")
public class FastdfsFileUpoad implements FileUpload {
@Value("${fastdfs.url}")
private String url;
/***
* 文件上傳
* @param buffers:文件位元組數組
* @param extName:尾碼名
* @return
*/
@Override
public String upload(byte[] buffers, String extName) {
/***
* 文件上傳後的返回值
* uploadResults[0]:文件上傳所存儲的組名,例如:group1
* uploadResults[1]:文件存儲路徑,例如:M00/00/00/wKjThF0DBzaAP23MAAXz2mMp9oM26.jpeg
*/
String[] uploadResults = null;
try {
//獲取StorageClient對象
StorageClient storageClient = getStorageClient();
//執行文件上傳
uploadResults = storageClient.upload_file(buffers, extName, null);
return url+uploadResults[0]+"/"+uploadResults[1];
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/***
* 初始化tracker信息
*/
static {
try {
//獲取tracker的配置文件fdfs_client.conf的位置
String filePath = new ClassPathResource("fdfs_client.conf").getPath();
//載入tracker配置信息
ClientGlobal.init(filePath);
} catch (Exception e) {
e.printStackTrace();
}
}
/***
* 獲取StorageClient
* @return
* @throws Exception
*/
public static StorageClient getStorageClient() throws Exception{
//創建TrackerClient對象
TrackerClient trackerClient = new TrackerClient();
//通過TrackerClient獲取TrackerServer對象
TrackerServer trackerServer = trackerClient.getConnection();
//通過TrackerServer創建StorageClient
StorageClient storageClient = new StorageClient(trackerServer,null);
return storageClient;
}
}
FileUploadProxy代理實現:
@Data
@Component
@ConfigurationProperties(prefix = "upload")
public class FileUploadProxy implements ApplicationContextAware{
private ApplicationContext act;
//aliyunOSSFileUpload -> mp4,avi
private Map<String,List<String>> filemap;
/***
* 文件上傳
* @param file:上傳的文件
* @return
*/
public String upload(MultipartFile file) throws Exception{
//文件名字 1.mp4
String fileName = file.getOriginalFilename();
//擴展名 mp4,jpg
String extName = StringUtils.getFilenameExtension(fileName);
//迴圈filemap
for (Map.Entry<String, List<String>> entry : filemap.entrySet()) {
for (String suffix : entry.getValue()) {
//匹配當前extName和當前map中對應的類型是否匹配
if(extName.equalsIgnoreCase(suffix)){
//一旦匹配,則把key作為唯一值,從容器中獲取對應實例
return act.getBean(entry.getKey(), FileUpload.class).upload(file.getBytes(),extName);
}
}
}
return null;
}
//註入容器對象
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.act=applicationContext;
}
}
FileController控制器實現:
@RestController
@RequestMapping(value = "/file")
public class FileController {
@Autowired
private FileUploadProxy fileUploadProxy;
/***
* 文件上傳
* @param file
* @return
* @throws IOException
*/
@PostMapping(value = "/upload")
public String upload(MultipartFile file) throws IOException {
return fileUploadProxy.upload(file.getBytes(), StringUtils.getFilenameExtension(file.getOriginalFilename()));
}
}
文件上傳預覽效果:
<https://sklll.oss-cn-beijing.aliyuncs.com/video/77df7ada-4eea-4698-bfc5-bedd2c16f240.mp4>
FastDFS地址:
<http://192.168.211.137:28181/group1/M00/00/00/wKjTiV7kLtGASw5TAADJ9uXzZAQ622.png>
4 享元模式
定義:
運用共用技術來有効地支持大量細粒度對象的復用。它通過共用已經存在的對象來大幅度減少需要創建的對象數量、避免大量相似類的開銷,從而提高系統資源的利用率。
享元模式和單利的區別:
單利是對象只能自己創建自己,整個應用中只有1個對象
享元模式根據需要共用,不限制被誰創建(有可能有多個對象實例)
優點:
特定環境下,相同對象只要保存一份,這降低了系統中對象的數量,從而降低了系統中細粒度對象給記憶體帶來的壓力。
缺點:
為了使對象可以共用,需要將一些不能共用的狀態外部化,這將增加程式的複雜性。
4.1 享元模式實戰
案例:用戶下單,會話共用
4.2 會話跟蹤分析
會話跟蹤,如果是傳統項目用Session或者是Cookie,全項目通用,但在微服務項目中,不用Session也不用Cookie,所以想要在微服務項目中實現會話跟蹤,是有一定難度的。
當前微服務項目中,身份識別的主流方法是前端將用戶令牌存儲到請求頭中,每次請求將請求頭中的令牌攜帶到後臺,後臺每次從請求頭中獲取令牌來識別用戶身份。
我們在項目操作過程中,很多地方都會用到用戶身份信息,比如下訂單的時候,要知道當前訂單屬於哪個用戶,記錄下單關鍵日誌的時候,需要記錄用戶操作的信息以及用戶信息,關鍵日誌記錄我們一般用AOP進行攔截操作,此時沒法直接把用戶身份信息傳給AOP。這個時候我們可以利用享元模式實現用戶會話信息共用操作。操作流程如下圖:
4.3 會話共用案例實現
基於上面的分析,我們採用享元模式實現用戶會話共用操作,要解決如下幾個問題:
1、用戶會話共用
2、會話多線程安全
3、訂單數據用戶信息獲取
4、AOP日誌記錄用戶信息獲取
定義共用組件:Session
Session
裡面定義了每個線程中不變的用戶身份信息username
、role
、sex
,其他的是可能存在變化的數據可以寫一個類繼承該類。
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public abstract class Session {
//需要共用的用戶信息
private String username;
private String name;
private String sex;
private String role;
private Integer level;
//擴展方法
public abstract void handler();
}
享元組件邏輯操作對象:SessionShar
SessionShar
該對象主要用於給當前線程填充共用數據,以及變更訪問方法和訪問信息等信息的邏輯操作,代碼如下:
public class SessionShar extends Session {
//方便實例化
public SessionShar(String username, String name, String sex, String role, Integer level) {
super(username, name, sex, role, level);
}
/***
* 擴展對象
*/
@Override
public void handler() {
System.out.println("擴展功能!");
}
}
多線程安全控制:ThreadSession
每個線程請求的時候,我們需要保障會話安全,比如A線程訪問和B線程訪問,他們的用戶會話身份不能因為併發原因而發生混亂。這裡我們可以採用ThreadLocal來實現。我們創建一個ThreadSession
對象,併在該對象中創建ThreadLocal<Session>
用戶存儲每個線程的會話信息,並實現ThreadLocal<Session>
的操作,代碼如下:
@Component
public class ThreadSession {
//存儲需要共用的對象
private static ThreadLocal<Session> sessions = new ThreadLocal<Session>();
/****
* 添加用戶信息記錄
*/
public void add(Session session){
sessions.set(session);
}
/****
* 獲取LogComponent
*/
public Session get(){
return sessions.get();
}
/****
* 移除
*/
public void remove(){
sessions.remove();
}
}
線程會話初始化:AuthorizationInterceptor
AuthorizationInterceptor
攔截器的作用是用於初始化用戶訪問的時候用戶的身份信息,並將身份信息存儲到ThreadSession
的ThreadLocal
中,在用戶訪問方法結束,銷毀ThreadSession
的ThreadLocal
中會話,代碼如下:
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {
@Autowired
private ThreadSession threadSession;
/****
* 將用戶會話存儲到ThreadLocal中
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
//獲取令牌
String authorization = request.getHeader("token");
//解析令牌
if(!StringUtils.isEmpty(authorization)){
Map<String, Object> tokenMap = JwtTokenUtil.parseToken(authorization);
//封裝用戶身份信息,存儲到ThreadLocal中,供當前線程共用使用
//1.封裝需要共用的信息
//2.創建一個對象繼承封裝信息,每次共用該對象 (不需要共用,則可以創建另外一個對象繼承它)
//3.創建共用管理對象,實現共用信息的增加、獲取、移除功能
threadSession.add(new SessionShar(
tokenMap.get("username").toString(),
tokenMap.get("name").toString(),
tokenMap.get("sex").toString(),
tokenMap.get("role").toString(),
Integer.valueOf(tokenMap.get("level").toString())
));
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
//輸出令牌校驗失敗
response.setContentType("application/json;charset=utf-8");
response.getWriter().print("身份校驗失敗!");
response.getWriter().close();
return false;
}
/**
* 移除會話信息
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
threadSession.remove();
}
}
共用信息使用:
①AOP記錄日誌:創建AOP切麵類LogAspect
用於記錄日誌,代碼如下:
@Component
@Aspect
@Slf4j
public class LogAspect {
@Autowired
private ThreadSession threadSession;
/***
* 記錄日誌
*/
@SneakyThrows
@Before("execution(int com.itheima.shop.service.impl.*.*(..))")
public void logRecode(JoinPoint joinPoint){
//獲取方法名字和參數
String methodName = joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName();
//記錄日誌
log.info("用戶【"+threadSession.get().toString()+"】訪問:"+methodName);
}
/****
* 參數獲取
*/
public String args(Object[] args){
StringBuffer buffer = new StringBuffer();
for (int i = 0; i <args.length ; i++) {
buffer.append(" args("+i+"):"+args[i].toString());
}
return buffer.toString();
}
}
②添加訂單獲取用戶信息:在添加訂單方法OrderServiceImpl.add(Order order)
中,從ThreadSession中獲取用戶會話,並填充給Order,代碼如下:
添加訂單,日誌輸出可以看到調用添加訂單和修改庫存時,都記錄了日誌,並且獲取了用戶會話,效果如下:
LogComponent(username=zhaoliu, sex=男, role=ROLE_USER, methodName=com.itheima.shop.service.impl.OrderServiceImpl.add, message= args(0):Order(itemId=1, id=1, money=9999, status=1, num=1, username=null))
LogComponent(username=zhaoliu, sex=男, role=ROLE_USER, methodName=com.itheima.shop.service.impl.ItemServiceImpl.modify, message= args(0):1 args(1):1)
添加的訂單資料庫數據中也擁有用戶信息,效果如下:
5 裝飾者模式
定義:
動態的向一個現有的對象添加新的功能,同時又不改變其結構。它屬於結構型模式。
擴展新功能,不需要修改現有對象就能實現--->裝飾者模式
優點:
裝飾類和被裝飾類可以獨立發展,不會相互耦合,裝飾模式是繼承的一個替代模式,裝飾模式可以動態擴展一個實現類的功能。
缺點:
多層裝飾比較複雜。
5.1 裝飾者模式實戰
案例:結算價格計算,根據不同價格嵌套運算
5.2 訂單結算價格實戰
在訂單提交的時候,訂單價格和結算價格其實是兩碼事,訂單價格是當前商品成交價格,而結算價格是用戶最終需要支付的金額,最終支付的金額並不是一成不變,它也並不是商品成交價格,能改變結算價格的因素很多,比如滿100減10元,VIP用戶再減5塊。訂單結算金額計算我們就可以採用裝飾者模式。
5.3 裝飾者模式價格運算實現
實現思路分析:
1、創建介面(MoneyOperation),定義訂單價格計算,因為所有價格波動,都是基於訂單價格來波動的。
2、創建訂單價格計算類(OrderPayMoneyOperation),實現MoneyOperation介面,實現訂單價格計算。
3、創建裝飾者對象(Decorator),以供功能擴展。
4、實現優惠券優惠金額計算功能擴展,創建Decorator的擴展類CouponsMoneyOperation,先計算訂單金額,再計算優惠券使用之後的優惠金額。
5、實現金幣抵現功能擴展,創建Decorator的擴展類GoldMoneyOperation,先計算訂單金額,再實現金幣優惠之後的金額。
基礎介面:創建介面MoneySum
,該介面只用於定義計算訂單金額的方法。
public interface MoneySum {
//訂單金額求和計算
void sum(Order order);
}
訂單金額計算類:創建類OrderPayMoneyOperation
實現訂單金額的計算。
@Component(value = "orderMoneySum")
public class OrderMoneySum implements MoneySum {
@Autowired
private ItemDao itemDao;
//總金額計算
@Override
public void sum(Order order) {
//商品單價*總數量
Item item = itemDao.findById(order.getItemId());
order.setPaymoney(item.getPrice()*order.getNum());
order.setMoney(item.getPrice()*order.getNum());
}
}
裝飾者類:創建裝飾者類DecoratorMoneySum
供其他類擴展。
public class DecoratorMoneySum implements MoneySum {
private MoneySum moneySum;
public void setMoneySum(MoneySum moneySum) {
this.moneySum = moneySum;
}
//計算金額
@Override
public void sum(Order order) {
moneySum.sum(order);
}
}
滿100減10元價格計算:創建類FullMoneySum
擴展裝飾者類,實現滿減價格計算。
@Component(value = "fullMoneySum")
public class FullMoneySum extends DecoratorMoneySum{
//原來的功能上進行增強
@Override
public void sum(Order order) {
//原有功能
super.sum(order);
//增強
moneySum(order);
}
//滿100減5塊
public void moneySum(Order order){
Integer paymoney = order.getPaymoney();
if(paymoney>=100){
order.setPaymoney(paymoney-10);
}
}
}
VIP優惠10元價格計算:創建類VipMoneySum
,實現VIP優惠計算。
@Component(value = "vipMoneySum")
public class VipMoneySum extends DecoratorMoneySum {
//原有方法上增強
@Override
public void sum(Order order) {
//原有功能
super.sum(order);
//增強
vipMoneySum(order);
}
//Vip價格優惠-5
public void vipMoneySum(Order order){
order.setPaymoney(order.getPaymoney()-5);
}
}
支付金額計算:修改OrderServiceImpl
的add()
方法,添加訂單金額以及訂單支付金額的計算功能,代碼如下:
測試效果:
測試數據中,我們選擇購買1件商品,當前登錄用戶為王五,擁有5個金幣,當前購買的商品id=1,商品單價是150元,滿減100,VIP優惠5元,最終支付135元。
{
"itemId":"1",
"id":"1",
"status":1,
"num":1,
"couponsId":"1"
}
測試生成的訂單如下:
不僅如此,我們可以隨時撤掉滿減和Vip優惠功能。
6 策略模式
定義:
策略模式是對演算法的包裝,把演算法的使用和演算法本身分隔開,委派給不同的對象管理。策略模式通常把一系列的演算法包裝到一系列的策略類裡面,作為一個抽象策略類的子類或者介面的實現類。
簡單來說就是就定義一個策略介面,策略類去實現該介面去定義不同的策略。然後定義一個環境(Context,也就是需要用到策略的對象)類,以策略介面作為成員變數,根據環境來使用具體的策略。
優點:
1、演算法可以自由切換。
2、避免使用多重條件判斷。
3、擴展性良好。
缺點:
1、策略類會增多。
2、所有策略類都需要對外暴露。
6.1 策略模式實戰
案例:結算價格計算,根據Vip不同等級進行運算
6.2 不同VIP優惠價格分析
用戶在購買商品的時候,很多時候會根據Vip等級打不同折扣,尤其是線上商城中體現的淋漓盡致。我們這裡也基於真實電商案例來實現VIP等級價格制:
Vip0->普通價格
Vip1->減5元
Vip2->7折
Vip3->5折
6.3 代碼實現
定義策略介面:Strategy
public interface Strategy {
//價格計算
Integer payMoney(Integer payMoney);
}
定義Vip0策略:StrategyVipOne
@Component(value = "strategyVipOne")
public class StrategyVipOne implements Strategy {
//普通會員,沒有優惠
@Override
public Integer payMoney(Integer payMoney) {
return payMoney;
}
}
定義Vip1策略:StrategyVipTwo
@Component(value = "strategyVipTwo")
public class StrategyVipTwo implements Strategy{
//策略2
@Override
public Integer payMoney(Integer payMoney) {
return payMoney-5;
}
}
定義Vip2策略:StrategyVipThree
@Component(value = "strategyVipThree")
public class StrategyVipThree implements Strategy{
//策略3
@Override
public Integer payMoney(Integer payMoney) {
return (int)(payMoney*0.7);
}
}
定義Vip3策略:StrategyVipFour
@Component(value = "strategyVipFour")
public class StrategyVipFour implements Strategy{
//策略4
@Override
public Integer payMoney(Integer payMoney) {
return (int)(payMoney*0.5);
}
}
定義策略工廠:StrategyFactory
@Data
@ConfigurationProperties(prefix = "strategy")
@Component
public class StrategyFactory implements ApplicationContextAware{
//ApplicationContext
//1、定義一個Map存儲所有策略【strategyVipOne=instanceOne】
// 【strategyVipTwo=instanceTwo】
private ApplicationContext act;
//定義一個Map,存儲等級和策略的關係,通過application.yml配置註入進來
private Map<Integer,String> strategyMap;
//3、根據會員等級獲取策略【1】【2】【3】
public Strategy getStrategy(Integer level){
//根據等級獲取策略ID
String id = strategyMap.get(level);
//根據ID獲取對應實例
return act.getBean(id,Strategy.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
act=applicationContext;
}
}
等級策略配置:修改application.yml,將如下策略配置進去
#策略配置
strategy:
strategyMap:
1: strategyVipOne
2: strategyVipTwo
3: strategyVipThree
4: strategyVipFour
等級控制:修改UserHandler
添加等級屬性
修改UserHandlerShare
定義等級,代碼如下:
裝飾者模式中修改VipMoneySum
的價格運算,代碼如下:
測試:
本文由
傳智教育博學谷
教研團隊發佈。如果本文對您有幫助,歡迎
關註
和點贊
;如果您有任何建議也可留言評論
或私信
,您的支持是我堅持創作的動力。轉載請註明出處!