網(wǎng)站首頁 編程語言 正文
前言
自定義View實(shí)現(xiàn)的跑馬燈一直沒有實(shí)現(xiàn)類似 Android TextView 的跑馬燈首尾相接的效果,所以一直想看看Android TextView 的跑馬燈是如何實(shí)現(xiàn)
本文主要探秘 Android TextView 的跑馬燈實(shí)現(xiàn)原理及實(shí)現(xiàn)自下往上效果的跑馬燈
探秘
TextView#onDraw
原生 Android TextView 如何設(shè)置開啟跑馬燈效果,此處不再描述,View 的繪制都在 onDraw 方法中,這里直接查看 TextView#onDraw() 方法,刪減一些不關(guān)心的代碼
protected void onDraw(Canvas canvas) {
// 是否需要重啟啟動(dòng)跑馬燈
restartMarqueeIfNeeded();
?
// Draw the background for this view
super.onDraw(canvas);
// 刪減不關(guān)心的代碼
?
// 創(chuàng)建`mLayout`對(duì)象, 此處為`StaticLayout`
if (mLayout == null) {
assumeLayout();
}
?
Layout layout = mLayout;
?
canvas.save();
?
// 刪減不關(guān)心的代碼
?
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
?
// 判斷跑馬燈設(shè)置項(xiàng)是否正確
if (isMarqueeFadeEnabled()) {
if (!mSingleLine && getLineCount() == 1 && canMarquee()
&& (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
?
// 判斷跑馬燈是否啟動(dòng)
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
// 移動(dòng)畫布
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
}
?
final int cursorOffsetVertical = voffsetCursor - voffsetText;
?
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
// 繪制文本
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
?
// 判斷是否可以繪制尾部文本
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
final float dx = mMarquee.getGhostOffset();
// 移動(dòng)畫布
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
// 繪制尾部文本
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
?
canvas.restore();
}
Marquee
根據(jù) onDraw() 方法分析,跑馬燈效果的實(shí)現(xiàn)主要依賴 mMarquee 這個(gè)對(duì)象來實(shí)現(xiàn),好的,看下 Marquee 吧,Marquee 代碼較少,就貼上全部源碼吧
private static final class Marquee {
// TODO: Add an option to configure this
// 縮放相關(guān),不關(guān)心此字段
private static final float MARQUEE_DELTA_MAX = 0.07f;
// 跑馬燈跑完一次后多久開始下一次
private static final int MARQUEE_DELAY = 1200;
// 繪制一次跑多長(zhǎng)距離因子,此字段與速度相關(guān)
private static final int MARQUEE_DP_PER_SECOND = 30;
?
// 跑馬燈狀態(tài)常量
private static final byte MARQUEE_STOPPED = 0x0;
private static final byte MARQUEE_STARTING = 0x1;
private static final byte MARQUEE_RUNNING = 0x2;
?
// 對(duì)TextView進(jìn)行弱引用
private final WeakReference<TextView> mView;
// 幀率相關(guān)
private final Choreographer mChoreographer;
?
// 狀態(tài)
private byte mStatus = MARQUEE_STOPPED;
// 繪制一次跑多長(zhǎng)距離
private final float mPixelsPerMs;
// 最大滾動(dòng)距離
private float mMaxScroll;
// 是否可以繪制右陰影, 右側(cè)淡入淡出效果
private float mMaxFadeScroll;
// 尾部文本什么時(shí)候開始繪制
private float mGhostStart;
// 尾部文本繪制位置偏移量
private float mGhostOffset;
// 是否可以繪制左陰影,左側(cè)淡入淡出效果
private float mFadeStop;
// 重復(fù)限制
private int mRepeatLimit;
?
// 跑動(dòng)距離
private float mScroll;
// 最后一次跑動(dòng)時(shí)間,單位毫秒
private long mLastAnimationMs;
?
Marquee(TextView v) {
final float density = v.getContext().getResources().getDisplayMetrics().density;
// 計(jì)算每次跑多長(zhǎng)距離
mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
mView = new WeakReference<TextView>(v);
mChoreographer = Choreographer.getInstance();
}
?
// 幀率回調(diào),用于跑馬燈跑動(dòng)
private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
tick();
}
};
?
// 幀率回調(diào),用于跑馬燈開始跑動(dòng)
private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mStatus = MARQUEE_RUNNING;
mLastAnimationMs = mChoreographer.getFrameTime();
tick();
}
};
?
// 幀率回調(diào),用于跑馬燈重新跑動(dòng)
private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStatus == MARQUEE_RUNNING) {
if (mRepeatLimit >= 0) {
mRepeatLimit--;
}
start(mRepeatLimit);
}
}
};
?
// 跑馬燈跑動(dòng)實(shí)現(xiàn)
void tick() {
if (mStatus != MARQUEE_RUNNING) {
return;
}
?
mChoreographer.removeFrameCallback(mTickCallback);
?
final TextView textView = mView.get();
// 判斷TextView是否處于獲取焦點(diǎn)或選中狀態(tài)
if (textView != null && (textView.isFocused() || textView.isSelected())) {
// 獲取當(dāng)前時(shí)間
long currentMs = mChoreographer.getFrameTime();
// 計(jì)算當(dāng)前時(shí)間與上次時(shí)間的差值
long deltaMs = currentMs - mLastAnimationMs;
mLastAnimationMs = currentMs;
// 根據(jù)時(shí)間差計(jì)算本次跑動(dòng)的距離,減輕視覺上跳動(dòng)/卡頓
float deltaPx = deltaMs * mPixelsPerMs;
// 計(jì)算跑動(dòng)距離
mScroll += deltaPx;
// 判斷是否已經(jīng)跑完
if (mScroll > mMaxScroll) {
mScroll = mMaxScroll;
// 發(fā)送重新開始跑動(dòng)事件
mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
} else {
// 發(fā)送下一次跑動(dòng)事件
mChoreographer.postFrameCallback(mTickCallback);
}
// 調(diào)用此方法會(huì)觸發(fā)執(zhí)行`onDraw`方法
textView.invalidate();
}
}
?
// 停止跑馬燈
void stop() {
mStatus = MARQUEE_STOPPED;
mChoreographer.removeFrameCallback(mStartCallback);
mChoreographer.removeFrameCallback(mRestartCallback);
mChoreographer.removeFrameCallback(mTickCallback);
resetScroll();
}
?
private void resetScroll() {
mScroll = 0.0f;
final TextView textView = mView.get();
if (textView != null) textView.invalidate();
}
?
// 啟動(dòng)跑馬燈
void start(int repeatLimit) {
if (repeatLimit == 0) {
stop();
return;
}
mRepeatLimit = repeatLimit;
final TextView textView = mView.get();
if (textView != null && textView.mLayout != null) {
// 設(shè)置狀態(tài)為在跑
mStatus = MARQUEE_STARTING;
// 重置跑動(dòng)距離
mScroll = 0.0f;
// 計(jì)算TextView寬度
final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
- textView.getCompoundPaddingRight();
// 獲取文本第0行的寬度
final float lineWidth = textView.mLayout.getLineWidth(0);
// 取TextView寬度的三分之一
final float gap = textWidth / 3.0f;
// 計(jì)算什么時(shí)候可以開始繪制尾部文本:首部文本跑動(dòng)到哪里可以繪制尾部文本
mGhostStart = lineWidth - textWidth + gap;
// 計(jì)算最大滾動(dòng)距離:什么時(shí)候認(rèn)為跑完一次
mMaxScroll = mGhostStart + textWidth;
// 尾部文本繪制偏移量
mGhostOffset = lineWidth + gap;
// 跑動(dòng)到哪里時(shí)不繪制左側(cè)陰影
mFadeStop = lineWidth + textWidth / 6.0f;
// 跑動(dòng)到哪里時(shí)不繪制右側(cè)陰影
mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
?
textView.invalidate();
// 開始跑動(dòng)
mChoreographer.postFrameCallback(mStartCallback);
}
}
?
// 獲取尾部文本繪制位置偏移量
float getGhostOffset() {
return mGhostOffset;
}
?
// 獲取當(dāng)前滾動(dòng)距離
float getScroll() {
return mScroll;
}
?
// 獲取可以右側(cè)陰影繪制的最大距離
float getMaxFadeScroll() {
return mMaxFadeScroll;
}
?
// 判斷是否可以繪制左側(cè)陰影
boolean shouldDrawLeftFade() {
return mScroll <= mFadeStop;
}
?
// 判斷是否可以繪制尾部文本
boolean shouldDrawGhost() {
return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
}
?
// 跑馬燈是否在跑
boolean isRunning() {
return mStatus == MARQUEE_RUNNING;
}
?
// 跑馬燈是否不跑
boolean isStopped() {
return mStatus == MARQUEE_STOPPED;
}
}
好的,分析完 Marquee,跑馬燈實(shí)現(xiàn)原理豁然明亮
- 在 TextView 開啟跑馬燈效果時(shí)調(diào)用 Marquee#start() 方法
- 在 Marquee#start() 方法中觸發(fā) TextView 重繪,開始計(jì)算跑動(dòng)距離
- 在 TextView#onDraw() 方法中根據(jù)跑動(dòng)距離移動(dòng)畫布并繪制首部文本,再根據(jù)跑動(dòng)距離判斷是否可以移動(dòng)畫布繪制尾部文本
小結(jié)
TextView 通過移動(dòng)畫布繪制兩次文本實(shí)現(xiàn)跑馬燈效果,根據(jù)兩幀繪制的時(shí)間差計(jì)算跑動(dòng)距離,怎一個(gè)"妙"字了得
應(yīng)用
上面分析完原生 Android TextView 跑馬燈的實(shí)現(xiàn)原理,但是原生 Android TextView 跑馬燈有幾點(diǎn)不足:
- 無法設(shè)置跑動(dòng)速度
- 無法設(shè)置重跑間隔時(shí)長(zhǎng)
- 無法實(shí)現(xiàn)上下跑動(dòng)
以上第1、2點(diǎn)在上面 Marquee 分析中已經(jīng)有解決方案,接下來根據(jù)原生實(shí)現(xiàn)原理實(shí)現(xiàn)第3點(diǎn)上下跑動(dòng)
MarqueeTextView
這里給出實(shí)現(xiàn)方案,列出主要實(shí)現(xiàn)邏輯,繼承 AppCompatTextView,復(fù)寫 onDraw() 方法,上下跑動(dòng)主要是計(jì)算上下跑動(dòng)的距離,然后再次重繪 TextView 上下移動(dòng)畫布繪制文本
/**
* 繼承AppCompatTextView,復(fù)寫onDraw方法
*/
public class MarqueeTextView extends AppCompatTextView {
?
private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFEFEFEF");
?
@IntDef({HORIZONTAL, VERTICAL})
@Retention(RetentionPolicy.SOURCE)
public @interface OrientationMode {
}
?
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
?
private Marquee mMarquee;
private boolean mRestartMarquee;
private boolean isMarquee;
?
private int mOrientation;
?
public MarqueeTextView(@NonNull Context context) {
this(context, null);
}
?
public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
?
public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
?
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MarqueeTextView, defStyleAttr, 0);
?
mOrientation = ta.getInt(R.styleable.MarqueeTextView_orientation, HORIZONTAL);
?
ta.recycle();
}
?
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
?
if (mOrientation == HORIZONTAL) {
if (getWidth() > 0) {
mRestartMarquee = true;
}
} else {
if (getHeight() > 0) {
mRestartMarquee = true;
}
}
}
?
private void restartMarqueeIfNeeded() {
if (mRestartMarquee) {
mRestartMarquee = false;
startMarquee();
}
}
?
public void setMarquee(boolean marquee) {
boolean wasStart = isMarquee();
?
isMarquee = marquee;
?
if (wasStart != marquee) {
if (marquee) {
startMarquee();
} else {
stopMarquee();
}
}
}
?
public void setOrientation(@OrientationMode int orientation) {
mOrientation = orientation;
}
?
public int getOrientation() {
return mOrientation;
}
?
public boolean isMarquee() {
return isMarquee;
}
?
private void stopMarquee() {
if (mOrientation == HORIZONTAL) {
setHorizontalFadingEdgeEnabled(false);
} else {
setVerticalFadingEdgeEnabled(false);
}
?
requestLayout();
invalidate();
?
if (mMarquee != null && !mMarquee.isStopped()) {
mMarquee.stop();
}
}
?
private void startMarquee() {
if (canMarquee()) {
?
if (mOrientation == HORIZONTAL) {
setHorizontalFadingEdgeEnabled(true);
} else {
setVerticalFadingEdgeEnabled(true);
}
?
if (mMarquee == null) mMarquee = new Marquee(this);
mMarquee.start(-1);
}
}
?
private boolean canMarquee() {
if (mOrientation == HORIZONTAL) {
int viewWidth = getWidth() - getCompoundPaddingLeft() -
getCompoundPaddingRight();
float lineWidth = getLayout().getLineWidth(0);
return (mMarquee == null || mMarquee.isStopped())
&& (isFocused() || isSelected() || isMarquee())
&& viewWidth > 0
&& lineWidth > viewWidth;
} else {
int viewHeight = getHeight() - getCompoundPaddingTop() -
getCompoundPaddingBottom();
float textHeight = getLayout().getHeight();
return (mMarquee == null || mMarquee.isStopped())
&& (isFocused() || isSelected() || isMarquee())
&& viewHeight > 0
&& textHeight > viewHeight;
}
}
?
/**
* 仿照TextView#onDraw()方法
*/
@Override
protected void onDraw(Canvas canvas) {
restartMarqueeIfNeeded();
?
super.onDraw(canvas);
?
// 再次繪制背景色,覆蓋下面由TextView繪制的文本,視情況可以不調(diào)用`super.onDraw(canvas);`
// 如果沒有背景色則使用默認(rèn)顏色
Drawable background = getBackground();
if (background != null) {
background.draw(canvas);
} else {
canvas.drawColor(DEFAULT_BG_COLOR);
}
?
canvas.save();
?
canvas.translate(0, 0);
?
// 實(shí)現(xiàn)左右跑馬燈
if (mOrientation == HORIZONTAL) {
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
canvas.translate(dx, 0.0F);
}
?
getLayout().draw(canvas, null, null, 0);
?
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
final float dx = mMarquee.getGhostOffset();
canvas.translate(dx, 0.0F);
getLayout().draw(canvas, null, null, 0);
}
} else {
// 實(shí)現(xiàn)上下跑馬燈
if (mMarquee != null && mMarquee.isRunning()) {
final float dy = -mMarquee.getScroll();
canvas.translate(0.0F, dy);
}
?
getLayout().draw(canvas, null, null, 0);
?
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
final float dy = mMarquee.getGhostOffset();
canvas.translate(0.0F, dy);
getLayout().draw(canvas, null, null, 0);
}
}
?
canvas.restore();
}
}
Marquee
private static final class Marquee {
// 修改此字段設(shè)置重跑時(shí)間間隔 - 對(duì)應(yīng)不足點(diǎn)2
private static final int MARQUEE_DELAY = 1200;
?
// 修改此字段設(shè)置跑動(dòng)速度 - 對(duì)應(yīng)不足點(diǎn)1
private static final int MARQUEE_DP_PER_SECOND = 30;
?
private static final byte MARQUEE_STOPPED = 0x0;
private static final byte MARQUEE_STARTING = 0x1;
private static final byte MARQUEE_RUNNING = 0x2;
?
private static final String METHOD_GET_FRAME_TIME = "getFrameTime";
?
private final WeakReference<MarqueeTextView> mView;
private final Choreographer mChoreographer;
?
private byte mStatus = MARQUEE_STOPPED;
private final float mPixelsPerSecond;
private float mMaxScroll;
private float mMaxFadeScroll;
private float mGhostStart;
private float mGhostOffset;
private float mFadeStop;
private int mRepeatLimit;
?
private float mScroll;
private long mLastAnimationMs;
?
Marquee(MarqueeTextView v) {
final float density = v.getContext().getResources().getDisplayMetrics().density;
mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
mView = new WeakReference<>(v);
mChoreographer = Choreographer.getInstance();
}
?
private final Choreographer.FrameCallback mTickCallback = frameTimeNanos -> tick();
?
private final Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mStatus = MARQUEE_RUNNING;
mLastAnimationMs = getFrameTime();
tick();
}
};
?
/**
* `getFrameTime`是隱藏api,此處使用反射調(diào)用,高系統(tǒng)版本可能失效,可使用某些方案繞過此限制
*/
@SuppressLint("PrivateApi")
private long getFrameTime() {
try {
Class<? extends Choreographer> clz = mChoreographer.getClass();
Method getFrameTime = clz.getDeclaredMethod(METHOD_GET_FRAME_TIME);
getFrameTime.setAccessible(true);
return (long) getFrameTime.invoke(mChoreographer);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
?
private final Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStatus == MARQUEE_RUNNING) {
if (mRepeatLimit >= 0) {
mRepeatLimit--;
}
start(mRepeatLimit);
}
}
};
?
void tick() {
if (mStatus != MARQUEE_RUNNING) {
return;
}
?
mChoreographer.removeFrameCallback(mTickCallback);
?
final MarqueeTextView textView = mView.get();
if (textView != null && (textView.isFocused() || textView.isSelected() || textView.isMarquee())) {
long currentMs = getFrameTime();
long deltaMs = currentMs - mLastAnimationMs;
mLastAnimationMs = currentMs;
float deltaPx = deltaMs / 1000F * mPixelsPerSecond;
mScroll += deltaPx;
if (mScroll > mMaxScroll) {
mScroll = mMaxScroll;
mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
} else {
mChoreographer.postFrameCallback(mTickCallback);
}
textView.invalidate();
}
}
?
void stop() {
mStatus = MARQUEE_STOPPED;
mChoreographer.removeFrameCallback(mStartCallback);
mChoreographer.removeFrameCallback(mRestartCallback);
mChoreographer.removeFrameCallback(mTickCallback);
resetScroll();
}
?
private void resetScroll() {
mScroll = 0.0F;
final MarqueeTextView textView = mView.get();
if (textView != null) textView.invalidate();
}
?
void start(int repeatLimit) {
if (repeatLimit == 0) {
stop();
return;
}
mRepeatLimit = repeatLimit;
final MarqueeTextView textView = mView.get();
if (textView != null && textView.getLayout() != null) {
mStatus = MARQUEE_STARTING;
mScroll = 0.0F;
?
// 分別計(jì)算左右和上下跑動(dòng)所需的數(shù)據(jù)
if (textView.getOrientation() == HORIZONTAL) {
int viewWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
textView.getCompoundPaddingRight();
float lineWidth = textView.getLayout().getLineWidth(0);
float gap = viewWidth / 3.0F;
mGhostStart = lineWidth - viewWidth + gap;
mMaxScroll = mGhostStart + viewWidth;
mGhostOffset = lineWidth + gap;
mFadeStop = lineWidth + viewWidth / 6.0F;
mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
} else {
int viewHeight = textView.getHeight() - textView.getCompoundPaddingTop() -
textView.getCompoundPaddingBottom();
float textHeight = textView.getLayout().getHeight();
float gap = viewHeight / 3.0F;
mGhostStart = textHeight - viewHeight + gap;
mMaxScroll = mGhostStart + viewHeight;
mGhostOffset = textHeight + gap;
mFadeStop = textHeight + viewHeight / 6.0F;
mMaxFadeScroll = mGhostStart + textHeight + textHeight;
}
?
textView.invalidate();
mChoreographer.postFrameCallback(mStartCallback);
}
}
?
float getGhostOffset() {
return mGhostOffset;
}
?
float getScroll() {
return mScroll;
}
?
float getMaxFadeScroll() {
return mMaxFadeScroll;
}
?
boolean shouldDrawLeftFade() {
return mScroll <= mFadeStop;
}
?
boolean shouldDrawTopFade() {
return mScroll <= mFadeStop;
}
?
boolean shouldDrawGhost() {
return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
}
?
boolean isRunning() {
return mStatus == MARQUEE_RUNNING;
}
?
boolean isStopped() {
return mStatus == MARQUEE_STOPPED;
}
}
效果
總結(jié)
原文鏈接:https://juejin.cn/post/7094205592402657310
相關(guān)推薦
- 2023-07-24 前端常見狀態(tài)碼
- 2022-01-16 jQuery實(shí)現(xiàn)動(dòng)畫效果和導(dǎo)航欄動(dòng)態(tài)顯示
- 2023-03-23 Pandas分組聚合之groupby()、agg()方法的使用教程_python
- 2022-04-11 C++17之std::visit的具體使用_C 語言
- 2023-01-20 Python中數(shù)組切片的用法實(shí)例詳解_python
- 2022-12-12 Flutter手機(jī)權(quán)限檢查與申請(qǐng)實(shí)現(xiàn)方法詳解_Android
- 2022-05-10 ioc基于注解方式
- 2022-01-06 react實(shí)現(xiàn)todolist的增刪改查
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支