自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

扯下@EventListener這個(gè)注解的神秘面紗

開(kāi)發(fā) 項(xiàng)目管理
按照歪歪歪師傅的老規(guī)矩,第一步啥也別說(shuō),先搞一個(gè) Demo 出來(lái),沒(méi)有 Demo 的源碼解讀,就像是吃面的時(shí)候沒(méi)有大蒜,差點(diǎn)意思。

你好呀,我是歪歪。

前段時(shí)間看到同事在項(xiàng)目里面使用了一個(gè)叫做 @EventLintener 的注解。

在這之前,我知道這個(gè)注解的用法和想要達(dá)到的目的,但是也僅限于此,其內(nèi)部工作原理對(duì)我來(lái)說(shuō)是一個(gè)黑盒,我完完全全不知道它怎么就實(shí)現(xiàn)了“監(jiān)聽(tīng)”的效果。

現(xiàn)在既然已經(jīng)出現(xiàn)在項(xiàng)目里面了,投入上生產(chǎn)上去使用了,所以我打算盤(pán)一下它,以免以后碰到問(wèn)題的時(shí)候錯(cuò)過(guò)一個(gè)裝逼的...

哦,不。

錯(cuò)過(guò)一個(gè)表現(xiàn)自己的機(jī)會(huì)。

圖片

Demo

首先,按照歪歪歪師傅的老規(guī)矩,第一步啥也別說(shuō),先搞一個(gè) Demo 出來(lái),沒(méi)有 Demo 的源碼解讀,就像是吃面的時(shí)候沒(méi)有大蒜,差點(diǎn)意思。

先鋪墊一個(gè)背景吧。

假設(shè)現(xiàn)在的需求是用戶注冊(cè)成功之后給他發(fā)個(gè)短信,通知他一下。

正常來(lái)說(shuō),偽代碼很簡(jiǎn)單:

boolean success = userRegister(user);
if(success){
sendMsg("客官,你注冊(cè)成功了哦。記得來(lái)玩兒~");
}

這代碼能用,完全沒(méi)有任何問(wèn)題。但是,你仔細(xì)想,發(fā)短信通知這個(gè)動(dòng)作按理來(lái)說(shuō),不應(yīng)該和用戶注冊(cè)的行為“耦合”在一起,難道你短信發(fā)送的時(shí)候失敗了,用戶就不算注冊(cè)成功嗎?

上面的代碼就是一個(gè)耦合性很強(qiáng)的代碼。

怎么解耦呢?

應(yīng)該是在用戶注冊(cè)成功之后,發(fā)布一個(gè)“有用戶注冊(cè)成功了”的事件:

boolean success = userRegister(user);
if(success){
publicRegisterSuccessEvent(user);
}

然后有地方去監(jiān)聽(tīng)這個(gè)事件,在監(jiān)聽(tīng)事件的地方觸發(fā)“短信發(fā)送”的動(dòng)作。

這樣的好處是后續(xù)假設(shè)不發(fā)短信了,要求發(fā)郵件,或者短信、郵件都要發(fā)送,諸如此類(lèi)的需求變化,我們的用戶注冊(cè)流程的代碼不需要進(jìn)行任何變化,僅僅是在事件監(jiān)聽(tīng)的地方搞事情就完事了。

這樣就算是完成了兩個(gè)動(dòng)作的“解耦”。

怎么做呢?

我們可以基于 Spring 提供的 ApplicationListener 去做這個(gè)時(shí)間。

我的 Demo 里面用的 Spring 版本是 5.2.10。

這次的 Demo 也非常的簡(jiǎn)單,我們首先需要一個(gè)對(duì)象來(lái)封裝事件相關(guān)的信息,比如我這里用戶注冊(cè)成功,肯定要關(guān)心的是 userName:

@Data
public class RegisterSuccessEvent {

private String userName;

public RegisterSuccessEvent(String userName) {
this.userName = userName;
}
}

我這里只是為了做 Demo,對(duì)象很簡(jiǎn)單,實(shí)際使用過(guò)程中,你需要什么字段就放進(jìn)去就行。

然后需要一個(gè)事件的監(jiān)聽(tīng)邏輯:

@Slf4j
@Component
public class RegisterEventListener {

@EventListener
public void handleNotifyEvent(RegisterSuccessEvent event) {
log.info("監(jiān)聽(tīng)到用戶注冊(cè)成功事件:" +
"{},你注冊(cè)成功了哦。記得來(lái)玩兒~", event.getUserName());
}

}

接著,通過(guò) Http 接口來(lái)進(jìn)行事件發(fā)布:

@Resource
private ApplicationContext applicationContext;
@GetMapping("/publishEvent")
public void publishEvent() {
applicationContext.publishEvent(new RegisterSuccessEvent("歪歪"));
}

最后把服務(wù)啟動(dòng)起來(lái),調(diào)用一次:

圖片

輸出正常,完事兒,這個(gè) Demo 就算是搞定了,就只有十多行代碼。

這么簡(jiǎn)單的 Demo 你都不想親自動(dòng)手去搭一個(gè)的話,想要靠肉眼學(xué)習(xí)的話,那么我只能說(shuō):

圖片

Debug

來(lái),我問(wèn)你,如果是你的話,就這幾行代碼,第一個(gè)斷點(diǎn)你會(huì)打在哪里?

這沒(méi)啥好猶豫的,肯定是選擇打事件監(jiān)聽(tīng)的這個(gè)地方:

圖片

然后直接就是一個(gè)發(fā)起調(diào)用,拿到調(diào)用棧再說(shuō):

圖片

通過(guò)觀察調(diào)用棧發(fā)現(xiàn),全是 Spring 的 event 包下的方法。

此時(shí),我還是一頭霧水的,完全不知道應(yīng)該怎么去看,所以我只有先看第一個(gè)涉及到 Spring 源碼的地方,也就是這個(gè)反射調(diào)用的地方:

org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke

圖片

通過(guò)觀察這三個(gè)關(guān)鍵的參數(shù),我們可以斷定此時(shí)確實(shí)是通過(guò)反射在調(diào)用我們 Demo 里面的 RegisterEventListener 類(lèi)的 handleNotifyEvent 方法,入?yún)⑹?RegisterSuccessEvent 對(duì)象,其 userName 字段的值是“歪歪”:

圖片

此時(shí),我的第一個(gè)問(wèn)題就來(lái)了:Spring 是怎么知道要去觸發(fā)我的這個(gè)方法的呢?

或者換個(gè)問(wèn)法:handleNotifyEvent 這個(gè)我自己寫(xiě)的方法名稱怎么就出現(xiàn)在這里了呢?

然后順著這個(gè) method 找過(guò)去一看:

圖片

哦,原來(lái)是當(dāng)前類(lèi)的一個(gè)字段,隨便還看到了 beanName,也是其一個(gè)字段,對(duì)應(yīng)著 Demo 的 RegisterEventListener。

到這里,第二個(gè)問(wèn)題就隨之而來(lái)了:既然關(guān)鍵字段都在當(dāng)前類(lèi)里面了,那么這個(gè)當(dāng)前類(lèi),也就是 ApplicationListenerMethodAdapter 是什么時(shí)候冒出來(lái)的呢?

帶著這個(gè)問(wèn)題,繼續(xù)往下查看調(diào)用棧,會(huì)看到這里的這個(gè) listener 就是我們要找的這個(gè)“當(dāng)前類(lèi)”:

圖片

所以,我們的問(wèn)題就變成了,這個(gè) listener 是怎么來(lái)的?

然后你就會(huì)來(lái)到這個(gè)地方,把目光停在這個(gè)地方:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

圖片

為什么會(huì)在這個(gè)地方停下來(lái)呢?

因?yàn)樵谶@個(gè)方法里面,就是整個(gè)調(diào)用鏈中 listener 第一次出現(xiàn)的地方。

所以,第二個(gè)斷點(diǎn)的位置,我們也找到了,就是這個(gè)地方:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

圖片

但是,朋友們注意,我要但是了。

但是,當(dāng)然把斷點(diǎn)打在這個(gè)地方,重啟服務(wù)準(zhǔn)備調(diào)試的時(shí)候,你會(huì)發(fā)現(xiàn)重啟的過(guò)程中就會(huì)停在斷點(diǎn)處,而停下來(lái)的時(shí)候,你去調(diào)試會(huì)發(fā)現(xiàn)根本就不是你所關(guān)心的邏輯。

全是 Spring 啟動(dòng)過(guò)程中觸發(fā)的一些框架的監(jiān)聽(tīng)邏輯。比如應(yīng)用啟動(dòng)事件,就會(huì)在斷點(diǎn)處停下:

圖片

怎么辦呢?

圖片

針對(duì)這種情況,有兩個(gè)辦法。

第一個(gè)是服務(wù)啟動(dòng)過(guò)程中,把斷點(diǎn)停用,啟動(dòng)完成之后再次打開(kāi)斷點(diǎn),然后觸發(fā)調(diào)用。

idea 也提供了這樣的功能,這個(gè)圖標(biāo)就是全局的斷點(diǎn)啟用和停用的圖標(biāo):

圖片

這個(gè)方法在我們本次調(diào)試的過(guò)程中是行之有效的,但是假設(shè)如果以后你想要調(diào)試的代碼,就是要在框架啟動(dòng)過(guò)程中調(diào)試的代碼呢?

所以,我更想教你第二種方案:使用條件斷點(diǎn)。

通過(guò)觀察入?yún)?,我們可以看?event 對(duì)象里面有個(gè) payload 字段,里面放的就是我們 Demo 中的 RegisterSuccessEvent 對(duì)象:

圖片

那么,我們可不可以打上斷點(diǎn),然后讓 idea 識(shí)別到是上述情況的時(shí)候,即有 RegisterSuccessEvent 對(duì)象的時(shí)候,才在斷點(diǎn)處停下來(lái)呢?

當(dāng)然是可以的,打條件斷點(diǎn)就行。

在斷點(diǎn)處右鍵,然后彈出框里面有個(gè) Condition 輸入框:

圖片

Condition,都認(rèn)識(shí)吧,高考詞匯,四級(jí)詞匯了,抓緊時(shí)間背一背:

圖片

在 idea 的斷點(diǎn)這里,它是“條件”的意思,帶著個(gè)輸入框,代表讓你輸入條件的意思。

另外,關(guān)于 Condition 還有一個(gè)短語(yǔ),叫做 in good condition。

反應(yīng)過(guò)來(lái)大概是“狀況良好”的意思。

比如:我已出倉(cāng),in good condition。

再比如:Your hair is not in good condition。

就是說(shuō)你頭發(fā)狀況不太好,需要注意一下。

圖片

扯遠(yuǎn)了,說(shuō)回條件斷點(diǎn)。

在這里,我們的條件是:event 對(duì)象里面的 payload 字段放的是我們 Demo 中的 RegisterSuccessEvent 對(duì)象時(shí)就停下來(lái)。

所以應(yīng)該是這樣的:

event instanceof PayloadApplicationEvent && (((PayloadApplicationEvent) event).payload instanceof RegisterSuccessEvent)

圖片

當(dāng)我們這樣設(shè)置完成之后,重啟項(xiàng)目,你會(huì)發(fā)現(xiàn)重啟過(guò)程非常絲滑,并沒(méi)有在斷點(diǎn)處停下來(lái),說(shuō)明我們的條件斷點(diǎn)起作用了。

然后,我們?cè)俅伟l(fā)起調(diào)用,在斷點(diǎn)處停下來(lái)了:

圖片

主要關(guān)注 134 行的 listener 是怎么來(lái)的。

當(dāng)我們觀察 getApplicationListeners 方法的時(shí)候,會(huì)發(fā)現(xiàn)這個(gè)方法它主要是在對(duì) retrieverCache 這個(gè)緩存在搞事情。

圖片

這個(gè)緩存里面放的就是在項(xiàng)目啟動(dòng)過(guò)程中已經(jīng)觸發(fā)過(guò)的框架自帶的 listener 對(duì)象:

圖片

調(diào)用的時(shí)候,如果能從緩存中拿到對(duì)應(yīng)的 listener,則直接返回。而我們 Demo 中的自定義 listener 是第一次觸發(fā),所以肯定是沒(méi)有的。

因此關(guān)鍵邏輯就在 retrieveApplicationListeners 方法里面:

org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners

這個(gè)方法里面的邏輯較多,我不會(huì)逐行解析。

只說(shuō)一下這個(gè)關(guān)鍵的 for 循環(huán):

圖片

這個(gè) for 循環(huán)在干啥事呢?

就是循環(huán)當(dāng)前所有的 listener,過(guò)濾出能處理當(dāng)前這個(gè)事件的 listener。

可以看到當(dāng)前一共有 20 個(gè) listener,最后一個(gè) listener 就是我們自定義的 registerEventListener:

圖片

每一個(gè) listener 都經(jīng)過(guò)一次 supportsEvent 方法判斷:

supportsEvent(listener, eventType, sourceType)

這個(gè)方法,就是判斷 listener 是否支持給定的事件:

圖片

因?yàn)槲覀冎喇?dāng)前的事件是我們發(fā)布的 RegisterSuccessEvent 對(duì)象。

對(duì)應(yīng)到源碼中,這里給定的事件,也就是 eventType 字段,對(duì)應(yīng)的就是我們的 RegisterSuccessEvent 對(duì)象。

圖片

所以當(dāng)循環(huán)到我們的 registerEventListener 的時(shí)候,在 supportsEventType 方法中,用 eventType 和 declaredEventTypes 做了一個(gè)對(duì)比,如果比上了,就說(shuō)明當(dāng)前的 listener 能處理這個(gè) eventType。

前面說(shuō)了 eventType 是 RegisterSuccessEvent 對(duì)象。

那么這個(gè) declaredEventTypes 是個(gè)啥玩意呢?

declaredEventTypes 字段也在之前就出現(xiàn)過(guò)的 ApplicationListenerMethodAdapter 類(lèi)里面。supportsEventType 方法也是這個(gè)類(lèi)的方法:

圖片

而這個(gè) declaredEventTypes,就是 RegisterSuccessEvent 對(duì)象:

圖片

這不就呼應(yīng)上了嗎?

所以,這個(gè) for 循環(huán)結(jié)束之后,里面一定是有 registerEventListener的,因?yàn)樗芴幚懋?dāng)前的 RegisterSuccessEvent 這個(gè)事件。

圖片

但是你會(huì)發(fā)現(xiàn)循環(huán)結(jié)束之后 list 里面有兩個(gè)元素,突然冒出來(lái)個(gè) DelegatingApplicationListener 是什么鬼?

這個(gè)時(shí)候怎么辦?

別去研究它,它不會(huì)影響我們的程序運(yùn)行,所以可以先做個(gè)簡(jiǎn)單的記錄,不要分心,要抓住主要矛盾。

經(jīng)過(guò)前面的一頓分析,我們現(xiàn)在又可以回到這里了。

通過(guò) debug 我們知道這個(gè)時(shí)候我們拿到的就是我們自定義的 listener 了:

圖片

從這個(gè) listener 里面能拿到類(lèi)名、方法名,從 event 中能拿到請(qǐng)求參數(shù)。

后續(xù)反射調(diào)用的過(guò)程,條件齊全,順理成章的就完成了事件的發(fā)布。

看到這里,你細(xì)細(xì)回想一下,整個(gè)的調(diào)試過(guò)程,是不是一環(huán)扣一環(huán)。只要思路不亂,抓住主干,問(wèn)題不大。

進(jìn)一步思考

到這里,你是不是認(rèn)為已經(jīng)調(diào)試的差不多了?

自己已經(jīng)知道了 Spring 自定義 listener 的大致工作原理了?

閉著眼睛想一想也就知道大概是一個(gè)什么流程了?

那么我問(wèn)你一個(gè)問(wèn)題:你回想一下我最最開(kāi)始定位到反射這個(gè)地方的時(shí)候是怎么說(shuō)的?

圖片

是不是給了你這一張圖,說(shuō) beanName、method、declaredEventTypes 啥的都在 ApplicationListenerMethodAdapter 這個(gè)類(lèi)里面?

請(qǐng)問(wèn):這些屬性是什么時(shí)候設(shè)置到這個(gè)類(lèi)里面的呢?

圖片

這個(gè)...

好像...

是不是確實(shí)沒(méi)講?

是的,所以說(shuō)這部分我也得給你補(bǔ)上。

但是如果我不主動(dòng)提,你是不是也想不起來(lái)呢,所以我也完全可以就寫(xiě)到這里就結(jié)束了。

我把這部分單獨(dú)寫(xiě)一個(gè)小節(jié)就是提一下這個(gè)問(wèn)題:如果你只是跟著網(wǎng)上的文章看,特別是源碼解讀或者方案設(shè)計(jì)類(lèi)文章,只是看而不帶著自己的思路,不自己親自下手,其實(shí)很多問(wèn)題你思考不全的,關(guān)鍵是看完以后你還會(huì)誤以為你學(xué)全了。

圖片

現(xiàn)在我們看一下 ApplicationListenerMethodAdapter 這個(gè)類(lèi)是咋來(lái)的。

我們不就是想看看 beanName 是啥時(shí)候和這個(gè)類(lèi)扯上關(guān)系的嘛,很簡(jiǎn)單,剛剛才提到的條件斷點(diǎn)又可以用起來(lái)了:

圖片

重啟之后,在啟動(dòng)的過(guò)程中就會(huì)在構(gòu)造方法中停下,于是我們又有一個(gè)調(diào)用棧了:

圖片

可以看到,在這個(gè)構(gòu)造方法里面,就是在構(gòu)建我們要尋找的 beanName、method、declaredEventTypes 這類(lèi)字段。

而之所以會(huì)觸發(fā)這個(gè)構(gòu)造方法,是因?yàn)?Spring 容器在啟動(dòng)的過(guò)程中調(diào)用了下面這個(gè)方法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

圖片

在這個(gè)方法里面,會(huì)去遍歷 beanNames,然后在 processBean 方法里面找到帶有 @EventListener 注解的 bean:

圖片

在標(biāo)號(hào)為 ① 地方找到這個(gè) bean 具體是哪些方法標(biāo)注了 @EventListener。

在標(biāo)號(hào)為 ② 的地方去觸發(fā) ApplicationListenerMethodAdapter 類(lèi)的構(gòu)造方法,此時(shí)就可以把 beanName,代理目標(biāo)類(lèi),代理方法通過(guò)參數(shù)傳遞過(guò)去。

在標(biāo)號(hào)為 ③ 的地方,將這個(gè) listener 加入到 Spring 的上下文中,后續(xù)觸發(fā)的時(shí)候直接從這里獲取即可。

那么 afterSingletonsInstantiated 這個(gè)方法是什么時(shí)候觸發(fā)的呢?

還是看調(diào)用棧:

圖片

你即使再不熟悉 Spring,你至少也聽(tīng)說(shuō)過(guò)容器啟動(dòng)過(guò)程中有一個(gè) refresh 的動(dòng)作吧?

就是這個(gè)地方:

圖片

這里,refreshContext,就是整個(gè) SpringBoot 框架啟動(dòng)過(guò)程的核心方法中的一步。

就是在這個(gè)方法里面中,在服務(wù)啟動(dòng)的過(guò)程中,ApplicationListenerMethodAdapter 這個(gè)類(lèi)和一個(gè) beanName 為 registerEventListener 的類(lèi)扯上了關(guān)系,為后續(xù)的事件發(fā)布的動(dòng)作,埋好了伏筆。

細(xì)節(jié)

前面了解了關(guān)于 Spring 的事件發(fā)布機(jī)制主干代碼的流程之后,相信你已經(jīng)能從“容器啟動(dòng)時(shí)”和“請(qǐng)求發(fā)起時(shí)”這兩個(gè)階段進(jìn)行了一個(gè)粗獷的說(shuō)明了。

但是,注意,我又要“但是”了。

里面其實(shí)還有很多細(xì)節(jié)需要注意的,比如事件發(fā)布是一個(gè)串行化的過(guò)程。假設(shè)某個(gè)事件監(jiān)聽(tīng)邏輯處理時(shí)間很長(zhǎng),那么勢(shì)必會(huì)導(dǎo)致其他的事件監(jiān)聽(tīng)出現(xiàn)等待的情況。

比如我搞兩個(gè)事件監(jiān)聽(tīng)邏輯,在其中一個(gè)的處理邏輯中睡眠 3s,模擬業(yè)務(wù)處理時(shí)間。發(fā)起調(diào)用之后,從日志輸出時(shí)間上可以看出來(lái),確實(shí)是串行化,確實(shí)是出現(xiàn)了等待的情況:

圖片

針對(duì)這個(gè)問(wèn)題,我們前面講源碼關(guān)于獲取到 listener 之后,其實(shí)有這樣的一個(gè)邏輯:

圖片

這不就是線程池異步的邏輯嗎?

只不過(guò)默認(rèn)情況下是沒(méi)有開(kāi)啟線程池的。

開(kāi)始之后,日志就變成了這樣:

圖片

那么怎么開(kāi)啟呢?

主干流程都給你說(shuō)了個(gè)大概了,這些分支細(xì)節(jié),就自己去研究吧。

再比如,@EventListener 注解里面還有這兩個(gè)參數(shù),我們是沒(méi)有使用到的:

圖片

它應(yīng)該怎么使用并且其到的作用是什么呢?

對(duì)應(yīng)的源碼是哪個(gè)部分呢?

這也是屬于分支細(xì)節(jié)的部分,自己去研究吧

再再比如,前面講到 ApplicationListenerMethodAdapter 這個(gè)類(lèi)的時(shí)候:

圖片

你會(huì)發(fā)現(xiàn)它還有一個(gè)子類(lèi),點(diǎn)過(guò)去一看,它有一個(gè)叫做 ApplicationListenerMethodTransactionalAdapter 的兒子:

圖片

這個(gè)兒子的名字里面帶著個(gè) “Transactional”,你就知道這是和事務(wù)相關(guān)的東西了。

它里面有個(gè)叫做 TransactionalEventListener 的字段,它也是一個(gè)注解,里面對(duì)應(yīng)著事務(wù)的多個(gè)不同階段:

圖片

想都不用想,肯定是可以針對(duì)事務(wù)不同階段進(jìn)行事件監(jiān)聽(tīng)。

這部分“兒子”的邏輯,是不是也可以去研究研究。

再再再比如,前面提到了 Spring 容器在啟動(dòng)的過(guò)程中調(diào)用了下面這個(gè)方法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

圖片

這個(gè)方法屬于哪個(gè)類(lèi)?

它屬于 EventListenerMethodProcessor 這個(gè)類(lèi)。

那么請(qǐng)問(wèn)這個(gè)類(lèi)是什么時(shí)候出現(xiàn)在 Spring 容器里面的呢?

圖片

這個(gè)...

好像...

是不是確實(shí)沒(méi)講?

是的,但是這個(gè)類(lèi)在整個(gè)框架里面只有一次調(diào)用:

圖片

調(diào)試起來(lái)那不是手拿把掐的事情?

也可以去研究研究嘛,看著看著,不就慢慢的從 @EventLintener 這個(gè)小口子,把源碼越撕越大了?

圖片

好了,本文的技術(shù)部分就到這里啦。

 本文轉(zhuǎn)載自微信公眾號(hào)「 why技術(shù)」,作者北上。轉(zhuǎn)載本文請(qǐng)聯(lián)系why技術(shù)公眾號(hào)。

責(zé)任編輯:武曉燕 來(lái)源: why技術(shù)
相關(guān)推薦

2022-03-31 09:26:33

代碼注解

2015-08-20 13:43:17

NFV網(wǎng)絡(luò)功能虛擬化

2011-11-18 09:26:18

Javafinally

2010-05-17 09:13:35

2014-03-12 11:11:39

Storage vMo虛擬機(jī)

2021-06-07 08:18:12

云計(jì)算云端阿里云

2010-05-26 19:12:41

SVN沖突

2009-09-15 15:34:33

Google Fast

2023-11-02 09:55:40

2016-04-06 09:27:10

runtime解密學(xué)習(xí)

2011-06-22 09:43:01

C++

2009-06-01 09:04:44

Google WaveWeb

2018-03-01 09:33:05

軟件定義存儲(chǔ)

2010-06-17 10:53:25

桌面虛擬化

2011-08-02 08:59:53

2017-10-16 05:56:00

2021-07-28 21:49:01

JVM對(duì)象內(nèi)存

2021-08-11 09:01:48

智能指針Box

2021-09-17 15:54:41

深度學(xué)習(xí)機(jī)器學(xué)習(xí)人工智能

2020-04-14 10:44:01

區(qū)塊鏈滲透測(cè)試比特幣
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)