網站首頁 編程語言 正文
引言
從前面我們可以大致了解了協程的玩法,如果一個協程中使用子協程,那么該協程會等待子協程執行結束后才真正退出,而達到這種效果的原因就是協程上下文,上下文貫穿了協程的生命周期,這套思想和我們app的上下文很像
在開始真正了解協程上下文之前,我們先來看看下面的例子
下面的圖代表了一個協程a的生命,就像一條從上至下的直線,它的生命只有100ms
當我們在a協程延遲函數100ms之前開啟一個子協程b,b做了200ms的事情,如果不考慮調度消耗的時間,那么a協程的生命也會延長成200ms
代碼驗證下:
fun `test context life`() = runBlocking {
//定義一個作用域
val a = CoroutineScope(Dispatchers.Default)
val startTime = System.currentTimeMillis()
//協程a開啟
val jobA = a.launch {
//子協程b開啟
val jobB = launch {
delay(200)
}
delay(100)
}
//等待協程a結束
jobA.join()
val endTime = System.currentTimeMillis()
println(endTime - startTime)
}
fun main() {
`test context life`()
}
結果:237
如果我們把子協程b增加到delay 300ms,那么結果也會相應的變為:
323
通過上面的列子,來對協程上下文的有一個初步概念:可以說協程的生命周期,就是上下文的生命周期
協程擁有很多新的概念,很多人一開始接觸就能難理解(包括我自己),這些概念都是在上下文的基礎上引申而來的,所以我一再強調它的重要性,協程的上下文必須理解透,才能玩好協程,接下來我們來真正了解協程上下文
一、協程上下文
1.CoroutineContext
協程上下文有以下幾項構成,它們都是實現了CoroutineContext.Element接口,有些是實現了AbstractCoroutineContextElement接口,而AbstractCoroutineContextElement繼承CoroutineContext.Element接口
1.Job:控制協程的生命周期,也是我們能拿到操作協程任務的唯一對象
2.CoroutineDispatcher:就是之前介紹的調度器
3.CoroutineName:協程的名字,一般輸出日志用的
4.CoroutineExceptionHandler:處理未捕獲的異常
協程上下文實現了運算符重載,我們可以用+號來組合一個CoroutineContext的元素
2.CorountineScope
一般情況下,協程體內所有的子協程,都繼承至根協程,協程的繼承的關系不是我們所了解的類的繼承關系,而是父協程和子協程的生命周期關系,還記得我們上面舉得例子么,除非在協程體內自己手動創建協程作用域,即:創建一個全新的協程上下文,我們之前已經介紹過了:
CorountineScope:創建協程作用域,新起線程,觀察源碼,內部實際實例化的是ContextScope,ContextScope被internal修飾,內部使用,我們實例化不了
其他的實際上都是繼承父協程上下文,或者內部實例化了ContextScope:
1.runBlocking:將主線程轉變為協程,會阻塞主線程,實際上用的是一個EmptyCoroutineContext作為上下文,它是一個主線程的協程上下文,靜態的全局變量,我們其實就可以理解成是主線程
2.GlobalScope:也是用的EmptyCoroutineContext
3.MainScope:使用ContextScope構造了新的上下文
4.coroutineScope:繼承的父協程上下文,不能算是全新的協程
等等
3.子協程繼承父協程
子協程繼承父協程時,除了Job會自動創建新的實例外,其他3項的不手動指定的話,都會自動繼承父協程的,Job對應的是協程任務,每次新的任務肯定都是新的Job對象
有了這些概念后,接下來通過代碼,再熟悉鞏固下
例子1:
fun `test context life1`() = runBlocking {
//定義一個作用域
val a = CoroutineScope(Dispatchers.Default)
//協程a開啟
val jobA = a.launch {
delay(100)
println("jobA finished")
}
println("main finished")
}
結果:
main finished
由于a是一個根協程,全新的上下文,runBlocking 是主線程的協程上下文,所以當a開啟任務時,不會阻塞主線程,當我們的進程都跑完了,jobA finished肯定不會打印了
例子2:
fun `test context life2`() = runBlocking {
//定義一個作用域
val a = CoroutineScope(Dispatchers.Default)
//協程a開啟
val jobA = a.launch {
delay(100)
println("jobA finished")
}
jobA.join()
println("main finished")
}
結果:
jobA finished
main finished
我們在主協程(主線程的協程)中,手動調用jobA的join方法,那么主線程就會阻塞,直到jobA執行完畢。這個和我們的多線程操作是一樣的,主線程等待A線程執行完后再往后執行
例子3:
fun `test context life3`() = runBlocking {
launch {
delay(100)
println("jobA finished")
}
println("main finished")
}
結果:
main finished
jobA finished
這回我們沒有構建新的協程作用域,而是在根協程中直接使用子協程的方式,當然了,協程的上下文繼承關系,使得我們的主協程等待子協程執行完畢后才結束生命
例子4:
fun `test context life4`() = runBlocking {
launch(Dispatchers.IO + CoroutineName("jobA")) {
delay(100)
println("${coroutineContext[CoroutineName]} finished")
}
println("main finished")
}
結果:
main finished
CoroutineName(jobA) finished
即使我們指定了子協程的調度器和協程名,也不會影響協程上下文繼承關系,主協程還是會等待子協程執行完畢后才結束生命
如果你已經完全理解了,那么就可以知道以上例子使用async啟動也是一樣的效果
二、協程的異常傳遞
1.協程的異常傳播
協程的異常傳播也是遵循了協程上下文的機制,除了取消異常(CancellationException)外,當一個協程有了異常,如果沒有主動捕獲異常,那么異常會向上傳播,直到根協程,子協程的異常都會導致根協程退出,自然其他子協程也會退出
例子1:
fun `test coroutineScope exception1`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(3000)
println("finished")
}
結果:
job2 finished
Exception in thread "main" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception1$1$job2$1.invokeSuspend(exceptionTest.kt:46)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.ResumeModeKt.resumeMode(ResumeMode.kt:67)
? ? at kotlinx.coroutines.DispatchedKt.resume(Dispatched.kt:309)
? ? at kotlinx.coroutines.DispatchedKt.dispatch(Dispatched.kt:298)
? ? at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:250)
? ? at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:260)
? ? at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:332)
? ? at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.kt:298)
? ? at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116)
? ? at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:80)
? ? at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
? ? at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
? ? at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
? ? at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.test coroutineScope exception1(exceptionTest.kt:37)
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.main(exceptionTest.kt:54)
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.main(exceptionTest.kt)Process finished with exit code 1
job2 1000ms后就發生了異常,導致job1和父協程都直接退出
2.不同上下文(沒有繼承關系)之間協程異常會怎么樣?
例子1:
fun `test coroutineScope exception2`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).launch{
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
delay(3000)
println("finished")
}
結果:
job2 finished
Exception in thread "DefaultDispatcher-worker-2" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception1$1$job2$1.invokeSuspend(exceptionTest.kt:46)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
job finished
finishedProcess finished with exit code 0
可以看出不同根協程的協程之間,異常并不會自動傳遞,我們的主線程上下文協程正常執行
再看例子2:
fun `test coroutineScope exception3`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).async{
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
delay(3000)
println("finished")
}
結果:
job2 finished
job finished
finished
和例子1的唯一區別是,使用了全新上下文的協程使用了async啟動,哈哈,這就奇怪了,為什么會這樣?
3.向用戶暴露異常
還記得async啟動的協程返回的是一個Deferred么,它可以使用await函數,來獲取協程運行結果。那么試想一下,如果我就是想要一個協程執行完返回一個異常呢?
所以async中的異常會作為返回值,返回給調用await函數
fun `test coroutineScope exception4`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).async{
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
job2.await()
delay(3000)
println("finished")
}
結果:
job2 finished
Exception in thread "main" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception4$1$job2$1.invokeSuspend(exceptionTest.kt:96)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)Process finished with exit code 1
await的時候出現異常了,當然會導致協程退出,我們可以在await的時候捕獲下這個異常,就不會影響主線程上下文的協程運行了
fun `test coroutineScope exception4`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).async {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
try {
job2.await()
} catch (e: Exception) {
e.printStackTrace()
}
delay(3000)
println("finished")
}
結果:
job2 finished
java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception4$1$job2$1.invokeSuspend(exceptionTest.kt:96)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
job finished
finishedProcess finished with exit code 0
值得注意的是,同一繼承關系下的協程使用await并無法捕獲異常,還是會遵循第一條,導致整個協程生命周期結束
fun `test coroutineScope exception5`() = runBlocking {
val job2 = CoroutineScope(Dispatchers.IO).launch {
val job1 = launch {
delay(2000)
println("job finished")
}
val job3 = async {
delay(1000)
println("job3 finished")
throw IllegalArgumentException()
}
try {
job3.await()
} catch (e: Exception) {
e.printStackTrace()
}
delay(2000)
println("job2 finished")
}
job2.join()
println("finished")
}
結果:
job3 finished
java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception5$1$job2$1$job3$1.invokeSuspend(exceptionTest.kt:119)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception5$1$job2$1$job3$1.invokeSuspend(exceptionTest.kt:119)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finishedProcess finished with exit code 0
可以發現job3.await()的try catch并沒有生效,所以向用戶暴露異常只適用于不同上下文(沒有繼承關系)的協程
三、協程的異常處理
使用SupervisorJob
如果想要一個協程出現異常后,不影響其繼承關系中的其他協程,可以使用SupervisorJob
fun `test SupervisorJob exception`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = async(SupervisorJob()) {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(3000)
println("finished")
}
結果:
job2 finished
job finished
finished
可以看到,job2的異常并沒有影響其他繼承關系的協程的執行
SupervisorScope,這個我們前面已經用過了,就不重復介紹了
異常捕獲器CoroutineExceptionHandler
協程上下文的4項之一,可以用CrashHandler理解,不過它并不能阻止協程的退出,只能夠獲取異常的信息
它使用有兩個條件:
1.異常是自動拋出異常(launch)
2.實例化CoroutineScope的時候指定異常捕獲器 或者 在一個根協程中
例子1:
fun `test SupervisorHandler exception1`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(handler)
val job1 = scope.launch {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(2000)
println("job finished")
}
delay(4000)
println("finished")
}
結果:
job2 finished
caught: java.lang.IllegalArgumentException
finished
job2拋出了異常,被捕獲到了,但是scope的其他協程隨之生命周期也都結束了
例子2:
fun `test SupervisorHandler exception2`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(2000)
println("job finished")
}
delay(4000)
println("finished")
}
結果:
job2 finished
caught: java.lang.IllegalArgumentException
finished
和例子1相同,因為我們handler指定在了根協程
例子3:
fun `test SupervisorHandler exception3`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
val job2 = launch(handler) {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(2000)
println("job finished")
}
delay(4000)
println("finished")
}
結果:
job2 finished
Exception in thread "DefaultDispatcher-worker-4" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test SupervisorHandler exception$1$job1$1$job2$1.invokeSuspend(exceptionTest.kt:161)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finishedProcess finished with exit code 0
handler不是在根協程中,不能捕獲
如果一個子協程會拋出異常,那么對它進行等待時(join或await),包裹一層try catch 會出現意料之外的事
例子4:
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
// val job3 = scope.launch {
// println("job3 finished")
// }
println("job delay")
delay(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
結果:
job2 finished
job delay
caught: java.lang.IllegalArgumentException
finished
如果把scope根協程中的delay函數注釋掉,會怎么樣呢?
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
// val job3 = scope.launch {
// println("job3 finished")
// }
println("job delay")
// delay(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
結果:
job2 finished
job delay
0
1
2
3
4
5
6
7
8
9
10
job finished
caught: java.lang.IllegalArgumentException
如果不包裹try catch 那么println("job delay")都不會執行
由例子4和例子5,我們可以推斷,如果子協程有異常發生了,我們在等待時捕獲異常后,根協程執行了掛起函數,那么它會直接中斷,不執行掛起函數以下的代碼,如果沒有掛起函數,那么后面的代碼還是會執行
為了加強驗證這點,我們使用Thread.sleep(2000)替換delay函數測試下:
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
// val job3 = scope.launch {
// println("job3 finished")
// }
println("job delay")
// delay(2000)
Thread.sleep(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
結果還是和例子5一樣:
job2 finished
job delay
0
1
2
3
4
5
6
7
8
9
10
job finished
caught: java.lang.IllegalArgumentException
finished
Process finished with exit code 0
其實出現這個情況,和我們之前取消協程是一樣的,出現異常后會開始取消協程,但是CPU密集型的代碼還會執行,但是遇到掛起函數就會拋一個CancellationException,導致協程結束運行,如果我們在掛起函數加上try catch打印,那么我們就可以看到CancellationException了
例子6,把job3的注釋放開:
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
val job3 = scope.launch {
println("job3 finished")
}
println("job delay")
delay(2000)
// Thread.sleep(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
結果:
job2 finished
job delay
caught: java.lang.IllegalArgumentException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test SupervisorHandler exception4$1$job1$1$job2$1.invokeSuspend(exceptionTest.kt:227)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finishedProcess finished with exit code 0
顯然有異常沒有被捕獲,很明顯這個異常是調用job3時輸出的,由此又可以推斷出,如果在等待任務結束時,任務出現異常并且手動捕獲異常后,再啟動子協程時,也會拋出異常,并且不可捕獲
注意:新版本kotlin已修復這個bug,不會拋出異常了
Android中全局異常的處理
最后,感謝動腦學院Jason老師出的kotlin協程教程,得到了很多理解和啟發
原文鏈接:https://www.jianshu.com/p/a933a2d980c7
相關推薦
- 2022-08-15 Python自制隨機數生成算法
- 2022-09-15 Python利用psutil實現獲取硬件,網絡和進程信息_python
- 2022-06-06 typescript使用class關鍵字定義一個類、static、readonly
- 2022-10-28 Django執行python?manage.py?makemigrations報錯的解決方案分享_p
- 2022-06-10 基于PyQt5制作一個群發郵件工具_python
- 2022-02-23 利用?trap?在?docker?容器優雅關閉前執行環境清理的方案_docker
- 2023-07-08 css如何給div添加一個條紋背景,在背景上畫一條有寬度的斜線
- 2022-10-08 ASP.NET泛型一之泛型簡介與基本語法_實用技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支