掌握設計模式 -- 裝飾模式
裝飾模式(Decorator Pattern)
裝飾模式是一種結構型設計模式,旨在在不改變原有對象結構的情況下動態地爲對象添加功能。通過將對象封裝到一系列裝飾器類中,可以以靈活和透明的方式擴展功能。
如果要擴展功能,裝飾模式提供了比繼承
更有彈性的替代方案,裝飾模式強調的是功能的擴展和靈活組合
。
裝飾模式強調的是擴展對象的功能
及擴展功能的組合
。比如,對象 A,需要擴展 X、Y 的功能,這些擴展功能按不同的順序組合可以實現不同的效果,先執行 X 再執行 Y 擴展功能或者先執行 Y 再執行 X 擴展功能。而繼承實現的只是單一的順序擴展功能,並且繼承是單一的。看後面的例子就很好理解了。
結構
通常包含一個核心對象
和若干裝飾對象
,這些裝飾對象通過引用同一接口或抽象類實現功能的疊加。
裝飾模式包含以下主要角色:
-
Component(組件接口)
定義核心對象的公共接口,允許動態添加行爲。 -
ConcreteComponent(具體組件)
具體實現了Component
接口的類,表示被裝飾的核心對象。 -
Decorator(裝飾器抽象類)
實現Component
接口,同時持有一個Component
的引用,表示對該對象的包裝。 -
ConcreteDecorator(具體裝飾器)
在裝飾器抽象類的基礎上,添加具體的功能。
代碼示例
實現對Socket
報文的多層加密
,加密順序可隨意組合
。示例中使用到了國密 SM4 加密和 Base64 編碼。Socket 報文可以先加密再編碼,也可以先編碼在加密,跟根據不同組合來實現不同的增強。
類圖
定義核心接口
public interface SocketStream {
void writeData(String data) throws IOException;
String readData() throws IOException;
void close() throws IOException;
}
被裝飾的核心對象
public class SimpleSocketStream implements SocketStream {
private Socket socket;
private OutputStream outputStream;
private InputStream inputStream;
public SimpleSocketStream(Socket socket){
this.socket = socket;
try {
this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void writeData(String data) throws IOException {
outputStream.write(data.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
socket.shutdownOutput();
}
@Override
public String readData() throws IOException {
// 讀取客戶端請求並解密
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String message = reader.readLine();
return message;
}
@Override
public void close() throws IOException {
if(inputStream!=null)
inputStream.close();
if(outputStream!=null)
outputStream.close();
if(socket!=null)
socket.close();
}
}
抽象裝飾器
public abstract class SocketStreamDecorator implements SocketStream{
private SocketStream socketStream;
public SocketStreamDecorator(SocketStream socketStream){
this.socketStream = socketStream;
}
@Override
public void writeData(String data) throws IOException {
socketStream.writeData(data);
}
@Override
public String readData() throws IOException {
return socketStream.readData();
}
@Override
public void close() throws IOException {
socketStream.close();
}
}
具體裝飾器 1--SM4 加密處理
public class SM4CipherSocketStreamDecorator extends SocketStreamDecorator{
public SM4CipherSocketStreamDecorator(SocketStream socketStream) {
super(socketStream);
}
@Override
public void writeData(String data) throws IOException {
// 加密
String sm4Encrypt = SM4EncryptUtil.sm4Encrypt(data);
System.out.println("--SM4加密數據:"+sm4Encrypt);
super.writeData(sm4Encrypt);
}
@Override
public String readData() throws IOException {
String readData = super.readData();
// 解密
System.out.println("--SM4解密前數據:"+readData);
String sm4Decrypt = SM4EncryptUtil.sm4Decrypt(readData);
return sm4Decrypt;
}
}
工具類
public class SM4EncryptUtil {
// 祕鑰
private static final String key = "1234567812345678";
static {
// 添加安全提供者(SM2,SM3,SM4等加密算法,CBC、CFB等加密模式,PKCS7Padding等填充方式,不在Java標準庫中,由BouncyCastleProvider實現)
Security.addProvider(new BouncyCastleProvider());
}
/**
* 輸入:待加密的字符串,16或24或32位字符串密碼
* 輸出:16進制字符串或Base64編碼的字符串密文(常用)
*/
public static String sm4Encrypt(String encrypt) {
String cipherString = null;
try {
// 指定加密算法
String algorithm = "SM4";
// 創建密鑰規範
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
// 獲取Cipher對象實例(BC中SM4默認使用ECB模式和PKCS5Padding填充方式,因此下列模式和填充方式無需指定)
Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
// 初始化Cipher爲加密模式
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
// 獲取加密byte數組
byte[] cipherBytes = cipher.doFinal(encrypt.getBytes(StandardCharsets.UTF_8));
// 輸出爲Base64編碼
cipherString = Base64.getEncoder().encodeToString(cipherBytes);
} catch (Exception e) {
e.printStackTrace();
}
return cipherString;
}
/**
* 輸入:密文,16或24或32位字符串密碼
* 輸出:明文
*/
public static String sm4Decrypt(String cipherString) {
String plainString = null;
try {
// 指定加密算法
String algorithm = "SM4";
// 創建密鑰規範
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
// 獲取Cipher對象實例(BC中SM4默認使用ECB模式和PKCS5Padding填充方式,因此下列模式和填充方式無需指定)
Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
// 初始化Cipher爲解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
// 獲取加密byte數組
byte[] cipherBytes = cipher.doFinal(Base64.getDecoder().decode(cipherString));
// 輸出爲字符串
plainString = new String(cipherBytes);
} catch (Exception e) {
e.printStackTrace();
}
return plainString;
}
}
具體裝飾器 2--Base64 編碼處理
public class Base64SocketStreamDecorator extends SocketStreamDecorator{
public Base64SocketStreamDecorator(SocketStream socketStream) {
super(socketStream);
}
@Override
public void writeData(String data) throws IOException {
String encode = this.encode(data);
System.out.println("--base64編碼後的數據:" + encode);
super.writeData(encode);
}
@Override
public String readData() throws IOException {
String readData = super.readData();
System.out.println("--base64解密前的數據:" + readData);
String decode = this.decode(readData);
return decode;
}
/**
* base64編碼
*/
public String encode(String str) {
if (str == null || str.isEmpty()) {
return "";
}
// String 轉 byte[]
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
// 編碼(base64字符串)
return Base64.getEncoder().encodeToString(bytes);
}
/**
* base64解碼
*/
public String decode(String base64Str) {
if (base64Str == null || base64Str.isEmpty()) {
return "";
}
// 編碼
byte[] base64Bytes = Base64.getDecoder().decode(base64Str);
// byte[] 轉 String(解碼後的字符串)
return new String(base64Bytes, StandardCharsets.UTF_8);
}
}
測試代碼
測試 Socket 服務端代碼
public class SM4SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("Server started...");
while (true) {
try (Socket socket = serverSocket.accept()) {
System.out.println("Client connected...\n");
// 加密數據流
SocketStream socketStreamDecorator = new Base64SocketStreamDecorator(
new SM4CipherSocketStreamDecorator(
new SimpleSocketStream(socket)));
// 讀取客戶端請求並解密
String message = socketStreamDecorator.readData();
System.out.println("服務端讀取數據: " + message);
// 處理請求並加密響應
String response = "我來自服務端!";
System.out.println("服務端發送數據: " + response);
socketStreamDecorator.writeData(response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
測試 Socket 客戶端代碼
public class SM4SocketClient {
public static void main(String[] args) throws IOException {
// 連接到服務器
try (Socket socket = new Socket("localhost", 9999)) {
System.out.println("Connected to server...\n");
// 讀取服務器的響應並解密
SocketStream socketStreamDecorator = new Base64SocketStreamDecorator(
new SM4CipherSocketStreamDecorator(
new SimpleSocketStream(socket)));
// 要發送的消息
String message = "我來自客戶端!";
System.out.println("客戶端發送數據: " + message);
// 使用加密流將消息發送到服務器
socketStreamDecorator.writeData(message);
// 讀取客戶端請求並解密
String readData = socketStreamDecorator.readData();
System.out.println("客戶端讀取數據: " + readData);
socketStreamDecorator.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
測試結果分析
先啓動 Socket 服務,再發起 Socket 請求。
客戶端輸出結果
Connected to server...
客戶端發送數據: 我來自客戶端!
--base64 編碼後的數據:5oiR5p2l6Ieq5a6i5oi356uv77yB
--SM4 加密數據:anTSZv18wcQOxQtA82w9ap2j8DjagsX9QqRVjzFCjWU=
--SM4 解密前數據:5azwyKP7MB6jSyG3eDlJmfOYx5y+woud2WgE2Jjf5lA=
--base64 解密前的數據:5oiR5p2l6Ieq5pyN5Yqh56uv77yB
客戶端讀取數據: 我來自服務端!
服務端輸出結果
Server started...
Client connected...
--SM4 解密前數據:anTSZv18wcQOxQtA82w9ap2j8DjagsX9QqRVjzFCjWU=
--base64 解密前的數據:5oiR5p2l6Ieq5a6i5oi356uv77yB
服務端讀取數據: 我來自客戶端!
服務端發送數據: 我來自服務端!
--base64 編碼後的數據:5oiR5p2l6Ieq5pyN5Yqh56uv77yB
--SM4 加密數據:5azwyKP7MB6jSyG3eDlJmfOYx5y+woud2WgE2Jjf5lA=
這行代碼 new 的先後順序決定代碼執行的先後順序:new Base64SocketStreamDecorator(new SM4CipherSocketStreamDecorator(new SimpleSocketStream(socket)));
,所以文中的案例測試執行順序如下:
數據流方向
-
寫入數據時:外層到內層逐步加工數據。
-
數據先經過
Base64
編碼,再經過SM4
加密,最後寫入SimpleSocketStream
。 -
讀取數據時:內層到外層逐步還原數據。
-
數據從
SimpleSocketStream
讀取後,先解密(SM4),再解碼(Base64)。
代碼層面的調用順序
-
構造階段:
-
SimpleSocketStream(socket)
→SM4CipherSocketStreamDecorator
→Base64SocketStreamDecorator
。 -
方法調用階段(如
write(data)
或read()
): -
調用
Base64SocketStreamDecorator.write(data)
。 -
該方法內部會調用
SM4CipherSocketStreamDecorator.write(data)
。 -
最終調用
SimpleSocketStream.write(data)
執行實際的數據寫入。
調整組合順序測試
如果哪天有需求要改爲先執行 SM4 加密,再執行 Base64 編碼,則代碼只需要修改爲new SM4CipherSocketStreamDecorator(new Base64SocketStreamDecorator(new SimpleSocketStream(socket)));
測試結果變爲:
客戶端輸出結果
客戶端發送數據: 我來自客戶端!
--SM4 加密數據:YoNCZhEm1shIjPWO5vyhsFRWq+XUdHPggFiE325GCKc=
--base64 編碼後的數據:WW9OQ1poRW0xc2hJalBXTzV2eWhzRlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=
--base64 解密前的數據:OUJTQ0ZLUVFIa2dxSGs0WTd2enB1RlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=
--SM4 解密前數據:9BSCFKQQHkgqHk4Y7vzpuFRWq+XUdHPggFiE325GCKc=
客戶端讀取數據: 我來自服務端!
服務端輸出結果變爲
--base64 解密前的數據:WW9OQ1poRW0xc2hJalBXTzV2eWhzRlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=
--SM4 解密前數據:YoNCZhEm1shIjPWO5vyhsFRWq+XUdHPggFiE325GCKc=
服務端讀取數據: 我來自客戶端!
服務端發送數據: 我來自服務端!
--SM4 加密數據:9BSCFKQQHkgqHk4Y7vzpuFRWq+XUdHPggFiE325GCKc=
--base64 編碼後的數據:OUJTQ0ZLUVFIa2dxSGs0WTd2enB1RlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=
裝飾模式的應用
Java 標準庫中的裝飾模式之一:IO 流體系
示例代碼
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(
new FileInputStream(
new File("test.txt"))));
逐步增強功能:IO 流的增強組合有順序要求
-
每一層包裝器都增加了功能:
-
File
:創建一個表示文件的對象test.txt
; -
FileInputStream
:從文件中讀取字節; -
InputStreamReader
:將字節流轉換爲字符流; -
BufferedReader
:提供字符緩衝功能,支持高效讀取和按行讀取。
總結
裝飾模式是一種靈活的結構型模式,允許我們在不修改現有類的情況下,動態地添加功能。它尤其適合於功能可以按需組合疊加、擴展的場景,但需要權衡複雜性與性能開銷。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Rw4iTuor_3wi9AVUp7aUTA