👉 歡迎 ,你將獲得: 專屬的計畫實戰 / Java 學習路線 / 一對一提問 / 學習打卡 / 每月贈書
新計畫: 仿小紅書 (微服務架構)正在更新中... , 全棧前後端分離部落格計畫 2.0 版本完結啦, 演示連結 : http://116.62.199.48/ 。全程手摸手,後端 + 前端全棧開發,從 0 到 1 講解每個功能點開發步驟,1v1 答疑,直到計畫上線。 目前已更新了261小節,累計41w+字,講解圖:1806張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將Java領域典型的計畫都整一波,如秒殺系統, 線上商城, IM即時通訊,Spring Cloud Alibaba 等等,
一、開篇
二、簡介
三、套用場景
案例一:建立商品多級校驗場景
案例二:工作流,費用報銷稽核流程
四、責任鏈的優缺點
五、源碼檢視
一、開篇
閱讀本文可以了解哪些知識?
結合具體案例,領略責任鏈模式的魅力。
責任鏈模式實作流程編排、動態擴充套件。
使用 Sping @Resource 註解註入的騷操作。
使用遞迴演算法設定責任鏈路。
二、簡介
責任鏈模式,簡而言之,就是將多個操作組裝成一條鏈路進行處理。請求在鏈路上傳遞,鏈路上的每一個節點就是一個處理器,每個處理器都可以對請求進行處理,或者傳遞給鏈路上的下一個處理器處理。
三、套用場景
責任鏈模式的套用場景,在實際工作中,通常有如下兩種套用場景。
操作需要經過一系列的校驗,透過校驗後才執行某些操作。
工作流。企業中通常會制定很多工作流程,一級一級的去處理任務。
下面透過兩個案例來學習一下責任鏈模式。
案例一:建立商品多級校驗場景
以建立商品為例,假設商品建立邏輯分為以下三步完成:①建立商品、②校驗商品參數、③保存商品。
第②步校驗商品又分為多種情況的校驗,必填欄位校驗、規格校驗、價格校驗、庫存校驗等等。這些檢驗邏輯像一個流水線,要想建立出一個商品,必須透過這些校驗。如下流程圖所示:
虛擬碼如下:
建立商品步驟,需要經過一系列的參數校驗,如果參數校驗失敗,直接返回失敗的結果;透過所有的參數校驗後,最終保存商品資訊。
如上程式碼看起來似乎沒什麽問題,它非常工整,而且程式碼邏輯很清晰。
PS:我沒有把所有的校驗程式碼都羅列在一個方法裏,那樣更能產生對比性,但我覺得抽象並分離單一職責的函式應該是每個程式設計師最基本的規範!
但是隨著業務需求不斷地疊加,相關的校驗邏輯也越來越多,新的功能使程式碼越來越臃腫,可維護性較差。更糟糕的是,這些校驗元件不可復用,當你有其他需求也需要用到一些校驗時,你又變成了Ctrl+C , Ctrl+V程式設計師,系統的維護成本也越來越高。如下圖所示:
虛擬碼同上,這裏就不贅述了。
終於有一天,你忍無可忍了,決定重構這段程式碼。
使用責任鏈模式最佳化 :建立商品的每個校驗步驟都可以作為一個單獨的處理器,抽離為一個單獨的類,便於復用。這些處理器形成一條鏈式呼叫,請求在處理器鏈上傳遞,如果校驗條件不透過,則處理器不再向下傳遞請求,直接返回錯誤資訊;若所有的處理器都透過檢驗,則執行保存商品步驟。
案例一實戰:責任鏈模式實作建立商品校驗
UML圖:一覽眾山小
AbstractCheckHandler 表示處理器抽象類,負責抽象處理器行為。其有3個子類別,分別是:
NullValueCheckHandler:空值校驗處理器
PriceCheckHandler:價格校驗處理
StockCheckHandler:庫存校驗處理器
AbstractCheckHandler 抽象類中,
handle()
定義了處理器的抽象方法,其子類別需要重寫
handle()
方法以實作特殊的處理器校驗邏輯;
protected ProductCheckHandlerConfig config 是處理器的動態配置類,使用protected聲明,每個子類別處理器都持有該物件。該物件用於聲明當前處理器、以及當前處理器的下一個處理器nextHandler,另外也可以配置一些特殊內容,比如說介面降級配置、超時時間配置等。
AbstractCheckHandler nextHandler 是當前處理器持有的下一個處理器的參照,當前處理器執行完畢時,便呼叫nextHandler執行下一處理器的handle()校驗方法;
protected Result next()
是抽象類中定義的,執行下一個處理器的方法,使用protected聲明,每個子類別處理器都持有該物件。當子類別處理器執行完畢(透過)時,呼叫父類的方法執行下一個處理器nextHandler。
HandlerClient 是執行處理器鏈路的客戶端,
HandlerClient.executeChain()
方法負責發起整個鏈路呼叫,並接收處理器鏈路的返回值。
擼起袖子開始擼程式碼吧 🤓 ~
商品參數物件:保存商品的入參
ProductVO是建立商品的參數物件,包含商品的基礎資訊。並且其作為責任鏈模式中多個處理器的入參,多個處理器都以ProductVO為入參進行特定的邏輯處理。實際業務中,商品物件特別復雜。咱們化繁為簡,簡化商品參數如下:
/**
* 商品物件
*/
@Data
@Builder
public class ProductVO {
/**
* 商品SKU,唯一
*/
private Long skuId;
/**
* 商品名稱
*/
private String skuName;
/**
* 商品圖片路徑
*/
private String imgPath;
/**
* 價格
*/
private BigDecimal price;
/**
* 庫存
*/
private Integer stock;
}
抽象類處理器:抽象行為,子類別共有內容、方法
AbstractCheckHandler :處理器抽象類,並使用@Component註解註冊為由Spring管理的Bean物件,這樣做的好處是,我們可以輕松的使用Spring來管理這些處理器Bean。
/**
* 抽象類處理器
*/
@Component
public abstract class AbstractCheckHandler {
/**
* 當前處理器持有下一個處理器的參照
*/
@Getter
@Setter
protected AbstractCheckHandler nextHandler;
/**
* 處理器配置
*/
@Setter
@Getter
protected ProductCheckHandlerConfig config;
/**
* 處理器執行方法
* @param param
* @return
*/
public abstract Result handle(ProductVO param);
/**
* 鏈路傳遞
* @param param
* @return
*/
protected Result next(ProductVO param) {
//下一個鏈路沒有處理器了,直接返回
if (Objects.isNull(nextHandler)) {
return Result.success();
}
//執行下一個處理器
return nextHandler.handle(param);
}
}
在AbstractCheckHandler抽象類處理器中,使用protected聲明子類別可見的內容和方法。使用 @Component註解,聲明其為Spring的Bean物件,這樣做的好處是可以利用Spring輕松管理所有的子類別,下面會看到如何使用。抽象類的內容和方法說明如下:
public abstract Result handle():表示抽象的校驗方法,每個處理器都應該繼承AbstractCheckHandler抽象類處理器,並重寫其handle方法,各個處理器從而實作特殊的校驗邏輯,實際上就是多型的思想。
protected ProductCheckHandlerConfig config:表示每個處理器的動態配置類,可以透過「配置中心」動態修改該配置,實作處理器的「動態編排」和「順序控制」。配置類中可以配置處理器的名稱、下一個處理器、以及處理器是否降級等內容。
protected AbstractCheckHandler nextHandler:表示當前處理器持有下一個處理器的參照,如果當前處理器handle()校驗方法執行完畢,則執行下一個處理器nextHandler的handle()校驗方法執行校驗邏輯。
protected Result next(ProductVO param):此方法用於處理器鏈路傳遞,子類別處理器執行完畢後,呼叫父類的next()方法執行在config 配置的鏈路上的下一個處理器,如果所有處理器都執行完畢了,就返回結果了。
ProductCheckHandlerConfig配置類 :
/**
* 處理器配置類
*/
@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
/**
* 處理器Bean名稱
*/
private String handler;
/**
* 下一個處理器
*/
private ProductCheckHandlerConfig next;
/**
* 是否降級
*/
private Boolean down = Boolean.FALSE;
}
子類別處理器:處理特有的校驗邏輯
AbstractCheckHandler抽象類處理器有3個子類別分別是:
NullValueCheckHandler:空值校驗處理器
PriceCheckHandler:價格校驗處理
StockCheckHandler:庫存校驗處理器
各個處理器繼承AbstractCheckHandler抽象類處理器,並重寫其handle()處理方法以實作特有的校驗邏輯。
NullValueCheckHandler :空值校驗處理器。針對性校驗建立商品中必填的參數。如果校驗未透過,則返回錯誤碼ErrorCode,責任鏈在此截斷(停止),建立商品返回被校驗住的錯誤資訊。註意程式碼中的降級配置!
super.getConfig().getDown()
是獲取AbstractCheckHandler處理器物件中保存的配置資訊,如果處理器配置了降級,則跳過該處理器,呼叫
super.next()
執行下一個處理器邏輯。
同樣,使用@Component註冊為由Spring管理的Bean物件,
/**
* 空值校驗處理器
*/
@Component
public class NullValueCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("空值校驗 Handler 開始...");
//降級:如果配置了降級,則跳過此處理器,執行下一個處理器
if (super.getConfig().getDown()) {
System.out.println("空值校驗 Handler 已降級,跳過空值校驗 Handler...");
return super.next(param);
}
//參數必填校驗
if (Objects.isNull(param)) {
return Result.failure(ErrorCode.PARAM_NULL_ERROR);
}
//SkuId商品主鍵參數必填校驗
if (Objects.isNull(param.getSkuId())) {
return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
}
//Price價格參數必填校驗
if (Objects.isNull(param.getPrice())) {
return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
}
//Stock庫存參數必填校驗
if (Objects.isNull(param.getStock())) {
return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
}
System.out.println("空值校驗 Handler 透過...");
//執行下一個處理器
return super.next(param);
}
}
PriceCheckHandler :價格校驗處理。針對建立商品的價格參數進行校驗。這裏只是做了簡單的判斷價格>0的校驗,實際業務中比較復雜,比如「價格門」這些防範措施等。
/**
* 價格校驗處理器
*/
@Component
public class PriceCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("價格校驗 Handler 開始...");
//非法價格校驗
boolean illegalPrice = param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
if (illegalPrice) {
return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
}
//其他校驗邏輯...
System.out.println("價格校驗 Handler 透過...");
//執行下一個處理器
return super.next(param);
}
}
StockCheckHandler :庫存校驗處理器。針對建立商品的庫存參數進行校驗。
/**
* 庫存校驗處理器
*/
@Component
public class StockCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("庫存校驗 Handler 開始...");
//非法庫存校驗
boolean illegalStock = param.getStock() < 0;
if (illegalStock) {
return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
}
//其他校驗邏輯..
System.out.println("庫存校驗 Handler 透過...");
//執行下一個處理器
return super.next(param);
}
}
客戶端:執行處理器鏈路
HandlerClient客戶端類負責發起整個處理器鏈路的執行,透過
executeChain()
方法。如果處理器鏈路返回錯誤資訊,即校驗未透過,則整個鏈路截斷(停止),返回相應的錯誤資訊。
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
//執行處理器
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient 責任鏈執行失敗返回:" + handlerResult.toString());
return handlerResult;
}
return Result.success();
}
}
以上,責任鏈模式相關的類已經建立好了。接下來就可以建立商品了。
建立商品:抽象步驟,化繁為簡
createProduct()
建立商品方法抽象為2個步驟:①參數校驗、②建立商品。參數校驗使用責任鏈模式進行校驗,包含:空值校驗、價格校驗、庫存校驗等等,只有鏈上的所有處理器均校驗透過,才呼叫
saveProduct()
建立商品方法;否則返回校驗錯誤資訊。
在
createProduct()
建立商品方法中,透過責任鏈模式,我們將校驗邏輯進行解耦。
createProduct()
建立商品方法中不需要關註都要經過哪些校驗處理器,以及校驗處理器的細節。
/**
* 建立商品
* @return
*/
@Test
public Result createProduct(ProductVO param) {
//參數校驗,使用責任鏈模式
Result paramCheckResult = this.paramCheck(param);
if (!paramCheckResult.isSuccess()) {
return paramCheckResult;
}
//建立商品
return this.saveProduct(param);
}
參數校驗:責任鏈模式
參數校驗
paramCheck()
方法使用責任鏈模式進行參數校驗,方法內沒有聲明具體都有哪些校驗,具體有哪些參數校驗邏輯是透過多個處理器鏈傳遞的。如下:
/**
* 參數校驗:責任鏈模式
* @param param
* @return
*/
private Result paramCheck(ProductVO param) {
//獲取處理器配置:通常配置使用統一配置中心儲存,支持動態變更
ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile();
//獲取處理器
AbstractCheckHandler handler = this.getHandler(handlerConfig);
//責任鏈:執行處理器鏈路
Result executeChainResult = HandlerClient.executeChain(handler, param);
if (!executeChainResult.isSuccess()) {
System.out.println("建立商品 失敗...");
return executeChainResult;
}
//處理器鏈路全部成功
return Result.success();
}
paramCheck()
方法步驟說明如下:
👉 步驟1:獲取處理器配置。
透過
getHandlerConfigFile()
方法獲取處理器配置類物件,配置類保存了鏈上各個處理器的上下級節點配置,支持流程編排、動態擴充套件。通常配置是透過Ducc(京東自研的配置中心)、Nacos(阿裏開源的配置中心)等配置中心儲存的,支持動態變更、即時生效。
基於此,我們便可以實作校驗處理器的編排、以及動態擴充套件了。我這裏沒有使用配置中心儲存處理器鏈路的配置,而是使用JSON串的形式去模擬配置,大家感興趣的可以自行實作。
/**
* 獲取處理器配置:通常配置使用統一配置中心儲存,支持動態變更
* @return
*/
private ProductCheckHandlerConfig getHandlerConfigFile() {
//配置中心儲存的配置
String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
//轉成Config物件
ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig. class);
return handlerConfig;
}
ConfigJson儲存的處理器鏈路配置JSON串,在程式碼中可能不便於觀看,我們可以使用json.cn等格式化看一下,如下,配置的整個呼叫鏈路規則特別清晰。
getHandlerConfigFile()
類獲到配置類的結構如下,可以看到,就是把在配置中心儲存的配置規則,轉換成配置類
ProductCheckHandlerConfig
物件,用於程式處理。
註意,此時配置類中儲存的僅僅是處理器Spring Bean的name而已,並非實際處理器物件。
接下來,透過配置類獲取實際要執行的處理器。
👉 步驟2:根據配置獲取處理器。
上面步驟1透過
getHandlerConfigFile()
方法獲取到處理器鏈路配置規則後,再呼叫
getHandler()
獲取處理器。
getHandler()
參數是如上ConfigJson配置的規則,即步驟1轉換成的
ProductCheckHandlerConfig
物件;根據
ProductCheckHandlerConfig
配置規則轉換成處理器鏈路物件。程式碼如下:
/**
* 使用Spring註入:所有繼承了AbstractCheckHandler抽象類的Spring Bean都會註入進來。Map的Key對應Bean的name,Value是name對應相應的Bean
*/
@Resource
private Map<String, AbstractCheckHandler> handlerMap;
/**
* 獲取處理器
* @param config
* @return
*/
private AbstractCheckHandler getHandler (ProductCheckHandlerConfig config) {
//配置檢查:沒有配置處理器鏈路,則不執行校驗邏輯
if (Objects.isNull(config)) {
return null;
}
//配置錯誤
String handler = config.getHandler();
if (StringUtils.isBlank(handler)) {
return null;
}
//配置了不存在的處理器
AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler());
if (Objects.isNull(abstractCheckHandler)) {
return null;
}
//處理器設定配置Config
abstractCheckHandler.setConfig(config);
//遞迴設定鏈路處理器
abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));
return abstractCheckHandler;
}
👉 👉 步驟2-1:配置檢查。
程式碼14~27行,進行了配置的一些檢查操作。如果配置錯誤,則獲取不到對應的處理器。程式碼23行
handlerMap.get(config.getHandler())
是從所有處理器對映Map中獲取到對應的處理器Spring Bean。
註意第5行程式碼,handlerMap儲存了所有的處理器對映,是透過Spring @Resource註解註入進來的。註入的規則是:所有繼承了AbstractCheckHandler抽象類(它是Spring管理的Bean)的子類別(子類別也是Spring管理的Bean)都會註入進來。
註入進來的handlerMap中 Map的Key對應Bean的name,Value是name對應的Bean例項,也就是實際的處理器,這裏指空值校驗處理器、價格校驗處理器、庫存校驗處理器。如下:
這樣根據配置ConfigJson(👉 步驟1:獲取處理器配置)中
handler:"priceCheckHandler"
的配置,使用
handlerMap.get(config.getHandler())
便可以獲取到對應的處理器Spring Bean物件了。
👉 👉 步驟2-2:保存處理器規則。
程式碼29行,將配置規則保存到對應的處理器中
abstractCheckHandler.setConfig(config)
,子類別處理器就持有了配置的規則。
👉 👉 步驟2-3:遞迴設定處理器鏈路。
程式碼32行,遞迴設定鏈路上的處理器。
//遞迴設定鏈路處理器 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));
這一步可能不太好理解,結合ConfigJson配置的規則來看,似乎就很很容易理解了。
由上而下,
NullValueCheckHandler
空值校驗處理器透過
setNextHandler()
方法設定自己持有的下一節點的處理器,也就是價格處理器PriceCheckHandler。
接著,PriceCheckHandler價格處理器,同樣需要經過步驟2-1配置檢查、步驟2-2保存配置規則,並且最重要的是,它也需要設定下一節點的處理器StockCheckHandler庫存校驗處理器。
StockCheckHandler庫存校驗處理器也一樣,同樣需要經過步驟2-1配置檢查、步驟2-2保存配置規則,但請註意StockCheckHandler的配置,它的next規則配置了null,這表示它下面沒有任何處理器要執行了,它就是整個鏈路上的最後一個處理節點。
透過遞迴呼叫
getHandler()
獲取處理器方法,就
將整個處理器鏈路物件串聯起來
了。如下:
友情提示:遞迴雖香,但使用遞迴一定要註意截斷遞迴的條件處理,否則可能造成死迴圈哦!
實際上,
getHandler()
獲取處理器物件的程式碼就是把在配置中心配置的規則ConfigJson,轉換成配置類
ProductCheckHandlerConfig
物件,再根據配置類物件,轉換成實際的處理器物件,這個處理器物件持有整個鏈路的呼叫順序。
👉 步驟3:客戶端執行呼叫鏈路。
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
//執行處理器
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient 責任鏈執行失敗返回:" + handlerResult.toString());
return handlerResult;
}
return Result.success();
}
}
getHandler()獲取完處理器後,整個呼叫鏈路的執行順序也就確定了,此時,客戶端該幹活了!
HandlerClient.executeChain(handler, param)
方法是HandlerClient客戶端類執行處理器整個呼叫鏈路的,並接收處理器鏈路的返回值。
executeChain()
透過
AbstractCheckHandler.handle()
觸發整個鏈路處理器順序執行,如果某個處理器校驗沒有透過
!handlerResult.isSuccess()
,則返回錯誤資訊;所有處理器都校驗透過,則返回正確資訊
Result.success()
。
總結:串聯方法呼叫流程
基於以上,再透過流程圖來回顧一下整個呼叫流程。
測試:程式碼執行結果
場景1:建立商品參數中有空值(如下skuId參數為null),鏈路被空值處理器截斷,返回錯誤資訊
//建立商品參數
ProductVO param = ProductVO.builder()
.skuId(null).skuName("華為手機").imgPath("http://...")
.price(new BigDecimal(1))
.stock(1)
.build();
測試結果
場景2:建立商品價格參數異常(如下price參數),被價格處理器截斷,返回錯誤資訊
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("華為手機").imgPath("http://...")
.price(new BigDecimal(-999))
.stock(1)
.build();
測試結果
場景 3:建立商品庫存參數異常(如下stock參數),被庫存處理器截斷,返回錯誤資訊。
//建立商品參數,模擬使用者傳入
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("華為手機").imgPath("http://...")
.price(new BigDecimal(1))
.stock(-999)
.build();
測試結果
場景4:建立商品所有處理器校驗透過,保存商品。
//建立商品參數,模擬使用者傳入
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("華為手機").imgPath("http://...")
.price(new BigDecimal(999))
.stock(1).build();
測試結果
案例二:工作流,費用報銷稽核流程
同事小賈最近剛出差回來,她迫不及待的就送出了費用報銷的流程。根據金額不同,分為以下幾種稽核流程。報銷金額低於1000元,三級部門管理者審批即可,1000到5000元除了三級部門管理者審批,還需要二級部門管理者審批,而5000到10000元還需要一級部門管理者審批。即有以下幾種情況:
小賈需報銷500元,三級部門管理者審批即可。
小賈需報銷2500元,三級部門管理者審批透過後,還需要二級部門管理者審批,二級部門管理者審批透過後,才完成報銷審批流程。
小賈需報銷7500元,三級管理者審批透過後,並且二級管理者審批透過後,流程流轉到一級部門管理者進行審批,一級管理者審批透過後,即完成了報銷流程。
UML圖
AbstractFlowHandler作為處理器抽象類,抽象了
approve()
稽核方法,一級、二級、三級部門管理者處理器繼承了抽象類,並重寫其
approve()
稽核方法,從而實作特有的稽核邏輯。
配置類如下所示,每層的處理器都要配置稽核人、價格稽核規則(稽核的最大、最小金額)、下一級處理人。配置規則是可以動態變更的,如果三級部門管理者可以稽核的金額增加到2000元,修改一下配置即可動態生效。
程式碼實作與案例一相似,感興趣的自己動動小手吧~
四、責任鏈的優缺點
五、源碼檢視
https://github.com/rongtao7/MyNotes
👉 歡迎 ,你將獲得: 專屬的計畫實戰 / Java 學習路線 / 一對一提問 / 學習打卡 / 每月贈書
新計畫: 仿小紅書 (微服務架構)正在更新中... , 全棧前後端分離部落格計畫 2.0 版本完結啦, 演示連結 : http://116.62.199.48/ 。全程手摸手,後端 + 前端全棧開發,從 0 到 1 講解每個功能點開發步驟,1v1 答疑,直到計畫上線。 目前已更新了261小節,累計41w+字,講解圖:1806張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將Java領域典型的計畫都整一波,如秒殺系統, 線上商城, IM即時通訊,Spring Cloud Alibaba 等等,
1.
2.
3.
4.
最近面試BAT,整理一份面試資料【Java面試BATJ通關手冊】,覆蓋了Java核心技術、JVM、Java並行、SSM、微服務、資料庫、數據結構等等。
獲取方式:點「在看」,關註公眾號並回復 Java 領取,更多內容陸續奉上。
PS:因公眾號平台更改了推播規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推播才會第一時間出現在你的訂閱列表裏。
點「在看」支持小哈呀,謝謝啦