前言
設計模式是軟件設計中常見問題的典型解決方案,你可以通過對其進行定製來解決代碼中的特定設計問題。
設計模式與方法或庫的使用方式不同, 你很難直接在自己的程序中套用某個設計模式。模式並不是一段特定的代碼, 而是解決特定問題的一般性概念。 你可以根據模式來實現符合自己程序實際所需的解決方案。
本文咱們將從設計模式入手,看一看在優秀的Java框架/中間件產品中,設計模式是怎麼被應用的。喫沒喫過豬肉不重要,總得先看看豬豬是咋跑的。
一、單例模式
單例模式是 Java 中最簡單的設計模式之一,它提供了一種創建對象的最佳方式。這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
單例模式雖然很簡單,但它的花樣一點都不少,我們一一來看。
1、餓漢式
餓漢式,顧名思義,就是我很餓,迫不及待。不管有沒有人用,我先創建了再說。
比如在Dubbo中的這段代碼,創建一個配置管理器。
public class ConfigManager {
private static final ConfigManager configManager = new ConfigManager();
private ConfigManager() {}
public static ConfigManager getInstance() {
return configManager;
}
}
又或者在RocketMQ中,創建一個MQ客戶端實例的時候。
public class MQClientManager {
private static MQClientManager instance = new MQClientManager();
private MQClientManager() {}
public static MQClientManager getInstance() {
return instance;
}
}
2、懶漢式
懶漢式是對應餓漢式而言的。它旨在第一次調用才初始化,避免內存浪費。但爲了線程安全和性能,一般都會使用雙重檢查鎖的方式來創建。
我們來看Seata框架中,通過這種方式來創建一個配置類。
public class ConfigurationFactory{
private static volatile Configuration CONFIG_INSTANCE = null;
public static Configuration getInstance() {
if (CONFIG_INSTANCE == null) {
synchronized (Configuration.class) {
if (CONFIG_INSTANCE == null) {
CONFIG_INSTANCE = buildConfiguration();
}
}
}
return CONFIG_INSTANCE;
}
}
3、靜態內部類
可以看到,通過雙重檢查鎖的方式來創建單例對象,還是比較複雜的。又是加鎖,又是判斷兩次,還需要加volatile修飾的。
使用靜態內部類的方式,可以達到雙重檢查鎖相同的功效,但實現上簡單了。
在Seata框架中,創建RM事件處理程序器的時候,就使用了靜態內部類的方式來創建單例對象。
public class DefaultRMHandler extends AbstractRMHandler{
protected DefaultRMHandler() {
initRMHandlers();
}
private static class SingletonHolder {
private static AbstractRMHandler INSTANCE = new DefaultRMHandler();
}
public static AbstractRMHandler get() {
return DefaultRMHandler.SingletonHolder.INSTANCE;
}
}
還有可以通過枚舉的方式來創建單例對象,但這種方式並沒有被廣泛採用,至少筆者在常見的開源框架中沒見過,所以就不再列舉。
有人說,餓漢式的單例模式不好,不能做到延遲加載,浪費內存。但筆者認爲似乎過於吹毛求疵,事實上很多開源框架中,用的最多的就是這種方式。
如果明確希望實現懶加載效果時,可以考慮用靜態內部類的方式;如果還有其他特殊的需求,比如創建對象的過程比較繁瑣,可以用雙重檢查鎖的方式。
二、工廠模式
工廠模式是 Java 中最常用的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
簡單來說,在工廠模式中,就是代替new實例化具體類的一種模式。
1、簡單工廠
簡單工廠,的確比較簡單,它的作用就是把對象的創建放到一個工廠類中,通過參數來創建不同的對象。
在分佈式事務框架Seata中,如果發生異常,則需要進行二階段回滾。
它的過程是,通過事務id找到undoLog記錄,然後解析裏面的數據生成SQL,將一階段執行的SQL給撤銷掉。
問題是SQL的種類包含了比如INSERT、UPDATE、DELETE
,所以它們反解析的過程也不一樣,就需要不同的執行器去解析。
在Seata中,有一個抽象的撤銷執行器,可以生成一條SQL。
public abstract class AbstractUndoExecutor{
//生成撤銷SQL
protected abstract String buildUndoSQL();
}
然後有一個獲取撤銷執行器的工廠,根據SQL的類型,創建不同類型的執行器並返回。
public class UndoExecutorFactory {
public static AbstractUndoExecutor getUndoExecutor(String dbType, SQLUndoLog sqlUndoLog) {
switch (sqlUndoLog.getSqlType()) {
case INSERT:
return new MySQLUndoInsertExecutor(sqlUndoLog);
case UPDATE:
return new MySQLUndoUpdateExecutor(sqlUndoLog);
case DELETE:
return new MySQLUndoDeleteExecutor(sqlUndoLog);
default:
throw new ShouldNeverHappenException();
}
}
}
使用的時候,直接通過工廠類獲取執行器。
AbstractUndoExecutor undoExecutor = UndoExecutorFactory.getUndoExecutor(dataSourceProxy.getDbType(),sqlUndoLog);
undoExecutor.executeOn(conn);
簡單工廠模式的優點,想必各位都能領會,我們不再贅述。但它還有個小小的缺點:
一旦有了新的實現類,就需要修改工廠實現,有可能造成工廠邏輯過於複雜,不利於系統的擴展和維護。
2、工廠方法
工廠方法模式解決了上面那個問題。它可以創建一個工廠接口和多個工廠實現類,這樣如果增加新的功能,只需要添加新的工廠類就可以,不需要修改之前的代碼。
另外,工廠方法模式還可以和模板方法模式結合一起,將他們共同的基礎邏輯抽取到父類中,其它的交給子類去實現。
在Dubbo中,有一個關於緩存的設計完美的體現了工廠方法模式+模板方法模式。
首先,有一個緩存的接口,它提供了設置緩存和獲取緩存兩個方法。
public interface Cache {
void put(Object key, Object value);
Object get(Object key);
}
然後呢,還有一個緩存工廠,它返回一個緩存的實現。
public interface CacheFactory {
Cache getCache(URL url, Invocation invocation);
}
由於結合了模板方法模式,所以Dubbo又搞了個抽象的緩存工廠類,它實現了緩存工廠的接口。
public abstract class AbstractCacheFactory implements CacheFactory {
//具體的緩存實現類
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
@Override
public Cache getCache(URL url, Invocation invocation) {
url = url.addParameter(Constants.METHOD_KEY, invocation.getMethodName());
String key = url.toFullString();
Cache cache = caches.get(key);
if (cache == null) {
//創建緩存實現類,交給子類實現
caches.put(key, createCache(url));
cache = caches.get(key);
}
return cache;
}
//抽象方法,交給子類實現
protected abstract Cache createCache(URL url);
}
在這裏,公共的邏輯就是通過getCahce()創建緩存實現類,那具體創建什麼樣的緩存實現類,就由子類去決定。
所以,每個子類都是一個個具體的緩存工廠類,比如包括:
ExpiringCacheFactory、JCacheFactory、LruCacheFactory、ThreadLocalCacheFactory。
這些工廠類,只有一個方法,就是創建具體的緩存實現類。
public class ThreadLocalCacheFactory extends AbstractCacheFactory {
@Override
protected Cache createCache(URL url) {
return new ThreadLocalCache(url);
}
}
這裏的ThreadLocalCache就是具體的緩存實現類,比如它是通過ThreadLocal來實現緩存功能。
public class ThreadLocalCache implements Cache {
private final ThreadLocal<Map<Object, Object>> store;
public ThreadLocalCache(URL url) {
this.store = new ThreadLocal<Map<Object, Object>>() {
@Override
protected Map<Object, Object> initialValue() {
return new HashMap<Object, Object>();
}
};
}
@Override
public void put(Object key, Object value) {
store.get().put(key, value);
}
@Override
public Object get(Object key) {
return store.get().get(key);
}
}
那在客戶端使用的時候,還是通過工廠來獲取緩存對象。
public static void main(String[] args) {
URL url = URL.valueOf("http://localhost:8080/cache=jacache&.cache.write.expire=1");
Invocation invocation = new RpcInvocation();
CacheFactory cacheFactory = new ThreadLocalCacheFactory();
Cache cache = cacheFactory.getCache(url, invocation);
cache.put("java","java");
System.out.println(cache.get("java"));
}
這樣做的好處有兩點。
第一,如果增加新的緩存實現,只要添加一個新的緩存工廠類就可以,別的都無需改動。
第二,通過模板方法模式,封裝不變部分,擴展可變部分。 提取公共代碼,便於維護。
另外,在Dubbo中,註冊中心的獲取也是通過工廠方法來實現的。
3、抽象工廠
抽象工廠模式,它能創建一系列相關的對象, 而無需指定其具體類。
工廠方法模式和抽象工廠模式,它們之間最大的區別在於:
- 工廠方法模式只有一個抽象產品類,具體工廠類只能創建一個具體產品類的實例;
- 抽象工廠模式有多個抽象產品類,具體工廠類可以創建多個具體產品類的實例。
我們拿上面緩存的例子來繼續往下說。
如果我們現在有一個數據訪問程序,需要同時操作緩存和數據庫,那就需要多個抽象產品和多個具體產品實現。
緩存相關的產品類都已經有了,我們接着來創建數據庫相關的產品實現。
首先,有一個數據庫接口,它是抽象產品類。
public interface DataBase {
void insert(Object tableName, Object record);
Object select(Object tableName);
}
然後,我們創建兩個具體產品類MysqlDataBase和OracleDataBase。
public class MysqlDataBase implements DataBase{
Map<Object,Object> mysqlDb = new HashMap<>();
@Override
public void insert(Object tableName, Object record) {
mysqlDb.put(tableName,record);
}
@Override
public Object select(Object tableName) {
return mysqlDb.get(tableName);
}
}
public class OracleDataBase implements DataBase {
Map<Object,Object> oracleDb = new HashMap<>();
@Override
public void insert(Object tableName, Object record) {
oracleDb.put(tableName,record);
}
@Override
public Object select(Object tableName) {
return oracleDb.get(tableName);
}
}
其次,創建抽象的工廠類,它可以返回一個緩存對象和數據庫對象。
public interface DataAccessFactory {
Cache getCache(URL url);
DataBase getDb();
}
最後是具體的工廠類,可以根據實際的需求,任意組合每一個具體的產品。
比如我們需要一個基於ThreadLocal的緩存實現和基於Mysql的數據庫對象。
public class DataAccessFactory1 implements DataAccessFactory {
@Override
public Cache getCache(URL url) {
return new ThreadLocalCache(url);
}
@Override
public DataBase getDb() {
return new MysqlDataBase();
}
}
如果需要一個基於Lru的緩存實現和基於Oracle的數據庫對象。
public class DataAccessFactory2 implements DataAccessFactory {
@Override
public Cache getCache(URL url) {
return new LruCache(url);
}
@Override
public DataBase getDb() {
return new OracleDataBase();
}
}
可以看到,抽象工廠模式隔離了具體類的生成,使得客戶並不需要知道什麼被創建。由於這種隔離,更換一個具體工廠就變得相對容易,所有的具體工廠都實現了抽象工廠中定義的那些公共接口,因此只需改變具體工廠的實例,就可以在某種程度上改變整個軟件系統的行爲。
三、模板方法模式
在模板模式中,一個抽象類公開定義了執行它的方法的方式/模板。它的子類可以按需要重寫方法實現,但調用將以抽象類中定義的方式進行。
簡單來說,有多個子類共有的方法,且邏輯相同,可以考慮作爲模板方法。
在上面Dubbo緩存的例子中,我們已經看到了模板方法模式的應用。但那個是和工廠方法模式結合在一塊的,我們再單獨找找模板方法模式的應用。
我們知道,當我們的Dubbo應用出現多個服務提供者時,服務消費者需要通過負載均衡算法,選擇其中一個服務來進行調用。
首先,有一個LoadBalance接口,返回一個Invoker。
public interface LoadBalance {
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
然後定義一個抽象類,AbstractLoadBalance,實現LoadBalance接口。
public abstract class AbstractLoadBalance implements LoadBalance {
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (invokers == null || invokers.isEmpty()) {
return null;
}
if (invokers.size() == 1) {
return invokers.get(0);
}
return doSelect(invokers, url, invocation);
}
//抽象方法,由子類選擇一個Invoker
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
}
這裏的公共邏輯就是兩個判斷,判斷invokers集合是否爲空或者是否只有一個實例。如果是的話,就無需調用子類,直接返回就好了。
具體的負載均衡實現有四個:
- 基於權重隨機算法的 RandomLoadBalance
- 基於最少活躍調用數算法的 LeastActiveLoadBalance
- 基於 hash 一致性的 ConsistentHashLoadBalance
- 基於加權輪詢算法的 RoundRobinLoadBalance
public class RandomLoadBalance extends AbstractLoadBalance {
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
//省略邏輯....
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
它們根據不同的算法實現,來返回一個具體的Invoker對象。
四、構造器模式
構造器模式使用多個簡單的對象一步一步構建成一個複雜的對象。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
這種模式,常見於在構建一個複雜的對象,裏面可能包含一些業務邏輯,比如檢查,屬性轉換等。如果都在客戶端手動去設置,那麼會產生大量的冗餘代碼。那麼這時候,我們就可以考慮使用構造器模式。
比如,在Mybatis中,MappedStatement的創建過程就使用了構造器模式。
我們知道,XML文件中的每一個SQL標籤就要生成一個MappedStatement對象,它裏面包含很多個屬性,我們要構造的對象也是它。
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private SqlSource sqlSource;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
//.....省略大部分屬性
}
然後有一個內部類Builder,它負責完成MappedStatement對象的構造。
首先,這個Builder類,通過默認的構造函數,先完成對MappedStatement對象,部分的構造。
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.resultSetType = ResultSetType.DEFAULT;
//.....省略大部分過程
}
}
然後,通過一系列方法,可以設置特定的屬性,並返回這個Builder類,這裏的方法適合處理一些業務邏輯。
public static class Builder {
public Builder parameterMap(ParameterMap parameterMap) {
mappedStatement.parameterMap = parameterMap;
return this;
}
public Builder resultMaps(List<ResultMap> resultMaps) {
mappedStatement.resultMaps = resultMaps;
for (ResultMap resultMap : resultMaps) {
mappedStatement.hasNestedResultMaps = mappedStatement.hasNestedResultMaps || resultMap.hasNestedResultMaps();
}
return this;
}
public Builder statementType(StatementType statementType) {
mappedStatement.statementType = statementType;
return this;
}
public Builder resultSetType(ResultSetType resultSetType) {
mappedStatement.resultSetType = resultSetType == null ? ResultSetType.DEFAULT : resultSetType;
return this;
}
}
最後呢,就是提供一個build方法,返回構建完成的對象就好了。
public MappedStatement build() {
assert mappedStatement.configuration != null;
assert mappedStatement.id != null;
assert mappedStatement.sqlSource != null;
assert mappedStatement.lang != null;
mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
return mappedStatement;
}
在客戶端使用的時候,先創建一個 Builder,然後鏈式的調用一堆方法,最後再調用一次 build() 方法,我們需要的對象就有了,這就是構造器模式的應用。
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
五、適配器模式
適配器模式是作爲兩個不兼容的接口之間的橋樑。這種類型的設計模式屬於結構型模式,它結合了兩個獨立接口的功能。
適配器模式一般用於屏蔽業務邏輯與第三方服務的交互,或者是新老接口之間的差異。
我們知道,在Dubbo中,所有的數據都是通過Netty來負責傳輸的,然後這就涉及了消息編解碼的問題。
所以,首先它有一個編解碼器的接口,負責編碼和解碼。
@SPI
public interface Codec2 {
@Adaptive({Constants.CODEC_KEY})
void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException;
@Adaptive({Constants.CODEC_KEY})
Object decode(Channel channel, ChannelBuffer buffer) throws IOException;
enum DecodeResult {
NEED_MORE_INPUT, SKIP_SOME_INPUT
}
}
然後,有幾個實現類,比如DubboCountCodec、DubboCodec、ExchangeCodec等。
但是,當我們打開這些類的時候,就會發現,他們都是Dubbo中普通的類,只是實現了Codec2接口,其實不能直接作用於Netty編解碼。
這是因爲,Netty編解碼需要實現ChannelHandler接口,這樣纔會被聲明成Netty的處理組件。比如像MessageToByteEncoder、ByteToMessageDecoder那樣。
鑑於此,Dubbo搞了一個適配器,專門來適配編解碼器接口。
final public class NettyCodecAdapter {
private final ChannelHandler encoder = new InternalEncoder();
private final ChannelHandler decoder = new InternalDecoder();
private final Codec2 codec;
private final URL url;
private final org.apache.dubbo.remoting.ChannelHandler handler;
public NettyCodecAdapter(Codec2 codec, URL url, org.apache.dubbo.remoting.ChannelHandler handler) {
this.codec = codec;
this.url = url;
this.handler = handler;
}
public ChannelHandler getEncoder() {
return encoder;
}
public ChannelHandler getDecoder() {
return decoder;
}
private class InternalEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
org.apache.dubbo.remoting.buffer.ChannelBuffer buffer = new NettyBackedChannelBuffer(out);
Channel ch = ctx.channel();
NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler);
codec.encode(channel, buffer, msg);
}
}
private class InternalDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out) throws Exception {
ChannelBuffer message = new NettyBackedChannelBuffer(input);
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
//解碼對象
codec.decode(channel, message);
//省略部分代碼...
}
}
}
上面的代碼中,我們看到,NettyCodecAdapter類適配的是Codec2接口,通過構造函數傳遞實現類,然後定義了內部的編碼器實現和解碼器實現,同時它們都是ChannelHandler。
這樣的話,在內部類裏面的編碼和解碼邏輯,真正調用的還是Codec2接口。
最後我們再來看看,該適配器的調用方式。
//通過SPI方式獲取編解碼器的實現類,比如這裏是DubboCountCodec
Codec2 codec = ExtensionLoader.getExtensionLoader(Codec2.class).getExtension("dubbo");
URL url = new URL("dubbo", "localhost", 22226);
//創建適配器
NettyCodecAdapter adapter = new NettyCodecAdapter(codec, url, NettyClient.this);
//向ChannelPipeline中添加編解碼處理器
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
以上,就是Dubbo中關於編解碼器對於適配器模式的應用。
六、責任鏈模式
責任鏈模式爲請求創建了一個接收者對象的鏈。允許你將請求沿着處理者鏈進行發送。收到請求後,每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。
我們來看一個Netty中的例子。我們知道,在Netty中服務端處理消息,就要添加一個或多個ChannelHandler。那麼,承載這些ChannelHandler的就是ChannelPipeline,它的實現過程就體現了責任鏈模式的應用。
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel channel) {
channel.pipeline()
.addLast(new ChannelHandler1())
.addLast(new ChannelHandler2())
.addLast(new ChannelHandler3());
}
});
需要知道的是,在 Netty 整個框架裏面,一條連接對應着一個 Channel,每一個新創建的 Channel 都將會被分配一個新的 ChannelPipeline。
ChannelPipeline裏面保存的是ChannelHandlerContext對象,它是Channel相關的上下文對象,裏面包着我們定義的處理器ChannelHandler。
根據事件的起源,IO事件將會被 ChannelInboundHandler或者ChannelOutboundHandler處理。隨後,通過調用ChannelHandlerContext 實現,它將被轉發給同一超類型的下一個ChannelHandler。
1、ChannelHandler
首先,我們來看責任處理器接口,Netty中的ChannelHandler,它充當了所有處理入站和出站數據的應用程序邏輯的容器。
public interface ChannelHandler {
//當把 ChannelHandler 添加到 ChannelPipeline 中時被調用
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
//當從 ChannelPipeline 中移除 ChannelHandler 時被調用
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
//當處理過程中在 ChannelPipeline 中有錯誤產生時被調用
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
然後 Netty 定義了下面兩個重要的 ChannelHandler 子接口:
- ChannelInboundHandler,處理入站數據以及各種狀態變化;
public interface ChannelInboundHandler extends ChannelHandler {
//當 Channel 已經註冊到它的 EventLoop 並且能夠處理 I/O 時被調用
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
//當 Channel 從它的 EventLoop 註銷並且無法處理任何 I/O 時被調用
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
//當 Channel 處於活動狀態時被調用;Channel 已經連接/綁定並且已經就緒
void channelActive(ChannelHandlerContext ctx) throws Exception;
//當 Channel 離開活動狀態並且不再連接它的遠程節點時被調用
void channelInactive(ChannelHandlerContext ctx) throws Exception;
當從 Channel 讀取數據時被調用
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
//當 Channel上的一個讀操作完成時被調用
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
- ChannelOutboundHandler,處理出站數據並且允許攔截所有的操作;
public interface ChannelOutboundHandler extends ChannelHandler {
//當請求將 Channel 綁定到本地地址時被調用
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception;
//當請求將 Channel 連接到遠程節點時被調用
void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,SocketAddress localAddress,
ChannelPromise promise) throws Exception;
//當請求將 Channel 從遠程節點斷開時被調用
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
//當請求關閉 Channel 時被調用
void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
//當請求將 Channel 從它的 EventLoop 註銷時被調用
void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
//當請求從 Channel 讀取更多的數據時被調用
void read(ChannelHandlerContext ctx) throws Exception;
//當請求通過 Channel 將數據寫到遠程節點時被調用
void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception;
//當請求通過 Channel 將入隊數據沖刷到遠程節點時被調用
void flush(ChannelHandlerContext ctx) throws Exception;
}
2、ChannelPipeline
既然叫做責任鏈模式,那就需要有一個“鏈”,在Netty中就是ChannelPipeline。
ChannelPipeline 提供了 ChannelHandler 鏈的容器,並定義了用於在該鏈上傳播入站和出站事件流的方法,另外它還具有添加刪除責任處理器接口的功能。
public interface ChannelPipeline{
ChannelPipeline addFirst(String name, ChannelHandler handler);
ChannelPipeline addLast(String name, ChannelHandler handler);
ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler);
ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler);
ChannelPipeline remove(ChannelHandler handler);
ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler);
@Override
ChannelPipeline fireChannelRegistered();
@Override
ChannelPipeline fireChannelActive();
@Override
ChannelPipeline fireExceptionCaught(Throwable cause);
@Override
ChannelPipeline fireUserEventTriggered(Object event);
@Override
ChannelPipeline fireChannelRead(Object msg);
@Override
ChannelPipeline flush();
//省略部分方法.....
}
然後我們看它的實現,默認有兩個節點,頭結點和尾結點。並在構造函數中,使它們首尾相連。這就是標準的鏈式結構。
public class DefaultChannelPipeline implements ChannelPipeline {
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
private final Channel channel;
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
}
當有新的ChannelHandler被添加時,則將其封裝爲ChannelHandlerContext對象,然後插入到鏈表中。
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
3、ChannelHandlerContext
ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之間的關聯,每當有 ChannelHandler 添加到 ChannelPipeline 中時,都會創建 ChannelHandlerContext。
ChannelHandlerContext 的主要功能是管理它所關聯的 ChannelHandler 和在同一個 ChannelPipeline 中的其他 ChannelHandler 之間的交互。
public interface ChannelHandlerContext{
Channel channel();
EventExecutor executor();
ChannelHandler handler();
ChannelPipeline pipeline();
@Override
ChannelHandlerContext fireChannelRegistered();
@Override
ChannelHandlerContext fireChannelUnregistered();
@Override
ChannelHandlerContext fireChannelActive();
@Override
ChannelHandlerContext fireChannelRead(Object msg);
@Override
ChannelHandlerContext read();
@Override
ChannelHandlerContext flush();
//省略部分方法...
}
ChannelHandlerContext負責在鏈上傳播責任處理器接口的事件。
它有兩個重要的方法,查找Inbound類型和Outbound類型的處理器。
值得注意的是,如果一個入站事件被觸發,它將被從ChannelPipeline的頭部開始一直被傳播到ChannelPipeline的尾端;一個出站事件將從ChannelPipeline的最右邊開始,然後向左傳播。
abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {
volatile AbstractChannelHandlerContext next;
volatile AbstractChannelHandlerContext prev;
//查找下一個Inbound類型的處理器,左 > 右
private AbstractChannelHandlerContext findContextInbound(int mask) {
AbstractChannelHandlerContext ctx = this;
EventExecutor currentExecutor = executor();
do {
ctx = ctx.next;
} while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
return ctx;
}
//查找下一個Outbound類型的處理器,右 > 左
private AbstractChannelHandlerContext findContextOutbound(int mask) {
AbstractChannelHandlerContext ctx = this;
EventExecutor currentExecutor = executor();
do {
ctx = ctx.prev;
} while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
return ctx;
}
}
4、處理流程
當我們向服務端發送消息的時候,將會觸發read方法。
public abstract class AbstractNioByteChannel extends AbstractNioChannel {
public final void read() {
//從Channel中獲取對應的ChannelPipeline
final ChannelPipeline pipeline = pipeline();
//數據載體
ByteBuf byteBuf = allocHandle.allocate(allocator);
//傳遞數據
pipeline.fireChannelRead(byteBuf);
}
}
上面的代碼中,就會調用到ChannelPipeline,它會從Head節點開始,根據上下文對象依次調用處理器。
public class DefaultChannelPipeline implements ChannelPipeline {
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
}
因爲第一個節點是默認的頭結點HeadContext,所以它是從ChannelHandlerContext開始的。
abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {
//找到下一個ChannelHandler並執行
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
}
然後在我們自定義的ChannelHandler中,就會被調用到。
public class ChannelHandler1 extends ChannelInboundHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg){
System.out.println("ChannelHandler1:"+msg);
ctx.fireChannelRead(msg);
}
}
如果消息有多個ChannelHandler,你可以自由選擇是否繼續往下傳遞請求。
比如,如果你認爲消息已經被處理且不應該繼續往下調用,把上面的ctx.fireChannelRead(msg);註釋掉就終止了整個責任鏈。
七、策略模式
該模式定義了一系列算法,並將每個算法封裝起來,使它們可以相互替換,且算法的變化不會影響使用算法的客戶。
策略模式是一個很常見,而且也很好用的設計模式,如果你的業務代碼中有大量的if...else,那麼就可以考慮是否可以使用策略模式改造一下。
RocketMQ我們大家都熟悉,是一款優秀的分佈式消息中間件。消息中間件,簡單來說,就是客戶端發送一條消息,服務端存儲起來並提供給消費者去消費。
請求消息的類型多種多樣,處理過程肯定也不一樣,每次都判斷一下再處理就落了下乘。在RocketMQ裏,它會把所有處理器註冊起來,然後根據請求消息的 code ,讓對應的處理器處理請求,這就是策略模式的應用。
首先,它們需要實現同一個接口,在這裏就是請求處理器接口。
public interface NettyRequestProcessor {
RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)throws Exception;
boolean rejectRequest();
}
這個接口只做一件事,就是處理來自客戶端的請求。不同類型的請求封裝成不同的RemotingCommand對象。
RocketMQ大概有90多種請求類型,都在RequestCode裏以 code 來區分。
然後,定義一系列策略類。我們來看幾個。
//默認的消息處理器
public class DefaultRequestProcessor implements NettyRequestProcessor {}
//發送消息的處理器
public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor {}
//拉取消息的處理器
public class PullMessageProcessor implements NettyRequestProcessor {}
//查詢消息的處理器
public class QueryMessageProcessor implements NettyRequestProcessor {}
//消費者端管理的處理器
public class ConsumerManageProcessor implements NettyRequestProcessor {}
接着,將這些策略類封裝起來。在RocketMQ中,在啓動Broker服務器的時候,註冊這些處理器。
public class BrokerController {
public void registerProcessor() {
SendMessageProcessor sendProcessor = new SendMessageProcessor(this);
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE,this.pullMessageProcessor,this.pullMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE, replyMessageProcessor, replyMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE_V2, replyMessageProcessor, replyMessageExecutor);
NettyRequestProcessor queryProcessor = new QueryMessageProcessor(this);
this.remotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor);
ClientManageProcessor clientProcessor = new ClientManageProcessor(this);
this.remotingServer.registerProcessor(RequestCode.HEART_BEAT, clientProcessor, this.heartbeatExecutor);
this.remotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientProcessor, this.clientManageExecutor);
//省略部分註冊過程.....
}
}
最後,在Netty接收到客戶端的請求之後,就會根據消息的類型,找到對應的策略類,去處理消息。
public abstract class NettyRemotingAbstract {
public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
//根據請求類型找到對應的策略類
final Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
//如果沒有找到就使用默認的
final Pair<NettyRequestProcessor, ExecutorService> pair =
null == matched ? this.defaultRequestProcessor : matched;
//執行策略
final RemotingCommand response = pair.getObject1().processRequest(ctx, cmd);
//省略大部分代碼......
}
}
如果有了新的請求消息類型,RocketMQ也無需修改業務代碼,新增策略類並將其註冊進來就好了。
八、代理模式
代理模式,爲其他對象提供一種代理以控制對這個對象的訪問。
在一些開源框架或中間件產品中,代理模式會非常常見。我們使用的時候越簡便,框架在背後幫我們做的事就可能越複雜。這裏面往往都體現着代理模式的應用,頗有移花接木的味道。
1、Dubbo
Dubbo作爲一個RPC框架,其中有一個很重要的功能就是:
提供高性能的基於代理的遠程調用能力,服務以接口爲粒度,爲開發者屏蔽遠程調用底層細節。
這裏我們關注兩個重點:
- 面向接口代理;
- 屏蔽調用底層細節。
比如我們有一個庫存服務,它提供一個扣減庫存的接口。
public interface StorageDubboService {
int decreaseStorage(StorageDTO storage);
}
在別的服務裏,需要扣減庫存的時候,就會通過Dubbo引用這個接口,也比較簡單。
@Reference
StorageDubboService storageDubboService;
我們使用起來很簡單,可 StorageDubboService 只是一個普通的服務類,並不具備遠程調用的能力。
Dubbo就是給這些服務類,創建了代理類。通過ReferenceBean來創建並返回一個代理對象。
public class ReferenceBean<T>{
@Override
public Object getObject() {
return get();
}
public synchronized T get() {
if (ref == null) {
init();
}
return ref;
}
}
在我們使用的時候,實則調用的是代理對象,代理對象完成複雜的遠程調用。比如連接註冊中心、負載均衡、集羣容錯、連接服務器發送消息等功能。
2、Mybatis
還有一個典型的應用,就是我們經常在用的Mybatis。我們在使用的時候,一般只操作Mapper接口,然後Mybatis會找到對應的SQL語句來執行。
public interface UserMapper {
List<User> getUserList();
}
如上代碼,UserMapper也只是一個普通的接口,它是怎樣最終執行到我們的SQL語句的呢?
答案也是代理。當Mybatis掃描到我們定義的Mapper接口時,會將其設置爲MapperFactoryBean,並創建返回一個代理對象。
protected T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
代理對象去通過請求的方法名找到MappedStatement對象,調用執行器,解析SqlSource對象來生成SQL,執行並解析返回結果等。
以上案例具體的實現過程,在這裏就不再深入細聊。有興趣可能翻閱筆者其他文章~
九、裝飾器模式
裝飾器模式,在不改變現有對象結構的情況下,動態地給該對象增加一些職責(即增加其額外功能)的模式,它屬於對象結構型模式。
Mybatis裏的緩存設計,就是裝飾器模式的典型應用。
首先,我們知道,MyBatis 執行器是 MyBatis 調度的核心,它負責SQL語句的生成和執行。
在創建SqlSession的時候,會創建這個執行器,默認的執行器是SimpleExecutor。
但是爲了給執行器增加緩存的職責,就變成了在SimpleExecutor上一層添加了CachingExecutor。
在CachingExecutor中的實際操作還是委託給SimpleExecutor去執行,只是在執行前後增加了緩存的操作。
首先,我們來看看它的裝飾過程。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//默認的執行器
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//使用緩存執行器來裝飾
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
當SqlSession執行方法的時候,則會先調用到CachingExecutor,我們來看查詢方法。
public class CachingExecutor implements Executor {
@Override
public <E> List<E> query()throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
}
這裏的代碼,如果開啓了緩存,則先從緩存中獲取結果。如果沒有開啓緩存或者緩存中沒有結果,則再調用SimpleExecutor執行器去數據庫中查詢。
十、觀察者模式
觀察者模式,定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。
在Spring或者SpringBoot項目中,有時候我們需要在Spring容器啓動並加載完之後,做一些系統初始化的事情。這時候,我們可以配置一個觀察者ApplicationListener,來達到這一目的。這就是觀察者模式的實踐。
@Component
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("幹一些系統初始化的事情....");
ApplicationContext context = event.getApplicationContext();
String[] names = context.getBeanDefinitionNames();
for (String beanName:names){
System.out.println("----------"+beanName+"---------");
}
}
}
首先,我們知道,ApplicationContext是 Spring 中的核心容器。
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
//觀察者容器
private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
//被觀察者
private ApplicationEventMulticaster applicationEventMulticaster;
}
在ApplicationContext容器刷新的時候,會初始化一個被觀察者,並註冊到Spring容器中。
然後,註冊各種觀察者到被觀察者中,形成一對多的依賴。
public abstract class AbstractApplicationContext{
protected void registerListeners() {
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
}
這時候,我們自定義的觀察者對象也被註冊到了applicationEventMulticaster裏面。
最後,當ApplicationContext完成刷新後,則發佈ContextRefreshedEvent事件。
protected void finishRefresh() {
publishEvent(new ContextRefreshedEvent(this));
}
通知觀察者,調用ApplicationListener.onApplicationEvent()。
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
listener.onApplicationEvent(event);
}
接下來我們再看看在Dubbo是如何應用這一機制的。
Dubbo服務導出過程始於 Spring 容器發佈刷新事件,Dubbo 在接收到事件後,會立即執行服務導出邏輯。
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
ApplicationEventPublisherAware {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
}
我們看到,Dubbo中的ServiceBean也實現了 ApplicationListener 接口,在 Spring 容器發佈刷新事件之後就會執行導出方法。我們重點關注,在Dubbo執行完導出之後,它也發佈了一個事件。
public class ServiceBean<T>{
public void export() {
super.export();
publishExportEvent();
}
private void publishExportEvent() {
ServiceBeanExportedEvent exportEvent = new ServiceBeanExportedEvent(this);
applicationEventPublisher.publishEvent(exportEvent);
}
}
ServiceBeanExportedEvent,服務導出事件,需要繼承Spring中的事件對象ApplicationEvent。
public class ServiceBeanExportedEvent extends ApplicationEvent {
public ServiceBeanExportedEvent(ServiceBean serviceBean) {
super(serviceBean);
}
public ServiceBean getServiceBean() {
return (ServiceBean) super.getSource();
}
}
然後我們自定義一個ApplicationListener,也就是觀察者,就可以監聽到Dubbo服務接口導出事件了。
@Component
public class ServiceBeanListener implements ApplicationListener<ServiceBeanExportedEvent> {
@Override
public void onApplicationEvent(ServiceBeanExportedEvent event) {
ServiceBean serviceBean = event.getServiceBean();
String beanName = serviceBean.getBeanName();
Service service = serviceBean.getService();
System.out.println(beanName+":"+service);
}
}