網站首頁 編程語言 正文
目的
網上已經有很多關于事件分發的優秀文章,為何我還要自己寫?因為別人總結的畢竟都是別人的,自己親自閱讀源碼不僅會讓自己更懂得原理,也會讓自己記得更清楚,而且也會發現另一番天地。
View處理事件的關鍵
由于所以的控件都直接或者間接繼承自View,因此View的事件分發機制就是最基礎的一環,需要首先掌握其原理。
那么View的事件從哪里來的呢?當然是父View(一個ViewGroup)。父View在尋找能處理事件的子View的時候,會調用子View的dispatchTouchEvent()
把事件傳遞給子View,如果子View的dispatchTouchEvent()
返回true
,代表子View處理了該事件,如果返回flase
就代表該子View不處理事件。如果所有子View都不處理該事件,那么就由父View自己處理。
今天我們這篇文章就是來分析View如何處理事件。我們重點關心View.dispatchTouchEvent()
啥時候返回true(代表處理了事件),啥時候返回false(代表不處理事件)。
View事件處理分析
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; final int actionMasked = event.getActionMasked(); // 當窗口被遮擋,是否過濾掉這個觸摸事件 if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; // 1. 外部監聽器處理 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 2. 自己處理 if (!result && onTouchEvent(event)) { result = true; } } return result; }
可以看到View.dispatchTouchEvent()
處理事件非常簡單,要么交給外部處理,要么自己來處理。只要任何一方處理了,也就相應的處理函數返回true
,View.dispatchTouchEvent()
就返回true
,代表View處理了事件。否則,View.dispatchTouchEvent()
返回false
,也就是View不處理該事件。
首先它把事件交給外部進行處理。外部處理指的什么呢?它指的就是交給setOnTouchListener()
設置的監聽器來處理。如果這個監聽器處理時返回true
,也就是OnTouchListener.onTouch()
方法返回true
,View.dispatchTouchEvent()
就返回true
,也就說明View處理了該事件。否則交給自己來處理,也就是交由onTouchEvent()
處理。
當然,如果要讓事件監聽器來處理,還必須要讓View處于enabled
狀態。可以通過setEnabled()
方法來改變View的enabled
狀態。并且可以通過isEnabled()
查詢View是否處于enabled
狀態。
當外部無法處理時,也就是上面的三個條件有一個不滿足時,就交給View.onTouchEvent()
來處理。此時View.onTouchEvent()
的返回值就決定了View.dispatchTouchEvent()
的返回值。也就是決定了View是否處理該事件。那么,我們來看下View.onTouchEvent()
什么時候返回true
,什么時候返回false
。
public boolean onTouchEvent(MotionEvent event) { // 判斷View是否可點擊(點擊/長按) final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 處理View是disabled狀態的情況 if ((viewFlags & ENABLED_MASK) == DISABLED) { // ... return clickable; } // 如果有處理代表,就先交給它處理。如果它不處理,就繼續交給自己處理 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 如果可以點擊,最后會返回true,代表處理了View處理了事件 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // ... return true; } return false; }
這太讓人意外了,只要View可以點擊(點擊/長按),就返回true
,否則返回false
。
忽略觸摸代理(Touch Delegate
)和CONTEXT_CLICKABLE
的特性,因為不常用,如果遇到了,可以再來查看。
那么,View默認可以點擊,長按嗎?當然是不能。這需要子View自己去設置,例如Button在構造函數中就設置了自己可以點擊。
我們從代碼角度解釋下View默認是否可以點擊和長按
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { // View構造函數中解析android:clickable屬性 case com.android.internal.R.styleable.View_clickable: // 第二個參數false,表示View默認不可點擊 if (a.getBoolean(attr, false)) { viewFlagValues |= CLICKABLE; viewFlagMasks |= CLICKABLE; } break; // View構造函數中解析android:longClickable屬性 case com.android.internal.R.styleable.View_longClickable: // 第二個參數false,表示View默認不可長按 if (a.getBoolean(attr, false)) { viewFlagValues |= LONG_CLICKABLE; viewFlagMasks |= LONG_CLICKABLE; } break; }
在View的構造函數中分別接下了android:clickable
和android:longClickable
屬性,從默認值可以看出,View默認是不可點擊和長按的。也就是說View默認不處理任何事件。
那么,我們用一張圖來解釋View如何處理觸摸事件的
通過這張圖,我們就可以清楚的了解到View.dispatchTouchEvent()
在什么情況下返回 true
,在什么情況下返回false
。也就了解了View在什么情況下處理了事件,在什么情況下不處理事件。
View.onTouchEvent()分析
View事件處理就這么簡單嗎?如果你只關心事件分發到哪里,以及誰處理了事件,那么掌握上面的流程就夠了。
但是你是否還有個疑問,View.onTouchEvent()
在干啥呢?OK,如果你保持這份好奇心,那么接著往下看。
View.onTouchEvent()
其實處理了三種情況
- 處理點擊事件
- 處理長按事件
- 處理View狀態改變
- 處理tap事件
處理長按事件
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; // 1. 判斷是否在一個滾動的容器中 boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { // 1.1 在滾動容器中 // ... } else { // 1.2 不是在滾動容器中 // 設置按下狀態 setPressed(true, x, y); // 檢測長按動作 checkForLongClick(0, x, y); } break;
setPressed()
方法首先會設置View為按下狀態, 代碼如下
mPrivateFlags |= PFLAG_PRESSED;
然后,通過checkForLongClick()
來檢測長按動作,這是如何實現呢
private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } // 在長按超時的時間點,執行一個Runable,也就是CheckForLongPres postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } } private final class CheckForLongPress implements Runnable { @Override public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { // 執行長按動作 if (performLongClick(mX, mY)) { // 如果處理了長按動作,mHasPerformedLongPress為true mHasPerformedLongPress = true; } } } }
其實它是把CheckForLongPress
這個Runnable
加入到Message Queue
中,然后在ViewConfiguration.getLongPressTimeout()
這個長按超時的時間點執行。
這是什么意思呢?首先在ACTION_DOWN
的時候我檢測到按下的動作,那么在還沒有執行ACTION_UP
之前,如果按下動作超時了,也就是超過了長按的時間點,那么我會執行長按動作performLongClick()
。我們現在看下執行長按做了哪些事情
public boolean performLongClick(float x, float y) { // 記錄長按的位置 mLongClickX = x; mLongClickY = y; // 執行長按的動作 final boolean handled = performLongClick(); // 重置數據 mLongClickX = Float.NaN; mLongClickY = Float.NaN; return handled; } public boolean performLongClick() { return performLongClickInternal(mLongClickX, mLongClickY); } private boolean performLongClickInternal(float x, float y) { boolean handled = false; final ListenerInfo li = mListenerInfo; // 1. 執行長按監聽器處理動作 if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } // 2. 如果長按監聽器不處理,就顯示上下文菜單 if (!handled) { final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); } // 3. 如果處理了長按事件,就執行觸摸反饋 if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
有三個動作在長按的時候執行
- 如果調用過
setOnLongClickListener()
給View設置長按事件監聽器,那么首先把長按事件交給這個監聽器處理。如果這個監聽器返回true
,代表監聽器已經處理了長按事件,那么直接執行第三步的觸摸反饋,并返回。如果這個監聽器返回了false
,代表監聽沒有處理長按事件,那么就執行第二步,交給系統處理。 - 當第一步處理不了時,系統自己來處理,它會顯示一個上下文菜單。
- 執行觸摸反饋。
如果你不了解什么是上下文菜單(Context Menu
)和觸摸反饋(Haptic Feednack
),可以自行搜索下。
我們已經了解了如果觸發長按做了哪些動作,但是我們也要記得觸發長按的時機,那就是從手指按下到抬起的時間要超過長按的超時時間。如果沒有超過這個長按超時時間,在ACTION_UP
的時候,系統會怎么做呢?
case MotionEvent.ACTION_UP: // 處于按下狀態 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 沒有執行長按動作 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除長按動作 removeLongPressCallback(); } } break;
當檢測到ACTION_UP
時,如果見到了View處于按下狀態,但是還沒有執行長按動作。也就是說,還沒有達到長按的時間點,手指就抬起了,那么系統就會移除在ACTION_DOWN
添加的長按動作,之后長按動作就不會觸發了。
處理點擊事件
我們先分析了長按事件而沒有分析點擊事件,其實是為了更好的講清楚點擊事件,看代碼
case MotionEvent.ACTION_DOWN: if (isInScrollingContainer) { } else { // 設置按下狀態 setPressed(true, x, y); } break;
當檢測到ACTION_DOWN
事件,首先的給它設置一個按下標記,這個前面說過。然后在沒有達到長按超時這個時間點前,如果檢測到ACTION_UP
事件,那么我們就可以認為這是一次點擊事件
case MotionEvent.ACTION_UP: // 處于按下狀態 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 沒有執行長按 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除長按動作 removeLongPressCallback(); // focusTaken是在touch mode下有效,現在討論的是簡單的手指觸摸 if (!focusTaken) { // 創建點擊事件 if (mPerformClick == null) { mPerformClick = new PerformClick(); } // 通過Message Queue執行點擊事件 if (!post(mPerformClick)) { performClick(); } } } } break;
Touch Mode
模式與D-pad
有關,讀者可以查閱官方文檔說明。
有了前面關于長按事件的知識,這里就非常好理解了。
如果沒有執行長按動作,就先移除長按回調,那么以后就不會再執行長按動作了。相反,如果已經執行長按動作,那么就不會執行點擊事件。
performClick()
用來執行點擊事件,那么來看下它做了什么
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { // 1. 首先交給外部的點擊監聽器處理 playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { // 2. 如果沒有外部監聽器,就不處理了 result = false; } return result; }
點擊事件的處理非常簡單粗暴,默認就不處理,也就是返回false
。當然,如果你想處理,調用setOnClickListener()
即可。
###處理View狀態改變
響應View狀態改變的操作都集中在setPressed()
方法中,其實我們再進一步思考下,View只對按下和抬起的狀態進行響應
public void setPressed(boolean pressed) { // 1. 判斷是否需要執行刷新動作 final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED); // 2. 設置狀態 if (pressed) { // 設置按下狀態 mPrivateFlags |= PFLAG_PRESSED; } else { // 取消按下狀態 mPrivateFlags &= ~PFLAG_PRESSED; } // 3. 如果需要刷新就刷新View管理的Drawable狀態 if (needsRefresh) { refreshDrawableState(); } // 4. 如果是ViewGroup,就需要把這個狀態分發給子View dispatchSetPressed(pressed); }
如果手指按下了,會調用setPressed(true)
,如果手指抬起了,會調用setPressed(false)
。
假設我們手指剛按下,那么就需要執行第三步的刷新Drawable狀態的動作
public void refreshDrawableState() { // 標記Drawable狀態需要刷新 mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY; // 執行Drawable狀態改變的動作 drawableStateChanged(); // 通知父View,子View的Drawable狀態改變了 ViewParent parent = mParent; if (parent != null) { parent.childDrawableStateChanged(this); } }
首先設置PFLAG_DRAWABLE_STATE_DIRTY
標記,表示Drawable狀態需要更新,然后調用drawableStateChange()
來執行Drawable狀態改變動作
@CallSuper protected void drawableStateChanged() { // 1. 獲取Drawable新狀態 final int[] state = getDrawableState(); boolean changed = false; // 2. 為View管理的各種Drawable設置新狀態 final Drawable bg = mBackground; if (bg != null && bg.isStateful()) { changed |= bg.setState(state); } final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (fg != null && fg.isStateful()) { changed |= fg.setState(state); } if (mScrollCache != null) { final Drawable scrollBar = mScrollCache.scrollBar; if (scrollBar != null && scrollBar.isStateful()) { changed |= scrollBar.setState(state) && mScrollCache.state != ScrollabilityCache.OFF; } } // 3. 為StateListAnimator設置新狀態,從而改變Drawable if (mStateListAnimator != null) { mStateListAnimator.setState(state); } // 4. 如果有Drawable狀態更新了,就重繪 if (changed) { invalidate(); } }
既然我們要給Drawable更新狀態,那么就的獲取新的狀態值,這就是第一步所做的事情,我們來看下getDrawableState()
如何獲取新狀態的
public final int[] getDrawableState() { // 如果Drawable狀態沒有改變,就直接返回之前的狀態值 if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) { return mDrawableState; } // 如果狀態值不存在,或者Drawable狀態需要更新 else { // 創建狀態值 mDrawableState = onCreateDrawableState(0); // 重置PFLAG_DRAWABLE_STATE_DIRTY狀態 mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY; return mDrawableState; } }
剛剛,我們設置了PFLAG_DRAWABLE_STATE_DIRTY
,標志著Drawable狀態需要更新,因此這里會調用onCreateDrawableState()
來獲取
protected int[] onCreateDrawableState(int extraSpace) { // 默認是沒有設置DUPLICATE_PARENT_STATE狀態 if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE && mParent instanceof View) { return ((View) mParent).onCreateDrawableState(extraSpace); } int[] drawableState; // 2. 根據各種flag, 獲取狀態 int privateFlags = mPrivateFlags; int viewStateIndex = 0; if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED; if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED; if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED; if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED; if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED; if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED; if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested && ThreadedRenderer.isAvailable()) { // This is set if HW acceleration is requested, even if the current // process doesn't allow it. This is just to allow app preview // windows to better match their app. viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED; } if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED; final int privateFlags2 = mPrivateFlags2; if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) { viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT; } if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) { viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED; } // 2. 把狀態值轉化為一個數組 drawableState = StateSet.get(viewStateIndex); if (extraSpace == 0) { return drawableState; } }
首先根據各種標志位,例如mPrivateFlags
和mPrivateFlags2
,來獲取狀態的值,然后根據狀態的值獲取一個狀態的數組。
我想你一定想直到這個狀態數組是咋樣的,我舉個例子,View默認是enabled
狀態,那么mViewFlags
默認設置了ENABLED
標記,當我們手指按下的時候,mPrivateFlags
設置了PFLAG_PRESSED
按下狀態標記。如果值選擇這兩個情況來獲取狀態值,那么viewStateIndex = VIEW_STATE_PRESSED | VIEW_STATE_ENABLED
,用二進制表示就是11000
。然后通過StateSet.get(viewStateIndex)
轉化為數組就是[StateSet.VIEW_STATE_ENABLED, StateSet.VIEW_STATE_PRESSED]
。
現在,我們獲取到Drawable新的狀態值,那么就可以進行drawableStateChanged()
函數的第二步,為各種Drawable設置新的狀態值,例如背景Drawable,前景Drawable。這些Drawable根據這些新的狀態值,自己判斷是否需要更新Drawable,例如更新顯示的大小,顏色等等。如果更新了Drawable,那么就會返回true
,否則返回false
。
drawableStateChanged()
函數的第三步,還針對了StateListAnimator
的處理。StateListAnimator
會根據View狀態值,改變Drawable的顯示。
如果大家不了解StateListAnimator
,可以網上查閱下它的使用,這樣就可以對View
狀態改變有更深層次的理解。
drawableStateChanged()
函數的第四步,如果有任意Drawable改變了狀態,那么就需要View進行重繪。
處理tap事件
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; // 1. 判斷View是否在滾動容器中 boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { // 標記要觸發tab事件 mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); // 2. 如果View在滾動容器中,那么檢測一個tab動作 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { } break;
第一步,判斷View是否在一個滾動的容器中
public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; }
通過循環遍歷父View,并調用父View的shouldDelayChildPressedState()
方法來判斷父View是否是一個滾動容器。
那么什么樣的ViewGroup
是滾動容器呢?例如ScrollView
就是一個滾動容器,因為它有讓子View滾動的特性,所以shouldDelayChildPressedState()
返回true
。而LinearLayout
就不是一個滾動容器,它本身沒有設計滾動特性,因此shouldDelayChildPressedState()
返回false
。
當View處于一個滾動容器中,并且容器處于滾動中,這個View需要檢測一個tap
事件,也就是表示快速點擊。它有個觸發的超時時間,大小為100ms(長按的觸發超時時間是500ms),因此只要按下的事件超過100ms, 都算作一次tap
事件。那么,我們先來看下觸發tap
事件都做了啥事
private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { // 先取消tab的標記 mPrivateFlags &= ~PFLAG_PREPRESSED; // 設置按下狀態 setPressed(true, x, y); // 檢測長按事件 checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } }
當觸發了tap
事件,首先取消標記,表示tap
事件已經執行。然后,既然已經發生了點擊事件,那么自然要設置按下狀態。最后由于tap
事件是在長按事件之前觸發,那么當tap
事件觸發后,自然要去檢測長按事件是否觸發。
我們剛剛說到,tap
事件觸發的條件是,在滾動容器中,從手指按下到抬起的時間要過100ms。那么如果在100ms之前抬起了手指,那么會怎么處理呢,我們來看下ACTION_UP
的處理邏輯
case MotionEvent.ACTION_UP: // 判斷tap動作是否已經完成 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; // 如果是按下狀態或者還沒有觸發tap動作 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 1. 如果還沒有觸發tap動作,就設置按下狀態 if (prepressed) { setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除長按回調 removeLongPressCallback(); // 2. 執行點擊事件 if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } // 3. 移除tap回調 removeTapCallback(); } break;
prepressed
為true
表示沒有執行tap
事件,那么當檢測到手指抬起時,先設置按下狀態。如果連tap
都沒執行,肯定也不會執行長按事件,因此只會執行點擊事件。最后,移除長按回調,這樣tap
事件就不會再觸發。
如果tap
事件執行了呢?只有一點差別,將會在第二步,根據是否執行了長按來決定是否執行點擊事件。
總結
通過本文的分析,我們可以清楚的知道View如何處理父View傳遞過來的事件,也可以清楚知道View在什么時候處理事件,什么時候不處理事件。
另外,本文也對View.onTouchEvent()
作出分析,我們可以清楚知道View如何處理點擊事件,如何處理長按事件,如何處理狀態改變,以及如何處理tap
事件。
原文鏈接:https://juejin.cn/post/6844903919370371079
相關推薦
- 2022-04-16 python修改全局變量可以不加global嗎?_python
- 2022-06-18 Qt?事件處理機制的深入理解_C 語言
- 2022-08-02 Redis實現登錄注冊的示例代碼_Redis
- 2022-05-03 python中的map函數語法詳解_python
- 2022-08-03 利用Python連接Oracle數據庫的基本操作指南_python
- 2022-07-19 使用普通指針實現數組倒敘和字符串的壓縮
- 2022-10-25 maven方式創建spring項目-eclipse篇
- 2022-05-24 flutter實現頭部tabTop滾動欄_Android
- 最近更新
-
- 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同步修改后的遠程分支