程序員新人周一優(yōu)化一行代碼,周三被勸退?
這周一,公司新來了一個同事,面試的時候表現(xiàn)得非常不錯,各種問題對答如流,老板和我都倍感欣慰。
這么優(yōu)秀的人,絕不能讓他浪費一分一秒,于是很快,我就發(fā)他了需求文檔、源碼,讓他先在本地熟悉一下業(yè)務(wù)和開發(fā)流程。
結(jié)果沒想到,周三大家一塊 review 代碼的時候就發(fā)現(xiàn)了問題,新來的同事直接把原來 @Transactional 優(yōu)化成了這個鬼樣子:
@Transactional(propagation?=?Propagation.REQUIRED,?rollbackFor?=?Exception.class)
就因為這一行代碼,老板(當(dāng)年也是一線互聯(lián)網(wǎng)大廠的好手)當(dāng)場就發(fā)飆了,馬上就要勸退這位新同事,我就趕緊打圓場,畢竟自己面試的人,不看僧面看佛面,是吧?于是老板答應(yīng)我說再試用一個月看看。
會議結(jié)束后,我就趕緊讓新同事復(fù)習(xí)了一遍事務(wù),以下是他自己做的總結(jié),還是非常詳細的,分享出來給大家一點點參考和啟發(fā)。相信大家看完后就明白為什么不能這樣優(yōu)化 @Transactional 注解了,純屬畫蛇添足和亂用。
關(guān)于事務(wù)
事務(wù)在邏輯上是一組操作,要么執(zhí)行,要不都不執(zhí)行。主要是針對數(shù)據(jù)庫而言的,比如說 MySQL。
只要記住這一點,理解事務(wù)就很容易了。在 Java 中,我們通常要在業(yè)務(wù)里面處理多個事件,比如說編程喵??有一個保存文章的方法,它除了要保存文章本身之外,還要保存文章對應(yīng)的標簽,標簽和文章不在同一個表里,但會通過在文章表里(posts)保存標簽主鍵(tag_id)來關(guān)聯(lián)標簽表(tags):
public?void?savePosts(PostsParam?postsParam)?{
?//?保存文章
?save(posts);
?//?處理標簽
??insertOrUpdateTag(postsParam,?posts);
}
那么此時就需要開啟事務(wù),保證文章表和標簽表中的數(shù)據(jù)保持同步,要么都執(zhí)行,要么都不執(zhí)行。
否則就有可能造成,文章保存成功了,但標簽保存失敗了,或者文章保存失敗了,標簽保存成功了——這些場景都不符合我們的預(yù)期。
為了保證事務(wù)是正確可靠的,在數(shù)據(jù)庫進行寫入或者更新操作時,就必須得表現(xiàn)出 ACID 的 4 個重要特性:
原子性(Atomicity):一個事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會結(jié)束在中間某個環(huán)節(jié)。事務(wù)在執(zhí)行過程中發(fā)生錯誤,會被回滾(Rollback)到事務(wù)開始前的狀態(tài),就像這個事務(wù)從來沒有執(zhí)行過一樣。 一致性(Consistency):在事務(wù)開始之前和事務(wù)結(jié)束以后,數(shù)據(jù)庫的完整性沒有被破壞。 事務(wù)隔離(Isolation):數(shù)據(jù)庫允許多個并發(fā)事務(wù)同時對其數(shù)據(jù)進行讀寫和修改,隔離性可以防止多個事務(wù)并發(fā)執(zhí)行時由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致。 持久性(Durability):事務(wù)處理結(jié)束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。
其中,事務(wù)隔離又分為 4 種不同的級別,包括:
未提交讀(Read uncommitted),最低的隔離級別,允許“臟讀”(dirty reads),事務(wù)可以看到其他事務(wù)“尚未提交”的修改。如果另一個事務(wù)回滾,那么當(dāng)前事務(wù)讀到的數(shù)據(jù)就是臟數(shù)據(jù)。 提交讀(read committed),一個事務(wù)可能會遇到不可重復(fù)讀(Non Repeatable Read)的問題。不可重復(fù)讀是指,在一個事務(wù)內(nèi),多次讀同一數(shù)據(jù),在這個事務(wù)還沒有結(jié)束時,如果另一個事務(wù)恰好修改了這個數(shù)據(jù),那么,在第一個事務(wù)中,兩次讀取的數(shù)據(jù)就可能不一致。 可重復(fù)讀(repeatable read),一個事務(wù)可能會遇到幻讀(Phantom Read)的問題。幻讀是指,在一個事務(wù)中,第一次查詢某條記錄,發(fā)現(xiàn)沒有,但是,當(dāng)試圖更新這條不存在的記錄時,竟然能成功,并且,再次讀取同一條記錄,它就神奇地出現(xiàn)了。 串行化(Serializable),最嚴格的隔離級別,所有事務(wù)按照次序依次執(zhí)行,因此,臟讀、不可重復(fù)讀、幻讀都不會出現(xiàn)。雖然 Serializable 隔離級別下的事務(wù)具有最高的安全性,但是,由于事務(wù)是串行執(zhí)行,所以效率會大大下降,應(yīng)用程序的性能會急劇降低。如果沒有特別重要的情景,一般都不會使用 Serializable 隔離級別。
需要格外注意的是:事務(wù)能否生效,取決于數(shù)據(jù)庫引擎是否支持事務(wù),MySQL 的 InnoDB 引擎是支持事務(wù)的,但 MyISAM 就不支持。
關(guān)于 Spring 對事務(wù)的支持
Spring 支持兩種事務(wù)方式,分別是編程式事務(wù)和聲明式事務(wù),后者最常見,通常情況下只需要一個 @Transactional 就搞定了(代碼侵入性降到了最低),就像這樣:
@Transactional
public?void?savePosts(PostsParam?postsParam)?{
?//?保存文章
?save(posts);
?//?處理標簽
??insertOrUpdateTag(postsParam,?posts);
}
1)編程式事務(wù)
編程式事務(wù)是指將事務(wù)管理代碼嵌入嵌入到業(yè)務(wù)代碼中,來控制事務(wù)的提交和回滾。
你比如說,使用 TransactionTemplate 來管理事務(wù):
@Autowired
private?TransactionTemplate?transactionTemplate;
public?void?testTransaction()?{
????????transactionTemplate.execute(new?TransactionCallbackWithoutResult()?{
????????????@Override
????????????protected?void?doInTransactionWithoutResult(TransactionStatus?transactionStatus)?{
????????????????try?{
????????????????????//?....??業(yè)務(wù)代碼
????????????????}?catch?(Exception?e){
????????????????????//回滾
????????????????????transactionStatus.setRollbackOnly();
????????????????}
????????????}
????????});
}
再比如說,使用 TransactionManager 來管理事務(wù):
@Autowired
private?PlatformTransactionManager?transactionManager;
public?void?testTransaction()?{
??TransactionStatus?status?=?transactionManager.getTransaction(new?DefaultTransactionDefinition());
??????????try?{
???????????????//?....??業(yè)務(wù)代碼
??????????????transactionManager.commit(status);
??????????}?catch?(Exception?e)?{
??????????????transactionManager.rollback(status);
??????????}
}
就編程式事務(wù)管理而言,Spring 更推薦使用 TransactionTemplate。
在編程式事務(wù)中,必須在每個業(yè)務(wù)操作中包含額外的事務(wù)管理代碼,就導(dǎo)致代碼看起來非常的臃腫,但對理解 Spring 的事務(wù)管理模型非常有幫助。
2)聲明式事務(wù)
聲明式事務(wù)將事務(wù)管理代碼從業(yè)務(wù)方法中抽離了出來,以聲明式的方式來實現(xiàn)事務(wù)管理,對于開發(fā)者來說,聲明式事務(wù)顯然比編程式事務(wù)更易用、更好用。
當(dāng)然了,要想實現(xiàn)事務(wù)管理和業(yè)務(wù)代碼的抽離,就必須得用到 Spring 當(dāng)中最關(guān)鍵最核心的技術(shù)之一,AOP,其本質(zhì)是對方法前后進行攔截,然后在目標方法開始之前創(chuàng)建或者加入一個事務(wù),執(zhí)行完目標方法之后根據(jù)執(zhí)行的情況提交或者回滾。
聲明式事務(wù)雖然優(yōu)于編程式事務(wù),但也有不足,聲明式事務(wù)管理的粒度是方法級別,而編程式事務(wù)是可以精確到代碼塊級別的。
事務(wù)管理模型
Spring 將事務(wù)管理的核心抽象為一個事務(wù)管理器(TransactionManager),它的源碼只有一個簡單的接口定義,屬于一個標記接口:
public?interface?TransactionManager?{
}
該接口有兩個子接口,分別是編程式事務(wù)接口 ReactiveTransactionManager 和聲明式事務(wù)接口 PlatformTransactionManager。我們來重點說說 PlatformTransactionManager,該接口定義了 3 個接口方法:
interface?PlatformTransactionManager?extends?TransactionManager{
????//?根據(jù)事務(wù)定義獲取事務(wù)狀態(tài)
????TransactionStatus?getTransaction(TransactionDefinition?definition)
????????????throws?TransactionException;
????//?提交事務(wù)
????void?commit(TransactionStatus?status)?throws?TransactionException;
????//?事務(wù)回滾
????void?rollback(TransactionStatus?status)?throws?TransactionException;
}
通過 PlatformTransactionManager 這個接口,Spring 為各個平臺如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了對應(yīng)的事務(wù)管理器,但是具體的實現(xiàn)就是各個平臺自己的事情了。
參數(shù) TransactionDefinition 和 @Transactional 注解是對應(yīng)的,比如說 @Transactional 注解中定義的事務(wù)傳播行為、隔離級別、事務(wù)超時時間、事務(wù)是否只讀等屬性,在 TransactionDefinition 都可以找得到。
返回類型 TransactionStatus 主要用來存儲當(dāng)前事務(wù)的一些狀態(tài)和數(shù)據(jù),比如說事務(wù)資源(connection)、回滾狀態(tài)等。
TransactionDefinition.java:
public?interface?TransactionDefinition?{
?//?事務(wù)的傳播行為
?default?int?getPropagationBehavior()?{
??return?PROPAGATION_REQUIRED;
?}
?//?事務(wù)的隔離級別
?default?int?getIsolationLevel()?{
??return?ISOLATION_DEFAULT;
?}
??//?事務(wù)超時時間
??default?int?getTimeout()?{
??return?TIMEOUT_DEFAULT;
?}
??//?事務(wù)是否只讀
??default?boolean?isReadOnly()?{
??return?false;
?}
}
Transactional.java
@Target({ElementType.TYPE,?ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public?@interface?Transactional?{
?Propagation?propagation()?default?Propagation.REQUIRED;
?Isolation?isolation()?default?Isolation.DEFAULT;
??int?timeout()?default?TransactionDefinition.TIMEOUT_DEFAULT;
??boolean?readOnly()?default?false;
}
@Transactional 注解中的 propagation 對應(yīng) TransactionDefinition 中的 getPropagationBehavior,默認值為 Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)。@Transactional 注解中的 isolation 對應(yīng) TransactionDefinition 中的 getIsolationLevel,默認值為 DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)。@Transactional 注解中的 timeout 對應(yīng) TransactionDefinition 中的 getTimeout,默認值為TransactionDefinition.TIMEOUT_DEFAULT。 @Transactional 注解中的 readOnly 對應(yīng) TransactionDefinition 中的 isReadOnly,默認值為 false。
說到這,我們來詳細地說明一下 Spring 事務(wù)的傳播行為、事務(wù)的隔離級別、事務(wù)的超時時間、事務(wù)的只讀屬性,以及事務(wù)的回滾規(guī)則。
事務(wù)傳播行為
當(dāng)事務(wù)方法被另外一個事務(wù)方法調(diào)用時,必須指定事務(wù)應(yīng)該如何傳播,例如,方法可能繼續(xù)在當(dāng)前事務(wù)中執(zhí)行,也可以開啟一個新的事務(wù),在自己的事務(wù)中執(zhí)行。
聲明式事務(wù)的傳播行為可以通過 @Transactional 注解中的 propagation 屬性來定義,比如說:
@Transactional(propagation?=?Propagation.REQUIRED)
public?void?savePosts(PostsParam?postsParam)?{
}
TransactionDefinition 一共定義了 7 種事務(wù)傳播行為:
01、PROPAGATION_REQUIRED
這也是 @Transactional 默認的事務(wù)傳播行為,指的是如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前沒有事務(wù),則創(chuàng)建一個新的事務(wù)。更確切地意思是:
如果外部方法沒有開啟事務(wù)的話,Propagation.REQUIRED 修飾的內(nèi)部方法會開啟自己的事務(wù),且開啟的事務(wù)相互獨立,互不干擾。 如果外部方法開啟事務(wù)并且是 Propagation.REQUIRED 的話,所有 Propagation.REQUIRED 修飾的內(nèi)部方法和外部方法均屬于同一事務(wù) ,只要一個方法回滾,整個事務(wù)都需要回滾。
Class?A?{
????@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
????public?void?aMethod?{
????????//do?something
????????B?b?=?new?B();
????????b.bMethod();
????}
}
Class?B?{
????@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
????public?void?bMethod?{
???????//do?something
????}
}
這個傳播行為也最好理解,aMethod 調(diào)用了 bMethod,只要其中一個方法回滾,整個事務(wù)均回滾。
02、PROPAGATION_REQUIRES_NEW
創(chuàng)建一個新的事務(wù),如果當(dāng)前存在事務(wù),則把當(dāng)前事務(wù)掛起。也就是說不管外部方法是否開啟事務(wù),Propagation.REQUIRES_NEW 修飾的內(nèi)部方法都會開啟自己的事務(wù),且開啟的事務(wù)與外部的事務(wù)相互獨立,互不干擾。
Class?A?{
????@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
????public?void?aMethod?{
????????//do?something
????????B?b?=?new?B();
????????b.bMethod();
????}
}
Class?B?{
????@Transactional(propagation=Propagation.REQUIRES_NEW)
????public?void?bMethod?{
???????//do?something
????}
}
如果 aMethod()發(fā)生異常回滾,bMethod()不會跟著回滾,因為 bMethod()開啟了獨立的事務(wù)。但是,如果 bMethod()拋出了未被捕獲的異常并且這個異常滿足事務(wù)回滾規(guī)則的話,aMethod()同樣也會回滾。
03、PROPAGATION_NESTED
如果當(dāng)前存在事務(wù),就在當(dāng)前事務(wù)內(nèi)執(zhí)行;否則,就執(zhí)行與 PROPAGATION_REQUIRED 類似的操作。
04、PROPAGATION_MANDATORY
如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前沒有事務(wù),則拋出異常。
05、PROPAGATION_SUPPORTS
如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前沒有事務(wù),則以非事務(wù)的方式繼續(xù)運行。
06、PROPAGATION_NOT_SUPPORTED
以非事務(wù)方式運行,如果當(dāng)前存在事務(wù),則把當(dāng)前事務(wù)掛起。
07、PROPAGATION_NEVER
以非事務(wù)方式運行,如果當(dāng)前存在事務(wù),則拋出異常。
3、4、5、6、7 這 5 種事務(wù)傳播方式不常用,了解即可。
事務(wù)隔離級別
前面我們已經(jīng)了解了數(shù)據(jù)庫的事務(wù)隔離級別,再來理解 Spring 的事務(wù)隔離級別就容易多了。
TransactionDefinition 中一共定義了 5 種事務(wù)隔離級別:
ISOLATION_DEFAULT,使用數(shù)據(jù)庫默認的隔離級別,MySql 默認采用的是 REPEATABLE_READ,也就是可重復(fù)讀。 ISOLATION_READ_UNCOMMITTED,最低的隔離級別,可能會出現(xiàn)臟讀、幻讀或者不可重復(fù)讀。 ISOLATION_READ_COMMITTED,允許讀取并發(fā)事務(wù)提交的數(shù)據(jù),可以防止臟讀,但幻讀和不可重復(fù)讀仍然有可能發(fā)生。 ISOLATION_REPEATABLE_READ,對同一字段的多次讀取結(jié)果都是一致的,除非數(shù)據(jù)是被自身事務(wù)所修改的,可以阻止臟讀和不可重復(fù)讀,但幻讀仍有可能發(fā)生。 ISOLATION_SERIALIZABLE,最高的隔離級別,雖然可以阻止臟讀、幻讀和不可重復(fù)讀,但會嚴重影響程序性能。
通常情況下,我們采用默認的隔離級別 ISOLATION_DEFAULT 就可以了,也就是交給數(shù)據(jù)庫來決定,可以通過 SELECT @@transaction_isolation; 命令來查看 MySql 的默認隔離級別,結(jié)果為 REPEATABLE-READ,也就是可重復(fù)讀。

事務(wù)的超時時間
事務(wù)超時,也就是指一個事務(wù)所允許執(zhí)行的最長時間,如果在超時時間內(nèi)還沒有完成的話,就自動回滾。
假如事務(wù)的執(zhí)行時間格外的長,由于事務(wù)涉及到對數(shù)據(jù)庫的鎖定,就會導(dǎo)致長時間運行的事務(wù)占用數(shù)據(jù)庫資源。
事務(wù)的只讀屬性
如果一個事務(wù)只是對數(shù)據(jù)庫執(zhí)行讀操作,那么該數(shù)據(jù)庫就可以利用事務(wù)的只讀屬性,采取優(yōu)化措施,適用于多條數(shù)據(jù)庫查詢操作中。
為什么一個查詢操作還要啟用事務(wù)支持呢?
這是因為 MySql(innodb)默認對每一個連接都啟用了 autocommit 模式,在該模式下,每一個發(fā)送到 MySql 服務(wù)器的 SQL 語句都會在一個單獨的事務(wù)中進行處理,執(zhí)行結(jié)束后會自動提交事務(wù)。
那如果我們給方法加上了 @Transactional 注解,那這個方法中所有的 SQL 都會放在一個事務(wù)里。否則,每條 SQL 都會單獨開啟一個事務(wù),中間被其他事務(wù)修改了數(shù)據(jù),都會實時讀取到。
有些情況下,當(dāng)一次執(zhí)行多條查詢語句時,需要保證數(shù)據(jù)一致性時,就需要啟用事務(wù)支持。否則上一條 SQL 查詢后,被其他用戶改變了數(shù)據(jù),那么下一個 SQL 查詢可能就會出現(xiàn)不一致的狀態(tài)。
事務(wù)的回滾策略
默認情況下,事務(wù)只在出現(xiàn)運行時異常(Runtime Exception)時回滾,以及 Error,出現(xiàn)檢查異常(checked exception,需要主動捕獲處理或者向上拋出)時不回滾。
https://tobebetterjavaer.com/exception/gailan.html
如果你想要回滾特定的異常類型的話,可以這樣設(shè)置:
@Transactional(rollbackFor=?MyException.class)
關(guān)于 Spring Boot 對事務(wù)的支持
以前,我們需要通過 XML 配置 Spring 來托管事務(wù),有了 Spring Boot 之后,一切就變得更加簡單了,只需要在業(yè)務(wù)層添加事務(wù)注解(@Transactional)就可以快速開啟事務(wù)。
也就是說,我們只需要把焦點放在 @Transactional 注解上就可以了。
@Transactional 的作用范圍
類上,表明類中所有 public 方法都啟用事務(wù) 方法上,最常用的一種 接口上,不推薦使用
@Transactional 的常用配置參數(shù)
雖然 @Transactional 注解源碼中定義了很多屬性,但大多數(shù)時候,我都是采用默認配置,當(dāng)然了,如果需要自定義的話,前面也都說明過了。
@Transactional 的使用注意事項總結(jié)
1)要在 public 方法上使用,在AbstractFallbackTransactionAttributeSource類的computeTransactionAttribute方法中有個判斷,如果目標方法不是public,則TransactionAttribute返回null,即不支持事務(wù)。
protected?TransactionAttribute?computeTransactionAttribute(Method?method,?@Nullable?Class>?targetClass)?{
????//?Don't?allow?no-public?methods?as?required.
????if?(allowPublicMethodsOnly()?&&?!Modifier.isPublic(method.getModifiers()))?{
??????return?null;
????}
????//?The?method?may?be?on?an?interface,?but?we?need?attributes?from?the?target?class.
????//?If?the?target?class?is?null,?the?method?will?be?unchanged.
????Method?specificMethod?=?AopUtils.getMostSpecificMethod(method,?targetClass);
????//?First?try?is?the?method?in?the?target?class.
????TransactionAttribute?txAttr?=?findTransactionAttribute(specificMethod);
????if?(txAttr?!=?null)?{
??????return?txAttr;
????}
????//?Second?try?is?the?transaction?attribute?on?the?target?class.
????txAttr?=?findTransactionAttribute(specificMethod.getDeclaringClass());
????if?(txAttr?!=?null?&&?ClassUtils.isUserLevelMethod(method))?{
??????return?txAttr;
????}
????if?(specificMethod?!=?method)?{
??????//?Fallback?is?to?look?at?the?original?method.
??????txAttr?=?findTransactionAttribute(method);
??????if?(txAttr?!=?null)?{
????????return?txAttr;
??????}
??????//?Last?fallback?is?the?class?of?the?original?method.
??????txAttr?=?findTransactionAttribute(method.getDeclaringClass());
??????if?(txAttr?!=?null?&&?ClassUtils.isUserLevelMethod(method))?{
????????return?txAttr;
??????}
????}
????return?null;
??}
2)避免同一個類中調(diào)用 @Transactional 注解的方法,這樣會導(dǎo)致事務(wù)失效。
測試事務(wù)是否起效
在測試之前,我們先把 Spring Boot 默認的日志級別 info 調(diào)整為 debug,在 application.yml 文件中 修改:
logging:
??level:
????org:
??????hibernate:?debug
??????springframework:
????????web:?debug
然后,來看修改之前查到的數(shù)據(jù):

開搞。在控制器中添加一個 update 接口,準備修改數(shù)據(jù),打算把沉默王二的狗腿子修改為沉默王二的狗腿:
@RequestMapping("/update")
public?String?update(Model?model)?{
????User?user?=?userService.findById(2);
????user.setName("沉默王二的狗腿");
????userService.update(user);
????return?"update";
}
在 Service 中為方法加上 @Transactional 注解并拋出運行時異常:
@Override
@Transactional
public?void?update(User?user)?{
????userRepository.save(user);
????throw?new?RuntimeException("啊,出現(xiàn)妖怪了!");
}
按照我們的預(yù)期,當(dāng)執(zhí)行 save 保存數(shù)據(jù)后,因為出現(xiàn)了異常,所以事務(wù)要回滾。所以數(shù)據(jù)不會被修改。
在瀏覽器中輸入 http://localhost:8080/user/update 進行測試,注意查看日志,可以確認事務(wù)起效了。

當(dāng)我們把事務(wù)去掉,同樣拋出異常:
@Override
public?void?update(User?user)?{
????userRepository.save(user);
????throw?new?RuntimeException("啊,出現(xiàn)妖怪了!");
}
再次執(zhí)行,發(fā)現(xiàn)雖然程序報錯了,但數(shù)據(jù)卻被更新了。

這也間接地證明,我們的 @Transactional 事務(wù)起效了。
看到這,是不是就明白為什么新同事的優(yōu)化純屬畫蛇添足/卵用了吧?
項目源碼
編程喵:https://github.com/itwanger/coding-more 本項目源碼:https://github.com/itwanger/codingmore-learning
參考來源:
維基百科:https://zh.wikipedia.org/wiki/ACID 維基百科:https://zh.wikipedia.org/wiki/事務(wù)隔離 廖雪峰:https://www.liaoxuefeng.com/wiki/1177760294764384/1179611198786848 JavaGuide:https://juejin.cn/post/6844903608224333838 全菜工程師小輝:https://aijishu.com/a/1060000000013284 空無:https://segmentfault.com/a/1190000040130617 一只襪子:https://www.jianshu.com/p/380a9d980ca5
最后,如果你感興趣的話,歡迎加入 二哥的編程知識星球 (點擊了解詳情),和 150 多名 小伙伴一起交流學(xué)習(xí),這是一個 Java 學(xué)習(xí)指南 + 編程實戰(zhàn)的私密圈子,你可以向二哥提問、幫你制定學(xué)習(xí)計劃、跟著二哥一起做實戰(zhàn)項目。
沒有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不系之舟。
推薦閱讀:

