开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

断更19个月,携 Threejs Shader 归来!(上

发表于 2023-04-16

断更19个月之久😢

好久没更新文章了,除去2022年1月30日发布的「免费领取古柳定制的红包封面啦!1万份用不完 - 牛衣古柳」,上一次更新得追溯到2021年9月12日写的「手把手带你上手D3.js数据可视化系列(四) - 牛衣古柳」。

一眨眼已过去580天,19个月,1.59年。断更之久,久到离谱,比当初开始更新 D3.js 可视化等内容之前的断更14个月有过之而不及,「年更博主冒个泡,或将开启可视化之旅 - 牛衣古柳 - 20200827」。

其实古柳中间有很多次想更新,但一时断更一时爽,一直断更一直爽。没有写文章的习惯后,想再捡起来还挺难的。

其实一直有在分享

当然古柳在可视化交流群里和朋友圈,还是分享过不少内容,甚至在卡塔尔世界杯前开了多年来首场直播——分享了 Canvas 及自动配色等内容。

虽然因为声音没测试好不太清晰,所以最终录播没放出去。

不过后续直播等最新动态还是在群里或朋友圈发布,欢迎👏来围观。

此外古柳将所学的知识活学活用做了些效果发到视频号,毕竟比写文章省力多、压力小很多。后续会讲解如何实现,也欢迎大家关注此账号。

重新更新的契机

这次终于久违地更新,是想到离古柳去年4月接触 Three.js、接触 Shader,并打开新世界的大门过去快一年。

不能免俗地觉得一年之际这个契机实在很好,适合重新开始更新文章,和大家简单分享下这段经历,也顺带为后续写 Shader 教程铺个路。(等 Shader 教程的更新步入正轨,会继续更新 D3.js 教程)

说起来,虽然古柳之前做的中国传统颜色可视化也是3D的,但其实对于3D的东西一直望而却步,不太敢碰 Three.js、图形学等3D的技术,总觉得2维里画个圆圈用 circle、画个矩形用 rect 多简单,而3维里还有相机、材质、灯光、几何体等概念,好麻烦。

自己吓自己,于是一直迂回、用些非主流的方式。比如最初实现的版本用了 Zdog、后来开源的版本用了 Sprite.js+D3.js……为了不学 Three.js 简直挖空心思。

链接:DesertsX/dataviz-in-action - GitHub

链接:演示效果

畏难的心使人徘徊不前,幸而终于获得更多的动力去克服心魔。

早先看群友分享过 the.data.guy 这个 tiktok 账号发的挺火🔥的 3D+AR/VR 的可视化视频,当时古柳就觉得超酷,于是2022年4月重新找出来并分享到群里给大家看看。

链接:flowimmersive、flowimmersive - tiktok

终于学起 Three.js

看到这样的作品古柳就很好奇如何实现的、自己是否有机会复现出来?再加上些其他缘故,带着被撩拨起来的兴致,终于在去年4月某日一时兴起开始入坑 Three.js。

先是翻了下张雯莉/羡辙的《Three.js入门指南》,虽然这书很旧、是2013年快十年前的,但不求甚解地快速过一遍,对3D的东西有初步印象便觉得足够了。

然后看了b站up主进华4月份刚发布的「three.js教程-从入门到入门」,讲的简洁清晰、很是受用。非常推荐没学过 Three.js 的朋友看看。

这个教程难能可贵的一点是,最后对照《Three.js开发指南/Learn Three.js》讲了下还有哪些内容没涉及,给大家后续学习指引了方向。

前不久也看到推特上一些人在晒最新的第四版。

后来群友问起这书,古柳去 zlibrary 上搜了下,发现第四版电子书也有了,大家可自行下载看看,也可以在「牛衣古柳」公众号后台回复 learn three.js 获取 PDF 版本。

宝藏 Yuri

因为没学过的内容里对粒子系统较感兴趣,于是上油管搜索 particle system three.js 打算学习下,然后偶然发现了 Yuri Artiukh 这个宝藏,这位乌克兰🇺🇦老哥的频道发了超多(上百期)用 Three.js/Shader/Canvas/Pixi.js 等技术复现各种网站效果或动画效果的教程。至今超喜欢 Yuri 的教程!

尤其是这期的 Pepyaka 效果,放在网页里太丝滑太酷,一眼爱上的入坑之作,从没想到能有机会学到这么酷的效果。如果大家也想学可以去试试看能不能看懂,后续古柳也会写教程教大家,见 #s3e6 ALL YOUR HTML, Making Pepyaka with Three.js - Yuri Artiukh - 20191201

友情提醒:目前原网站 Grant Yi 的主页 换成了其他 Shader 实现的效果,以免大家去原网站欣赏体验时扑空。不过新效果依旧是很酷,值得看源码学习!Mark 住。

扯回来,这是古柳第一次知道 Shader 的概念、第一次对 Three.js Shader 所能实现的效果有了直观的认知、第一次燃起空前的学习热情,也自此打开新世界的大门。

在此之前也曾见过一些网站,当时就觉得其中有些效果不是常规的 HTML/CSS/JS 所能实现,但到底怎么实现的却又不得而知,此刻才有种“众里寻她千百度”,终于找到答案的感觉。

Ying He

比如在此之前,古柳重新想到清华美院向帆老师的春晚颜色可视化/蚊香图这个作品(见 「李子柒130个视频1万图片5万颜色数据可视化的背后,是古柳三年的念念不忘 - 牛衣古柳 - 20201128」),然后突发奇想地想搜搜这几个作者,看看有没有个人网站可以顺藤摸瓜追踪些新的资讯。

于是找到了 Ying He 的个人网站,并发现背景图片有类似水纹的效果(不是视频),就不是常规技术所能实现的,而当时就不知道到底怎么实现的。

后来有群友看到「伴随 P5.js 入坑创意编程 - 牛衣古柳 - 20190628」这篇文章,知道我也晓得 AwardPuzzel——全国美展油画类获奖画作的数据视觉化作品——就说到他认识作者,一问才知道就是何老师,她在国内某厂做设计专家,而他刚好在她的团队实习,也是无巧不成书。

再后来知道作为 p5.js 大本营的纽约大学有出 p5.js shader 的教程,猜测毕业于 NYU ITP 的何老师多半上过这方面课程,那么个人网站里用到 shader 似乎也合情合理了。

未完待续

当然讲了这么久,可能很多人第一次听说 Shader,还是不知道这玩意儿到底是啥。

因为本文已经蛮长了,具体介绍还是留到下一篇文章继续。太久没写文章,确实磕磕绊绊花了好久,希望大家能有所收获。

下篇文章指路:断更19个月,携 Three.js Shader 归来!(下) - 牛衣古柳

照例

最后欢迎加入「可视化交流群」,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj,备注「可视化加群」即可。

欢迎关注古柳的公众号「牛衣古柳」,并设置星标,以便第一时间收到更新。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

自定义view实战(7):大小自动变换的类ViewPager

发表于 2023-04-14
前言

上一篇做了一个滑动折叠的Header控件,主要就是练习了一下滑动事件冲突的问题,控件和文章写的都不怎么样。本来想通过这篇文章的控件,整合一下前面六篇文章的内容的,结果写的太复杂了,就算了,没有新的技术知识,功能也和之前的安卓滚动选择控件类似,不过在写的过程还是有点难度的,用来熟悉自定义view知识还是很不错的。

需求

这里我也不知道应该怎么描述这个控件,标题里用的大小自动变换的类ViewPager,一开始我把它叫做模仿桌面切换的多页面切换控件。大致就是和电视那种切换页面时,中间页面大,边上页面小,切换到中间会有变大的动画效果,我是觉得这样的控件很炫酷。

核心思想如下:

  • 1、类似viewpager,但同时显示两种页面,中间为主页面,左右为小页面,小页面大小一样,间距排列
  • 2、左右滑动可以将切换页面,超过页面数量大小不能滑动,滑动停止主界面能自动移动到目标位置

效果图

效果图

编写代码

这里代码写的还是挺简单的,没有用到ViewPager那样的Adapter,也没有处理预加载问题,滑动起来不是特别流畅,页面放置到顶层时切换很突兀,但是还是达到了一开始的设计要求吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
koltin复制代码import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import androidx.core.animation.addListener
import androidx.core.view.children
import com.silencefly96.module_common.R
import java.util.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.roundToInt


/**
* @author silence
* @date 2022-10-20
*/
class DesktopLayerLayout @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attributeSet, defStyleAttr) {

companion object{
// 方向
const val ORIENTATION_VERTICAL = 0
const val ORIENTATION_HORIZONTAL = 1

// 状态
const val SCROLL_STATE_IDLE = 0
const val SCROLL_STATE_DRAGGING = 1
const val SCROLL_STATE_SETTLING = 2

// 默认padding值
const val DEFAULT_PADDING_VALUE = 50

// 竖向默认主界面比例
const val DEFAULT_MAIN_PERCENT_VERTICAL = 0.8f

// 横向默认主界面比例
const val DEFAULT_MAIN_PERCENT_HORIZONTAL = 0.6f

// 其他页面相对主界面页面最小的缩小比例
const val DEFAULT_OTHER_VIEW_SCAN_SIZE = 0.5f
}

/**
* 当前主页面的index
*/
@Suppress("MemberVisibilityCanBePrivate")
var curIndex = 0

// 由于将view提高层级会搞乱顺序,需要记录原始位置信息
private var mInitViews = ArrayList<View>()

// view之间的间距
private var mGateLength = 0

// 滑动距离
private var mDxLen = 0f

// 系统最小移动距离
private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop

// 控件状态
private var mState = SCROLL_STATE_IDLE

// 当前设置的属性动画
private var mValueAnimator: ValueAnimator? = null

// 实际布局的左右坐标值
private var mRealLeft = 0
private var mRealRight = 0

// 上一次按下的横竖坐标
private var mLastX = 0f

// 方向,从XML内获得
private var mOrientation: Int

// 是否对屏幕方向自适应,从XML内获得
private val isAutoFitOrientation: Boolean

// padding,从XML内获得,如果左右移动,则上下要有padding,但左右没有padding
private val mPaddingValue: Int

// 竖向主内容比例,从XML内获得,剩余两边平分
private val mMainPercentVertical: Float

// 横向主内容比例,从XML内获得,剩余两边平分
private val mMainPercentHorizontal: Float

// 其他页面相对主界面页面最小的缩小比例
private val mOtherViewScanMinSize: Float

init {
// 获取XML参数
val typedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.DesktopLayerLayout)

mOrientation = typedArray.getInteger(R.styleable.DesktopLayerLayout_mOrientation,
ORIENTATION_VERTICAL)

isAutoFitOrientation =
typedArray.getBoolean(R.styleable.DesktopLayerLayout_isAutoFitOrientation, true)

mPaddingValue = typedArray.getInteger(R.styleable.DesktopLayerLayout_mPaddingValue,
DEFAULT_PADDING_VALUE)

mMainPercentVertical =
typedArray.getFraction(R.styleable.DesktopLayerLayout_mMainPercentVertical,
1, 1, DEFAULT_MAIN_PERCENT_VERTICAL)

mMainPercentHorizontal =
typedArray.getFraction(R.styleable.DesktopLayerLayout_mMainPercentHorizontal,
1, 1, DEFAULT_MAIN_PERCENT_HORIZONTAL)

mOtherViewScanMinSize =
typedArray.getFraction(R.styleable.DesktopLayerLayout_mOtherViewScanMinSize,
1, 1, DEFAULT_OTHER_VIEW_SCAN_SIZE)

typedArray.recycle()
}

override fun onFinishInflate() {
super.onFinishInflate()
// 获得所有xml内的view,保留原始顺序
mInitViews.addAll(children)
}

// 屏幕方向变化并不会触发,初始时会触发,自适应
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// Log.e("TAG", "onSizeChanged: w=$w, h=$h")
// 根据屏幕变化修改方向,自适应
if (isAutoFitOrientation) {
mOrientation = if (w > h) ORIENTATION_HORIZONTAL else ORIENTATION_VERTICAL
requestLayout()
}
}

// 需要在manifest中注册捕捉事件类型,android:configChanges="orientation|keyboardHidden|screenSize"
public override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
mOrientation = ORIENTATION_VERTICAL
requestLayout()
}else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
mOrientation = ORIENTATION_HORIZONTAL
requestLayout()
}
}

// 排列规则:初始化第一个放中间,其他向右排列,中间最大,中心在左右边上的最小,不可见的也是最小
// view的大小应该只和它在可见页面的位置有关,不应该和curIndex有关,是充分不必要关系
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 获取默认尺寸,考虑背景大小
val width = max(getDefaultSize(0, widthMeasureSpec), suggestedMinimumWidth)
val height = max(getDefaultSize(0, heightMeasureSpec), suggestedMinimumHeight)

// 设置间距
mGateLength = width / 4

// 中间 view 大小
val maxWidth: Int
val maxHeight: Int

// 不同方向尺寸不同
if (mOrientation == ORIENTATION_HORIZONTAL) {
maxWidth = (width * mMainPercentHorizontal).toInt()
maxHeight = height - 2 * mPaddingValue
}else {
maxWidth = (width * mMainPercentVertical).toInt()
maxHeight = height - 2 * mPaddingValue
}

// 两侧 view 大小,第三排
val minWidth = (maxWidth * mOtherViewScanMinSize).toInt()
val minHeight = (maxHeight * mOtherViewScanMinSize).toInt()

var childWidth: Int
var childHeight: Int
for (i in 0 until childCount) {
val child = mInitViews[i]
val scanSize = getViewScanSize(i, scrollX)
childWidth = minWidth + ((maxWidth - minWidth) * scanSize).toInt()
childHeight = minHeight + ((maxHeight - minHeight) * scanSize).toInt()
// Log.e("TAG", "onMeasure($i): childWidth=$childWidth, childHeight=$childHeight")
child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY))
}

setMeasuredDimension(width, height)
}

// 选中view为最大,可见部分会缩放,不可见部分和第三排一样大
private fun getViewScanSize(index: Int, scrolledLen: Int): Float {
var scanSize = 0f

// 开始时当前view未测量,不计算
if (measuredWidth == 0) return scanSize

// 初始化的时候,第一个放中间,所以index移到可见范围为[2+index, index-2],可见!=可移动
val scrollLeftLimit = (index - 2) * mGateLength
val scrollRightLimit = (index + 2) * mGateLength

// 先判断child是否可见
if (scrolledLen in scrollLeftLimit..scrollRightLimit) {
// 根据二次函数计算比例
scanSize = scanByParabola(scrollLeftLimit, scrollRightLimit, scrolledLen).toFloat()
}

return scanSize
}

// 根据抛物线计算比例,y属于[0, 1]
// 映射关系:(form, 0) ((from + to) / 2, 0) (to, 0) -> (0, 0) (1, 1) (2, 0)
@Suppress("SameParameterValue")
private fun scanByParabola(from: Int, to: Int, cur: Int): Double {
// 公式:val y = 1 - (x - 1).toDouble().pow(2.0)
// Log.e("TAG", "scanByParabola:from=$from, to=$to, cur=$cur ")
val x = ((cur - from) / (to - from).toFloat() * 2).toDouble()
return 1 - (x - 1).pow(2.0)
}

// layout 按顺序间距排列即可,大小有onMeasure控制,开始位置在中心,也和curIndex无关
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val startX = (r + l) / 2
// 排列布局
for (i in 0 until childCount) {
val child = mInitViews[i]

// 中间减去间距,再减去一半的宽度,得到左边坐标
val left = startX + mGateLength * i - child.measuredWidth / 2
val top = (b + t) / 2 - child.measuredHeight / 2
val right = left + child.measuredWidth
val bottom = top + child.measuredHeight

// Log.e("TAG", "onLayout($i): left=$left, right=$right")
child.layout(left, top, right, bottom)
}

// 修改大小,布局完成后移动
scrollBy(mDxLen.toInt(), 0)
mDxLen = 0f

// 完成布局及移动后,绘制之前,将可见view提高层级
val targetIndex = getCurrentIndex()
for (i in 2 downTo 0) {
val preIndex = targetIndex - i
val aftIndex = targetIndex + i

// 逐次提高层级,注意在mInitViews拿就可以,不可见不管
if (preIndex in 0..childCount) {
bringChildToFront(mInitViews[preIndex])
}

if (aftIndex != preIndex && aftIndex in 0 until childCount) {
bringChildToFront(mInitViews[aftIndex])
}
}
}

// 根据滚动距离获得当前index
private fun getCurrentIndex()= (scrollX / mGateLength.toFloat()).roundToInt()

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev?.let {
when(it.action) {
MotionEvent.ACTION_DOWN -> {
mLastX = ev.x
if(mState == SCROLL_STATE_IDLE) {
mState = SCROLL_STATE_DRAGGING
}else if (mState == SCROLL_STATE_SETTLING) {
mState = SCROLL_STATE_DRAGGING
// 去除结束监听,结束动画
mValueAnimator?.removeAllListeners()
mValueAnimator?.cancel()
}
}
MotionEvent.ACTION_MOVE -> {
// 若ACTION_DOWN是本view拦截,则下面代码不会触发,要在onTouchEvent判断
val dX = mLastX - ev.x
return checkScrollInView(scrollX + dX)
}
MotionEvent.ACTION_UP -> {}
}
}
return super.onInterceptHoverEvent(ev)
}

// 根据可以滚动的范围,计算是否可以滚动
private fun checkScrollInView(length : Float): Boolean {
// 一层情况
if (childCount <= 1) return false
// 左右两边最大移动值,即把最后一个移到中间
val leftScrollLimit = 0
val rightScrollLimit = (childCount - 1) * mGateLength

return (length >= leftScrollLimit && length <= rightScrollLimit)
}

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent?): Boolean {
ev?.let {
when(it.action) {
// 防止点击空白位置或者子view未处理touch事件
MotionEvent.ACTION_DOWN -> return true
MotionEvent.ACTION_MOVE -> {
// 如果是本view拦截的ACTION_DOWN,要在此判断
val dX = mLastX - ev.x
if(checkScrollInView(scrollX + dX)) {
move(ev)
}
}
MotionEvent.ACTION_UP -> moveUp()
}
}
return super.onTouchEvent(ev)
}

private fun move(ev: MotionEvent) {
val dX = mLastX - ev.x

// 修改mScrollLength,重新measure及layout,再onLayout的最后实现移动
mDxLen += dX
if(abs(mDxLen) >= mTouchSlop) {
requestLayout()
}

// 更新值
mLastX = ev.x
}

private fun moveUp() {
// 赋值
val targetScrollLen = getCurrentIndex() * mGateLength
// 不能使用scroller,无法在移动的时候进行测量
// mScroller.startScroll(scrollX, scrollY, (targetScrollLen - scrollX), 0)

// 这里使用ValueAnimator处理剩余的距离,模拟滑动到需要的位置
val animator = ValueAnimator.ofFloat(scrollX.toFloat(), targetScrollLen.toFloat())
animator.addUpdateListener { animation ->
// Log.e("TAG", "stopMove: " + animation.animatedValue as Float)
mDxLen = animation.animatedValue as Float - scrollX
requestLayout()
}

// 在动画结束时修改curIndex
animator.addListener (onEnd = {
curIndex = getCurrentIndex()
mState = SCROLL_STATE_IDLE
})

// 设置状态
mState = SCROLL_STATE_SETTLING

animator.duration = 300L
animator.start()
}
}

desktop_layer_layout_style.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name ="DesktopLayerLayout">
<attr name="mOrientation">
<enum name ="vertical" value="0" />
<enum name ="horizontal" value="1" />
</attr>
<attr name="isAutoFitOrientation" format="boolean"/>
<attr name="mPaddingValue" format="integer"/>
<attr name="mMainPercentVertical" format="fraction"/>
<attr name="mMainPercentHorizontal" format="fraction"/>
<attr name="mOtherViewScanMinSize" format="fraction"/>
</declare-styleable>
</resources>

主要问题

这里用到的知识之前六篇文章都已经讲过了,主要就是有几点实现起来复杂了一些,下面讲讲。

页面的自动缩放

讲解页面的缩放之前,需要先将一下页面的摆放。这里以四分之一为间距来摆放来自XML的view,第一个view放在中间,其他都在其右边按顺序排列。

所以页面的缩放,只和view的位置有关,而view的位置又只和当前控件左右滑动的距离有关,变量就是当前控件横坐标上的滑动值scrollX。根据view的原始index可以得到每个view可见时的滑动值范围,在通过这个范围和实际的滑动值scrollX,进行映射换算得到其缩放比例。这里用到了抛物线进行换算:

1
2
css复制代码// 公式:y = 1 - (x - 1).toDouble().pow(2.0)
// 映射关系:(form, 0) ((from + to) / 2, 0) (to, 0) -> (0, 0) (1, 1) (2, 0)
滑动范围的限定

滑动范围的限定和上面类似,边界就是第一个或者最后一个view移动到正中间的范围,只要实际的滑动值scrollX在这个范围内,那滑动就是有效的。

页面层级提升与恢复

页面层级的提升在我之前文章:手撕安卓侧滑栏也有用到,就是自己把view放到children的最后去,实际上ViewGroup提供了类似的功能:bringChildToFront,但是原理是一样的。

1
2
3
4
5
6
7
8
9
10
11
java复制代码    @Override
public void bringChildToFront(View child) {
final int index = indexOfChild(child);
if (index >= 0) {
removeFromArray(index);
addInArray(child, mChildrenCount);
child.mParent = this;
requestLayout();
invalidate();
}
}

这里的提升view不止一个了,而且后面还要恢复,即不能打乱children的顺序。所以我在onFinishInflate中用一个数组保存下这些子view的原始顺序,使用的时候用这个数组就行,children里面的顺序不用管,只要让需要显示的view放在最后就行。我这里因为间距是四分之一的宽度,最多可以显示五个view,所以在onLayout的最后将这五个view得到,并按顺序放到children的最后。

onDraw探讨

这里我还想对onDraw探讨一下,一开始我以为既然onMeasure、onLayout中都需要去调用child的measure和layout,那能不能在onDraw里面自己去绘制child,不用自带的,结果发现这是不行的。onDraw实际是View里面的一个空方法,实际对页面的绘制是在控件的draw方法中,那重写draw方法自己去绘制child呢?实际也不行,当把draw方法里面的super.draw时提示报错:
tips

也就是说必须继承super.draw这个方法,点开源码发现,super.draw已经把child绘制了,而且onDraw方法也是从里面传出来的。所以没办法,乖乖用bringChildToFront放到children最后去,来提升层级吧,不然也不会提供这一个方法来是不是?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【问题解决】解决 swagger2 默认地址失效 前言 修改

发表于 2023-04-13

本文正在参加「金石计划」

前言

有段时间没用 Java 写过项目了,今天因为需求要搭建一个小项目,果然是略显生疏,一路磕磕碰碰的,不过总算都是让我解决了。

回归正题,本篇博文要讲的是,关于配置好 swagger2 之后,访问其页面却被告诉页面不存在,即默认地址失效的问题。

当然也顺带讲解一下 SpringBoot 和 Springfox 的版本兼容性问题。以下就先讲解如何简单地解决版本兼容性问题。

修改路径匹配策略

先介绍一下相关的配置信息,SpringBoot 用的版本是 2.7.10,maven 是 3.6.1,用的是阿里云的镜像。

swagger2 的安装配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

然后新建一个 SwaggerConfig.java 类,用于配置一些与 Swagger2 相关的内容,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
public Docket examApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("xxx")
.select()
.apis(RequestHandlerSelectors.basePackage("com.sidiot.xxx.controller"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("xxx")
.description("xxx")
.contact(new Contact("sid10t.", "https://www.sid10t.com", "e-mail"))
.version("1.0.0")
.build();
}
}

然后将程序启动,发现报错了:

image.png

错误原因是 org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException;

这个异常表示在启动 Spring 应用程序上下文时,documentationPluginsBootstrapper 这个 Bean 启动失败,并且嵌套异常是 NullPointerException。通常这种错误发生在调用一个空对象的方法或者访问一个空对象的属性时。

这是因为 SpringBoot 在 2.6.1 之后,SpringMVC 处理程序映射匹配请求路径的默认策略已从 AntPathMatcher 更改为 PathPatternParser。而 Springfox 使用的路径匹配还是 AntPathMatcher,因此导致了这个错误的发生。

那么这里只需要在配置文件 application.properties 中,重新修改策略即可:

1
properties复制代码spring.mvc.pathmatch.matching-strategy=ant-path-matcher

用 .yml 的小伙伴这样改:

1
2
3
4
yml复制代码spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher

修改完成之后就能正常访问到页面了!

关于 SpringBoot 在 2.6.1 之后的一些变化,可以参考这篇博文:Springboot 升级到 2.6.1 的坑;

用这个方法解决兼容性问题的小伙伴,是不会碰到 swagger2 默认地址失效的问题的,用下面一种方法解决兼容性问题就会遇到!

使用 @EnableWebMvc 注解

是的,除了上述提到的修改匹配策略之外,还有一种方式也能解决兼容性问题,那就是使用注解 @EnableWebMvc;

我们只需要在启动类上加上 @EnableWebMvc 这个注解就可以了:

1
2
3
4
5
6
7
8
9
java复制代码@EnableWebMvc
@SpringBootApplication
public class xxxApplication {

public static void main(String[] args) {
SpringApplication.run(xxxApplication.class, args);
}

}

添加上注解之后,启动我们的程序看一看,发现没有报错,是正常运行的,在打开 swagger 的页面瞅瞅,

image.png

发现找不到页面,在看看控制台也是如此:

1
yaml复制代码2023-04-13 17:34:54.885  WARN 17948 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound             : No mapping for GET /swagger-ui.html

试了试其他相关的 url 路径也是找不到,这是为什么呢?

先简单介绍一下 @EnableWebMvc 这个注解:

@EnableWebMvc 是 SpringMVC 框架中的一个注解,它的作用是开启对 SpringMVC 的支持。

具体来说,使用 @EnableWebMvc 注解会导入一系列与 SpringMVC 相关的配置类,并且会自动注册多个关键组件,如 HandlerMapping、HandlerAdapter、ViewResolver 等。这些组件可以让开发者方便地处理 HTTP 请求和响应、实现 MVC 模式以及生成视图。

但需要注意的是,如果使用了 @EnableWebMvc 注解,则默认情况下会禁用 SpringBoot 中的自动配置,因为 @EnableWebMvc 已经提供了类似的功能。如果想要同时使用 SpringBoot 的自动配置和@EnableWebMvc,可以通过在配置类上添加 @Import({WebMvcAutoConfiguration.class}) 注解来实现。

而 Swagger 通常是使用 springfox-swagger2 和 springfox-swagger-ui 这两个库来实现的。在使用 @EnableWebMvc 注解时,会覆盖掉 SpringBoot 自动配置中的 WebMvcAutoConfiguration,可能导致 Swagger 的默认地址 /swagger-ui.html 失效。因为在 WebMvcAutoConfiguration 类中有一个关于 Swagger 的默认配置项:

1
2
3
4
5
6
java复制代码@Configuration
@ConditionalOnClass({ UiConfiguration.class })
@EnableConfigurationProperties(SwaggerUiProperties.class)
public class SwaggerUiWebMvcConfiguration {
// ...
}

这个类在提供了许多 Swagger 相关的默认配置,包括默认的 UI 界面路径 /swagger-ui.html。但是,当添加 @EnableWebMvc 注解后,SpringMVC 将覆盖掉这个类的配置,进而导致 Swagger 的默认 UI 界面无法使用。

解决这个问题的方法是手动配置 Swagger 相关的 Bean,并指定 Swagger UI 的访问路径和资源文件位置。比如可以在配置类中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}

@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter() {
return new WebMvcConfigurerAdapter() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui.html**")
.addResourceLocations("classpath:/META-INF/resources/swagger-ui.html");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
};
}
}

这样就可以手动配置 Swagger 相关的 Bean,并指定 Swagger UI 的访问路径和资源文件位置,从而解决 @EnableWebMvc 导致 Swagger 默认地址失效的问题。

后记

以上就是 解决 swagger2 默认地址失效 的全部内容了,希望对大家有所帮助!

📝 上篇精讲:【问题解决】解决如何在 CPU 上加载多 GPU 训练的模型

💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;

👍 创作不易,请多多支持;

🔥 系列专栏:问题解决 JAVA

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【若川视野 x 源码共读】第43期 自从学了 react

发表于 2023-04-06

源码共读前言

为了能帮助到更多对源码感兴趣、想学会看源码、提升自己写作和前端技术能力的同学。 帮助读者夯实基础,查漏补缺,开阔眼界,拓宽视野,知其然知其所以然。

我倾力组织了每周一起学200行左右的源码共读活动。我写有《学习源码整体架构系列》20余篇,走过路过的小伙伴可以点击关注下这个目前是掘金关注数最多的专栏。

欢迎点此扫码加我微信 ruochuan02 交流,参与 源码共读 活动,每周大家一起学习200行左右的源码,共同进步。可以持续关注我@若川。

从易到难推荐学习顺序

活动介绍和顺序具体看这里从易到难推荐学习顺序

提交笔记

提交笔记方式,具体的看这里
简言之:看任务,看辅助文章、看源码,交流讨论,在掘金写笔记,写好后提交到本文评论区。

为了给大家谋福利,另外给大家的文章带来更多阅读量,便于搜索,从2022年3月27日起,笔记可以直接发布在掘金,以《标题自取》标题不限,可以取个好标题,容易被掘金推荐。

笔记文章开头加两句话:

  • 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
  • 这是源码共读的第xx期,链接:xxx。

笔记文章最后,建议写上总结、收获、感受等。

  • 开头第一句作用是:方便每月统计评优,掘金赞助送小礼物。顺便帮忙宣传推广,让更多人参与进来,一起学习。
  • 开头第二句作用是:加上是多少期,当前任务说明的链接,方便读者知道这是什么活动。

笔记写完后,到当前期活动的文章评论区留言自己的文章和笔记特点。方便大家查阅学习交流讨论。

任务发布时间

2023年4月3日 - 2023年4月14日(两周)。可以按照自己节奏学习,提交笔记即可(不一定要严格按照我规定的时间)。往期共读也可以及时复习,笔记未完成可以继续完成。

语雀本期任务说明链接

语雀有树形菜单,更方便查看,所以也放下语雀的这一期链接

学习任务

  1. 挑一些 想学的 react-use 自定义 hooks 学习
  2. 学习 storybook 搭建
  3. 可以学习测试用例等
  4. 等等
  • 参考我的文章
  • 自从学了 react-use 源码,我写自定义 React Hooks 越来越顺了~

参考文章

  • 自从学了 react-use 源码,我写自定义 React Hooks 越来越顺了~
  • 看文章,看源码,交流讨论,写笔记发布在掘金。再在掘金这篇文章下评论放上提交笔记的链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构与算法

发表于 2023-04-01

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。

本文是数据结构与算法系列的第 19 篇文章,完整文章目录请移步到文章末尾~

前言

大家好,我是小彭。

2023 开年以来,全球媒体最火爆的热点莫过于一个生成式 AI 聊天机器人 —— ChatGPT,我们都被大量的信息刷屏了。在这些信息中,你或许看过这样一则新闻 《ChatGPT Passes Google Coding Interview for Level 3 Engineer With $183K Salary》,它说 ChatGPT 通过了谷歌工程师面试,则这个职位的年薪有 18.3 万美元。

让会不会让一些小伙伴产生焦虑的想法?

图片截图自新闻:www.pcmag.com/news/chatgp…

谷歌面试是会考算法的,ChatGPT 已经具备这么强的算法能力了吗?如果答案是肯定的,那么我们借助 ChatGPT 的力量帮助提高算法能力,是不是可行的想法。

试想一下:我们要学习一个算法或者新知识新领域,直接将问题抛给 AI,让 AI 直接传道授业解惑,直接向你总结出全社会沉淀下来的最有价值的经验、方法论、内容或观点,你甚至都不需要去上学、找资料、看书。遇到不理解的地方可以继续向 AI 追问,AI 还会非常有耐心的帮你解释……

未来,固然需要想象。

现实,ChatGPT 能做到吗?

正好,最近有群友让我写一篇算法的入门文章,借此机会,就让我们来学习如何使用 ChatGPT 辅助算法学习:

在接下来的几篇文章中,小彭将继续为你介绍 AI 技术的使用攻略以及实践感悟。

如果你对 ChatGPT 还不够熟悉,希望我能够为你提供一些指引。

让我们开始吧!


今天文章比较长,写个简单的大纲:

1、LeetCode 算法题学习链路简要分析

2、ChatGPT 助手从入门到放弃

2.1 ChatGPT 的能力和限制

2.2 ChatGPT 的使用原则

3、面向 ChatGPT 编程的正确打开方式

4、总结


  1. LeetCode 算法题学习链路简要分析

首先,请你思考:完整地学习一道算法题包含哪些步骤或动作:

  • 步骤一:复制代码 🌚
  • 步骤二:粘贴运行 🌚
  • 步骤三:自我满足 🌚

说笑了,这么有任何价值(求饶),应该是:

  • 阶段 1 - 白板编码
  • 阶段 2 - 阅读优质题解
  • 阶段 3 - 抽象问题模型

1.1 阶段 1 - 白板编码

这里所说的 “白板” 并不是真的在白板上写出答案,而是说在无外力辅助的环境下独立解出题目。这个阶段不仅仅是写代码,而是:

  • 1.1 阅读: 快速阅读题目信息,提取出题目的关键信息,包括题目目标、关键约束、输入输出数据类型、数据量大小等;
  • 1.2 抽象: 结合已掌握的算法知识抽象出题目的问题模型,并思考解决问题的算法,需注意算法复杂度能否满足问题的数据量上界;
  • 1.3 编码: 题目一般有多种算法实现,优先写出复杂度最优的版本。如果做不到,则先写出最熟悉的算法,再优化为复杂度更优的算法(在面试和竞赛中策略类似);
  • 1.4 检查: 检查题目条件并完善代码,包括问题约束、数组越界、大数越界、小数精度、初始状态值、目标状态值等;
  • 1.5 调试: 重复第 1 - 4 个动作直到题目通过所有测试用例。

至此,你已经 “通过” 这道题(或许没有),然而你只是对已经学过的知识复习了一遍,对这部分算法更加熟悉了,但是并没有知识增量,所以你需要进入阶段 2:

1.2 阶段 2 - 阅读优质题解

阅读优质题解一直是最快的提升算法能力的途径,也是整个学习链路中最花时间的部分。好在社区中有非常多热爱算法的小伙伴,即使是刚刚发布的周赛题目,也不会出现没有题解的情况。

推荐一些优质算法题解作者:

  • Krahets 上海交通大学,著有 《Hello 算法》
  • 小羊肖恩 北京大学,Rank 全球 Top 20,全国 Top 10
  • 灵茶山艾府 浙江大学,Rank 全球 Top 20,全国 Top 10
  • liweiwei1419 四川师范大学,参与录制 LC 官方视频题解
  • 负雪明烛,美团,毕业时收获 BAT、亚马逊、微软等 Offer
  • 宫水三叶的刷题日记,微软

因此,阅读题解阶段主要障碍就是看不懂,哪里看不懂呢?

  • 难点 1 - 算法: 理解算法本身,文字、注释、代码、图表这些都是算法的表现形式;
  • 难点 2 - 算法推导: 理解从题目一步步到算法的推导过程(有些题解会省略推导过程);
  • 难点 3 - 算法证明: 理解算法的严格证明过程,特别是贪心算法(有些题解会省略证明过程)。

举个例子,题目 743. 网络延迟时间 有「 Dijkstra + 最小堆 」的算法,那么:

  • 算法:理解 “每轮迭代从小顶堆中获取候选集里距离起点最短路长度最小的节点,并使用该点松弛相邻边” 就是理解算法本身;
  • 算法推导:理解 “暴力 BFS/DFS → Floyd → 朴素 Dijkstra → Dijkstra + 最小堆” 就是理解算法推导过程;
  • 算法证明:理解 “选择候选集中距离起点最短路长度最小的节点的方案不存在更优解” 就是理解算法证明过程。

有些题目会由多个解法,等于有多个算法、多个算法推导过程以及多个算法证明过程,那么你要理解的信息量是成倍增加的。这里要根据时间和优先级有所取舍。

理解了阅读题解阶段的主要障碍,那么这些障碍是由哪些原因导致的呢?

  • 原因 1:题解结构缺失: 有的题解只提供了可运行的代码,但是省略了推导过程和证明过程,甚至没有复杂度分析(题解:没有复杂度分析我不完整了);
  • 原因 2 - 思维复杂度过高: 有些题解结果完整,讲解也很详细,但是其中某些难点或某几行代码思维复杂度很高(大脑复杂度 O(nn)O(n^n)O(nn));
  • 原因 3 - 前置知识缺失: 有些算法需要一些前置知识基础,例如 Dijkstra 算法就需要有图论基础(基础不牢地动山摇);
  • 原因 4 - 代码语言缺失: 有些题解只会提供一种语言的代码(说的就是我);

在你阅读题解时,你还会尝试根据题解写出代码,相当于回退到阶段 1 的编码阶段,但这个阶段会尽可能多地借助外力辅助。 因为此时编码不是目的,是通过编码的方式加深对算法的理解。

至此,你不仅理解了题解的大部分信息,还手撸了一遍代码,恭喜你已经 “学会” 这道题,但是如果换一道变型题呢?这道题学会了,并不代表这一类的题目你都学会了,所以你需要进入阶段 3:

1.3 阶段 3 - 抽象问题模型

抽象问题模型就是你在阶段 1-2 抽象步骤中做的事情。

你之所以能在短时间解决算法问题, 是因为你曾经做过这道问题,或者曾经做过这类问题,曾经抽象过这类问题(天赋选手除外),这就是我们常说的多做多刷多总结。

所谓抽象,就是总结出题目的模型 / 套路,怎么做呢?

  • 3.1 题目类型: 例如数学、双指针、回溯、动态规划、贪心、数据结构就是一级题目类型。继续细分下去,双指针又分为二分查找、滑动窗口、同向 / 相向双指针,动态规划又分为线性 DP、树形 DP、转压 DP、数位 DP 等,回溯又分为排列、组合、子集等等;
  • 3.2 算法模型: 模型就是我们说的解题模板 / 板子。例如二分查找有排除严格不成立方向的模型,背包问题有 01 背包和完全背包和滚动数组的模型,线段树有朴素线段树 +Lazy 的模型,质数有暴力和筛法的模板等等;
  • 3.3 编码技巧: 例如负数取模技巧、数组补 0 位技巧、链表 Dummy 节点技巧、除法转乘法技巧等等;

抽象题目模型需要建立在大量阅读量和刷题量的基础上,是最重要的环节。经过后文实验验证,目前 GPT-3.5 和 GPT-4 都无法达到顶尖算法高手的抽象水平。

最后,你还需要将整个思考过程按照题目编号记录下来,方便未来的你查阅,小彭就是简单记录在 Github 上。有时候我重新做一道题后,会发现今天写的代码质量比几个月前写的的代码质量高出很多,也在见证自己的成长。

至此,学习链路分析结束!

1.4 LeetCode 算法题学习链路小结

完成 LeetCode 算法题学习链路的简要分析后,用一个表格总结:

阶段 动作 描述
1、白板编码 1.1 阅读 快速阅读题目信息,提取出题目的关键信息
1.2 抽象 结合已掌握的算法知识抽象出题目的问题模型
1.3 编码 撸代码
1.4 检查 检查题目条件并完善代码
1.5 调试(for Loop) 重复第 1 - 4 个动作直到题目通过所有测试用例
2、阅读优质题解 2.1 理解算法 理解算法本身,文字、注释、代码、图表这些都是算法的表现形式
2.2 理解算法推导过程 理解从题目一步步到算法的推导过程
2.3 理解算法证明过程 理解算法的严格证明过程,特别是贪心算法
2.4 辅助编码(Async) 撸代码
3、抽象问题模型 3.1 抽象题目类型 例如数学、双指针、回溯、动态规划、贪心、数据结构
3.2 抽象算法模型 例如二分查找、背包问题、线段树 + Lazy、质数
3.3 抽象编码技巧 例如负数取模技巧、数组补 0 位技巧、链表 Dummy 节点技巧、除法转乘法

接下来,我们开始思考如何将 ChatGPT 有机地融入到算法题的学习链路中:


  1. ChatGPT 助手从入门到放弃

ChatGPT 在对自然语言理解方面的进步是令人惊叹的,很多情况下只需要输入一个模糊的指令,ChatGPT 就能直接生成比较完整的答案。

然而,随着问题的深入和复杂化,ChatGPT 的错误和问题也逐渐显露出来,这就是我从入门到 “放弃” 的整个过程:

2.1 向 ChatGPT 提问的一个误区

如果我们直接把问题交给 ChatGPT,试图让它直接输出详细的题解,会发生什么?

可以看到,这份回答有算法类型、算法描述、算法推导过程、算法实现和复杂度分析,算是一份相对完整的题解,但远远还谈不上优质,与 LeetCode 上优质的题解比相去甚远。

不是把 ChatGPT 吹到天上去了吗,问题出在哪里呢?

因为我们提出的一个开放(open-ended)问题,而这种提问方式对 ChatGPT 是低效的:

  • 详细:怎么定义详细?100 个字还是 1000 个字叫详细?
  • 优质:怎么定义优质?需要多少种算法,需要举一反三吗?
  • 题解:怎么定义题解?需要包含哪些信息?

总之,“请你写出详细优质的题解”、“你能帮我做这道题吗” 和 “请你告诉我这道题” 这三种问法,在 ChatGPT 看来并没有本质区别,而且实测结果出奇地一致(请避免 😁)。

2.2 一个万能的 ChatGPT prompt 模板

在经过一整天被人工智障折磨,以及阅读 《The Art of ChatGPT Prompting》后,我总结出一个提问模板:

  • 1、角色:限定知识领域(注意:New Bing 玩角色扮演出错概率偏高);
  • 2、目标:一句话概括需要的帮助;
  • 3、要求:对目标补充的具体清晰的要求,尽量表达清晰,避免模糊,一般采用 Do 和 Do not 格式;
  • 4、举例(可选):输入输出样例。

后来我发现 OpenAI 在 GPT-4 的测试论文 《Sparks of Artificial General Intelligence: Early experiments with GPT-4》中,也采用了类似 “角色-目标 - 要求 - 举例” 的模板 ☺。

GPT-4 早期测试报告:arxiv.org/pdf/2303.12…

2.3 一个被逼疯的 prompt

经过和 ChatGPT 的来回拉扯后,我终于写出一版相对满意的 prompt。我试图让 ChatGPT 写出优质的题解,类似于要求 ChatGPT 写一篇文章。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码我需要你担任算法教师和指导者,你需要给出一道 LeetCode 问题的代码和解题思路。
要求:
1、使用中文回答,使用 Kotlin 语言
2、代码必须带有注释
3、确保每个解法是严格不同的算法,并且每个解法包含算法名称、算法概括,算法详细描述、算法推导过程、代码实现、复杂度分析
4、先总结题目的解法个数,以及每个算法的算法名称
5、先输出复杂度最差的暴力解法,再依次输出复杂度更优的解法
6、不要输出题目描述和测试用例
题目:
718. 最长重复子数组

这道题有 4 种解法:

  • 1、使用暴力解法,枚举所有子数组,判断是否为重复子数组。
  • 2、使用动态规划解法,以二维数组 dp[i][j] 记录以A[i] 和 B[j] 为结尾的最长重复子数组长度。
  • 3、使用滑动窗口解法,枚举 A 和 B 所有的对齐方式,计算每种对齐方式的最长重复子数组长度。
  • 4、使用二分查找解法,最长重复子数组长度存在单调性,使用二分查找检查是否存在长度为 len 的重复子数组。

那 ChatGPT 会给出令人满意的答复吗?

可以看到:虽然回答比用 “请你写出详细优质的题解” 提问得到的回答优化了很多,但是连最简单的代码注释要求都没有满足,更不用说和 LeetCode 上的优质题解相比。

何苦看 ChatGPT❓

至于被爆吹的 New Bing 和 GPT-4 模型呢?实验结果和 GPT-3.5 没有明显差别(失望)。

GPT-4 实验结果:poe.com/s/VIkeeiqjD…

2.4 降低要求

在文章 《The Art of ChatGPT Prompting》中,提到:“It’s important to provide the ChatGPT with enough information to understand the context and purpose of the conversation, but too much information can be overwhelming and confusing.”

图片截图自原文:fka.gumroad.com/l/art-of-ch…

那么,有没有可能是因为我们提出的信息量太大,导致 ChatGPT 无法聚焦呢?

我们尝试让 ChatGPT 针对某个算法输出题解:

1
2
3
4
5
6
7
8
kotlin复制代码我需要你担任算法教师和指导者,你需要给出一道 LeetCode 问题的代码和解题思路。
要求:
1、使用中文回答,使用 Kotlin 语言;
2、使用二分查找+哈希表的解法
3、包含算法、算法概括,算法详细描述、算法推导过程、代码实现、复杂度分析
4、不要输出题目描述和测试用例
题目:
718. 最长重复子数组

大同小异,这也说明上一节的测试结果并不是因为要求过多导致。

2.5 重新认识 ChatGPT 的能力和限制

在经历过反复被折磨后,我决定放弃让 ChatGPT 写题解的想法,原因是:

  • 1、ChatGPT 确实有总结提炼的能力,但要 ChatGPT 给出准确、全面、深度的答案,目前 ChatGPT 做不到;
  • 2、相比于总结,人类更看重的是对问题的深度拆解和建构能力,目前 ChatGPT 做不到。

其实,目前爆火的两类 AI 技术,都是工具型 AI,而不是科幻作品中常见的通用型 AI。无论是基于大语言模型 LLM 的 ChatGPT,还是基于扩散算法 Diffusion 的 MidJourney 等绘画工具,本质上都是使用海量数据针对特定场景训练出的模型。它们拥有让全人类难望尘莫及的数据处理能力,但它的能力上限也被封印在这个躯壳中。

AI 这个词,被泛化了。

像 ChatGPT 就是使用互联网上海量的文本数据作为大型预训练语料库,让机器从语料库中学习语言模型,并通过 Transformer 模型来预测下一个单词的概率分布。这里面有 2 个关键词:

  • 概率: ChatGPT 语言模型的数学基础是概率论,通过预测词的概率来输出答案,它绝对无法给出 100% 准确的回答,更不用说自主创作,甚至经常一本正经地给出自相矛盾的回答(GPT-4 也一样)。“原创想法的拙劣表达” 比 “清晰表达的非原创想法” 更有价值,此观点我们在《什么是原创?独立完成就是原创吗?》这篇文章里讨论过。
  • 语料库: ChatGPT 是基于互联网上的公共数据库(截止至 2021 年),不包含互联网上的私有数据库,企业和个人的私有数据库,超出语料库范围的内容它无法给出答案的。

图片截图自 InstructGPT 论文 arxiv.org/pdf/2203.02…

回到文章开头的那个想象,ChatGPT 能做到吗?—— ChatGPT 不仅做不到,还远远做不到,在可见的未来,也不太可能做到。

2.6 在学习过程中使用 ChatGPT 的指导原则

话说回来,我们给 ChatGPT 一个很高的期待值,然后以它达不到期待值为由否定它,是客观的吗?不是。

刚开始使用 ChatGPT 的时候,它所表现的能力的确让我们很多人感觉非常惊叹。只是随着实践使用次数增多,随着提问问题的深入和复杂化,ChatGPT 的错误才开始逐渐显现出来,直到最后令人失望而已,是我们把它捧得太高了!

所以,在使用 ChatGPT 等 AI 技术时,我们应该遵循哪些基本原则呢:

  • 原则 1 - 主动降低预期:

不要神化 ChatGPT,也不要否定 ChatGPT,而是降低对 AI 的预期,主动掌握使用和控制 AI。

我认为将 ChatGPT 称为人类有史以来最智能的一本字典,或许是正确的定位。

在搜索引擎时代,信息以数据的形式存储在全世界的数据库中,我们要从这些信息中获得解决方案,就需要花费大量的时间去触达、筛选、阅读和加工。而在使用 ChatGPT 后,ChatGPT 能够帮我们搜索和整理信息,并直接呈现出整理后的信息,即使我们对这个领域一无所知。

ChatGPT 最大的意义,在于它能够解决 “信息量太大 ”而 “注意力太少” 的矛盾。它能够在一瞬间将每个人在木桶效应中最短的那根短板提高到平均水平,每个人的延展性将被极大地延伸。

因此,虽然目前 AI 无法有效解决综合性的复杂问题,但对于比较基本的模式化的问题,ChatGPT 能在短时间生成完整的方案,也是相当厉害的。把对 ChatGPT 的预期降低,理解它的能力和限制,才能更好的控制它。

  • 原则 2 - 监督和指导:

不要期望 ChatGPT 能够自主解决问题,更不用担心 AI 会取代人类。

ChatGPT 的确可以协助我们完成某些特定任务 / 特定动作,但它更需要在人类的监督和指导下才能有效产出,更无法代替人类的创造力和解决问题的思考力。

还有,大家都遇到过 ChatGPT 遇到复杂问题就开始一本正经地胡说八道,此时需要人类去主动查验和指导它直到给出正确答案。有时候,与其花时间调教 ChatGPT,还不如自己花时间一五一十解决问题来得快。

  • 原则 3 - 思考的权利:

将思考的权利让渡给 AI,是危险的。

ChatGPT 确实具有总结的能力,但是过渡依赖于 AI 来辅助学习,放弃思考的过程,放弃探索未知的过程,放弃折磨大脑皮层的过程,是危险的。长此以往势必会造成学习能力和主观能动性的退化,抗压能力的退化(窥视真理,得到的就一定是真理吗)。

很多时候,我们追求的不仅仅是最终的答案,还包括寻求答案的过程,是一种所谓的 “心流” 状态。因此,越是折磨大脑皮层的动作,我们越不能让渡给 AI,而那些需要花费大量时间的重复的搜索整理动作,不交给 AI 还交给谁呢?

阶段 结论
1、白板编码 100% 不暴露给 AI
2、阅读优质题解 理解过程尽量不暴露给 AI,使用 AI 作为 Checker
3、抽象问题模型 使用 AI 查找和整理信息

至此,我们结论确定。

接下来基于此结论开始使用 ChatGPT 辅助学习过程中的单个动作:


  1. 面向 ChatGPT 编程的正确打开方式

3.1 使用 ChatGPT 输出提示

在做题时,有时候一下子卡住了但是又想挑战自己,可以让 ChatGPT 输出提示试试看,类似于 LeetCode 上的 Hint 功能。

1
2
3
4
5
kotlin复制代码我需要您担任算法教师和指导者,给出一道 LeetCode 问题的 5 条提示。
要求:
1、用中文回答,回答精简,限制在 15 字内
2、如果存在多种解法,优先提示复杂度最差的暴力解法,再依次提示复杂度更优的解法
问题:718. 最长重复子数组

这次 AI 给出了 5 个解法,而在之前的回答只给出了 2 个解法。

3.2 使用 ChatGPT 补齐题解结构

有的题解只提供了可运行的代码,但是省略了推导过程或证明过程,甚至没有复杂度分析,我们可以题解交给 ChatGPT 补齐,以 newhar 的这篇题解为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
kotlin复制代码我需要你担任算法教师和指导者,你需要补齐一道 LeetCode 题的题解。
要求:
1、使用中文回答;
2、为代码增加注释
3、分析时间和空间复杂度
4、解释这个算法
代码:
class Solution {
public:
int collectTheCoins(vector<int>& coins, vector<vector<int>>& edges) {
int n = coins.size();
unordered_set<int> nes[n];
for(const auto& e : edges) {
nes[e[0]].insert(e[1]);
nes[e[1]].insert(e[0]);
}
// 1. 删除所有的无金币的叶子节点,直到树中所有的叶子节点都是有金币的(类似拓扑排序)
vector<int> deleted(n, 0);
queue<int> q;
for(int i = 0; i < n; ++i)
if(nes[i].size() == 1 && coins[i] == 0)
q.push(i);

while(q.size()) {
int cur = q.front(); q.pop();
deleted[cur] = 1;
for(int ne : nes[cur]) {
nes[ne].erase(cur);
if(coins[ne] == 0 && nes[ne].size() == 1) {
q.push(ne);
}
}
}

// 2. 删除树中所有叶子节点,及其相邻边(删两次)
for(int iter = 0; iter < 2; ++iter) {
for(int i = 0; i < n; ++i) {
if(!deleted[i] && nes[i].size() == 1) {
deleted[i] = 1;
}
}
for(int i = 0; i < n; ++i) {
if(deleted[i]) {
for(int ne : nes[i]) {
nes[ne].erase(i);
}
}
}
}

// 3. 答案就是剩下的树中的边数
int res = 0;
for(const auto& e : edges)
if(!deleted[e[0]] && !deleted[e[1]])
res += 2;

return res;
}
};

GPT-4 实验结果:poe.com/s/VIkeeiqjD…

3.3 使用 ChatGPT 辅助阅读

有些题解结果完整,讲解也很详细,但是其中某些难点或某几行代码过于复杂,可以针对性提出问题,ChatGPT 也有能力结合上下文回答。

3.3 使用 ChatGPT 翻译代码

有些题解只会提供一种语言或少部分语言的代码,比如 我的题解 一般只有 Java / Kotlin 语言,可以让 ChatGPT 翻译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
kotlin复制代码我需要你担任算法教师和指导者,你需要将代码翻译为 Python:
要求:
1、包含代码注释
代码:
class Solution {
fun collectTheCoins(coins: IntArray, edges: Array<IntArray>): Int {
val n = coins.size
// 入度表
val inDegrees = IntArray(n)
// 领接表
val graph = HashMap<Int, MutableList<Int>>()
for (edge in edges) {
graph.getOrPut(edge[0]) { LinkedList<Int>() }.add(edge[1])
graph.getOrPut(edge[1]) { LinkedList<Int>() }.add(edge[0])
inDegrees[edge[0]]++
inDegrees[edge[1]]++
}
// 剩余的边
var left_edge = edges.size // n - 1
// 1、拓扑排序剪枝无金币子树
val queue = LinkedList<Int>()
for (node in 0 until n) {
// 题目是无向图,所以叶子结点的入度也是 1
if (inDegrees[node] == 1 && coins[node] == 0) {
queue.offer(node)
}
}
while (!queue.isEmpty()) {
// 删除叶子结点
val node = queue.poll()
left_edge -= 1
// 修改相邻节点
for (edge in graph[node]!!) {
if (--inDegrees[edge] == 1 && coins[edge] == 0) queue.offer(edge)
}
}
// 2、拓扑排序剪枝与叶子结点距离不大于 2 的节点(裁剪 2 层)
// 叶子节点
for (node in 0 until n) {
if (inDegrees[node] == 1 && coins[node] == 1) {
queue.offer(node)
}
}
for (node in queue) {
// 2.1 删除叶子结点
left_edge -= 1
// 2.2 删除到叶子结点距离为 1 的节点
for (edge in graph[node]!!) {
if (--inDegrees[edge] == 1) left_edge -= 1
}
}
// println(inDegrees.joinToString())
// coins=[0,0],edges=[[0,1]] 会减去所有节点导致出现负数
return Math.max(left_edge * 2, 0)
}
}

ChatGPT 会根据原有代码逻辑转层翻译为目标语言,基本完成得不错。美中不足的是,我从来没遇到过代码一次转换后不出错的情况,需要反复指导才能改对,实际作用一般。

3.4 使用 ChatGPT 规范代码

我们可以将自己写的代码交给 ChatGPT 做 CodeReview:

1
2
3
4
5
6
kotlin复制代码我需要你担任算法教师和指导者,你需要对代码做 CodeReview:
要求:
1、评价代码规范性
2、指出不足指出
3、给出改进后的版本
代码:略

图片过大,只截取部分信息。

大部分是一本正经地胡说八道,效果非常一般,跟市面上已有的 Review 工具完全比不了,果然 CodeReview 是个经验活 🌚!!

GPT-4 实验结果:poe.com/s/XelhIjmHx…

3.5 使用 ChatGPT 推荐相似题目

我们可以让 ChatGPT 举出相似题型:

1
2
3
kotlin复制代码我需要你担任算法教师和指导者,你需要给出 10 道同类型题目和链接:
题目:
718. 最长重复子数组

可以看到,ChatGPT 的学习深度只能分析到 “动态规划” 这一层,推荐的题目实际关联度不高。

我们尝试增加限定条件,例如要求算法模型都包含动态规划、滑动窗口和二分查找:

1
2
3
4
5
kotlin复制代码我需要你担任算法教师和指导者,你需要给出 10 道同类型题目和链接:
要求:
1、都包含动态规划、滑动窗口和二分查找解法
题目:
718. 最长重复子数组

这次相似度很高,不错。

是不是逐渐掌握调教 ChatGPT 的正确方式?更多 case 就不再展示了,希望这篇文章能够给你带来一些灵感或者思路。


  1. 总结

回到文章开头的新闻。新闻里提到的 Google L3 级别,其实面向的是实习生或应届生的测试职位,考察的算法也是比较基础和模式化的问题,更不会涉及复杂的系统设计问题(但薪水也有 18.3 万美元,果然宇宙的尽头是外企)。

让我意外的是,在 OpenAI 的 GPT-4 技术报告中,GPT-4 在 LeetCode 算法上的测试数据表现非常糟糕。如果以 GPT-4 的水平是不可能通过 Google 的算法面试的,更不用说该新闻发表日期时的 ChatGPT 应该还在用 GPT-3.5 模型。

这个新闻的可信度很低。

GPT-4 技术报告:cdn.openai.com/papers/gpt-…

总结一下:

  • 1、ChatGPT 能够解决 “信息量太大 ”而 “注意力太少” 的矛盾,每个人的延展性将被极大扩展;
  • 2、ChatGPT 确实有总结提炼的能力,但对问题的深度拆解和建构能力,ChatGPT 无法做倒;
  • 3、将思考的权利让渡给 AI 是危险的,越是折磨大脑皮层的过程越不能让渡给 AI;
  • 4、我们的对手不是 AI,而是比你更懂控制 AI 的人。

最后,希望大家在学习算法的道路上共勉,小彭等拿到 Guardian 牌子后来回来还愿 😭。

你认为 ChatGPT 技术是否被过度炒作?你对这个问题有什么见解?欢迎聊聊你的看,也欢迎你转发、留言,给我一个反馈喔。


ChatGPT 实验资料

  • 交流|面向 ChatGPT 学习算法 —— Krahets 著
  • 如何与ChatGPT4结对编程提升研发效率 —— cheney(腾讯)著
  • 我和 chatGPT 对线操作系统! —— cxuan 著

参考资料

  • The Art of ChatGPT Prompting: A Guide to Crafting Clear and Effective Prompts —— fatih kadir akin 著
  • ChatGPT Is a Blurry JPEG of the Web —— Ted Chiang(《降临》作者)著
  • 通过谷歌面试的 ChatGPT 要取代码农了?硅谷工程师:先别急 —— 硅星人 著
  • 最近很火的 ChatGPT 究竟是什么?会给我们的生活带来什么改变? —— 李睿秋 Lachel 著

推荐阅读

数据结构与算法系列完整目录如下(2023/07/11 更新):

  • #1 链表问题总结
  • #2 链表相交 & 成环问题总结
  • #3 计算器与逆波兰表达式总结
  • #4 高楼丢鸡蛋问题总结
  • #5 为什么你学不会递归?谈谈我的经验
  • #6 回溯算法解题框架总结
  • #7 下次面试遇到二分查找,别再写错了
  • #8 什么是二叉树?
  • #9 什么是二叉堆 & Top K 问题
  • #10 使用前缀和数组解决 “区间和查询” 问题
  • #11 面试遇到线段树,已经这么卷了吗?
  • #12 使用单调队列解决 “滑动窗口最大值” 问题
  • #13 使用单调栈解决 “下一个更大元素” 问题
  • #14 使用并查集解决 “朋友圈” 问题
  • #15 如何实现一个优秀的 HashTable 散列表
  • #16 简答一波 HashMap 常见面试题
  • #17 二叉树高频题型汇总
  • #18 下跳棋,极富想象力的同向双指针模拟
  • #19 ChatGPT 通过谷歌算法面试,年薪 18.3 万美金

Java & Android 集合框架系列文章: 跳转阅读

LeetCode 上分之旅系列文章:跳转阅读

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文正在参加「金石计划」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

自从学了 react-use 源码,我写自定义 React

发表于 2023-03-31
  1. 前言

大家好,我是若川。我倾力持续组织了一年多每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.7k+人)第一的专栏,写有20余篇源码文章。

最近 React 出了 新文档 react.dev,新中文文档 zh-hans.react.dev。

现在用 react 开发离不开各种 hooks。学习各种 hooks 的工具库,有助于我们更好的使用和理解 hooks 。前端社区中有活跃的 ahooks。不过,这次我们来学习目前 36.2k star 的 react-use 库。

react-use 文档
是用 storybook 搭建的。

如果公司项目需要搭建组件库或者 hooks、工具库等,storybook 或许是不错的选择。

react-use 中文翻译仓库,最后更新是2年前,可能有点老。

  1. 环境准备

看一个开源仓库,第一步一般是看 README.md 和 contributing.md 贡献文档。第二步的克隆下来。按照贡献指南文档,把项目跑起来。

贡献文档中有如下文档。

2.1 创建一个新 hook 的步骤

  1. 创建 src/useYourHookName.ts 和 stories/useYourHookName.story.tsx,然后运行 yarn start。
  2. 创建 tests/useYourHookName.test.ts,运行 yarn test:watch 监听测试用例执行。
  3. 创建 docs/useYourHookName.md 文档。
  4. 在 src/index.ts 文件导出你写的 hook,然后添加你的 hook 到 REAMDE.md 中。

我们可以得知具体要做什么,新增 hook 关联哪些文件。

1
2
3
4
5
6
7
8
shell复制代码# 推荐克隆我的仓库
git clone https://github.com/lxchuan12/react-use-analysis.git
cd react-use-analysis/react-use
# 也可以克隆官方项目
git clone https://github.com/streamich/react-use.git
cd react-use
yarn install
yarn start

克隆项目到本地,安装依赖完成后,执行 yarn start。

命令终端运行 yarn start

本地环境打开 useEffectOnce docs:http://localhost:6008/?path=/story/lifecycle-useeffectonce--docs

我们先挑选这个 useEffectOnce 简单的 hook 来分析。

2.2 useEffectOnce

2.2.1 react-use/src/useEffectOnce.ts

1
2
3
4
5
6
7
8
9
10
ts复制代码// react-use/src/useEffectOnce.ts
import { EffectCallback, useEffect } from 'react';

// 源码非常简单,不依赖任何参数的函数。

const useEffectOnce = (effect: EffectCallback) => {
useEffect(effect, []);
};

export default useEffectOnce;

我们来看测试用例,直接使用测试用例调试 useEffectOnce 源码。

我之前写过相关文章。可以参考学习。
你可能不知道测试用例(Vitest)可以调试开源项目(Vue3) 源码

我装了 jest 和 jest runner vscode 插件,装完后测试用例中会直接显示 run、和 debug 按钮。还在装了 vitest、vitest runner vscode 插件,装完后测试用例中会直接显示 run(vitest)和 debug(vitest) 按钮。

如下图所示。

runner

这个项目使用的是 jest。于是我点击最右侧的 debug。

调试 gif 图

2.2.2 react-use/tests/useEffectOnce.test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ts复制代码// react-use/tests/useEffectOnce.test.ts
import { renderHook } from '@testing-library/react-hooks';
import { useEffectOnce } from '../src';

// mock 函数
const mockEffectCleanup = jest.fn();
const mockEffectCallback = jest.fn().mockReturnValue(mockEffectCleanup);

it('should run provided effect only once', () => {
const { rerender } = renderHook(() => useEffectOnce(mockEffectCallback));
// 只调用一次
expect(mockEffectCallback).toHaveBeenCalledTimes(1);

// 重新渲染时,只调用一次
rerender();
expect(mockEffectCallback).toHaveBeenCalledTimes(1);
});

it('should run clean-up provided on unmount', () => {
const { unmount } = renderHook(() => useEffectOnce(mockEffectCallback));
expect(mockEffectCleanup).not.toHaveBeenCalled();

unmount();
// 卸载时 执行一次
expect(mockEffectCleanup).toHaveBeenCalledTimes(1);
});

2.2.3 react-use/stories/useEffectOnce.story.tsx

xxx.story.tsx 渲染组件,可以直接操作。Demo 和 docs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ts复制代码// react-use/stories/useEffectOnce.story.tsx
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useEffectOnce } from '../src';
import ConsoleStory from './util/ConsoleStory';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
useEffectOnce(() => {
console.log('Running effect once on mount');

return () => {
console.log('Running clean-up of effect on unmount');
};
});

return <ConsoleStory />;
};

storiesOf('Lifecycle/useEffectOnce', module)
.add('Docs', () => <ShowDocs md={require('../docs/useEffectOnce.md')} />)
.add('Demo', () => <Demo />);

docs/useEffectOnce.md 省略,基本跟测试用例一样。可以说测试用例就是活文档。

接下来我们来看其他的 hooks 源码,限于篇幅,主要就讲述源码,不包含测试用例、文档、story。

TS 也不会过多描述。如果对TS不太熟悉,推荐学习这个《TypeScript 入门教程》。

我们先来看 Sensors 行为部分。

  1. Sensors 行为

3.1 useIdle

useIdle docs |
useIdle demo

tracks whether user is being inactive.
跟踪用户是否处于非活动状态。

主要是:监听用户行为的事件(默认的 'mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel' ),指定时间内没有用户操作行为就是非活动状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
js复制代码import { useEffect, useState } from 'react';
// 防抖、节流
import { throttle } from 'throttle-debounce';
// 事件解绑和监听函数
import { off, on } from './misc/util';

// 监听默认事件
const defaultEvents = ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'];
const oneMinute = 60e3;

const useIdle = (
ms: number = oneMinute,
initialState: boolean = false,
events: string[] = defaultEvents
): boolean => {
const [state, setState] = useState<boolean>(initialState);

useEffect(() => {
let mounted = true;
let timeout: any;
let localState: boolean = state;
const set = (newState: boolean) => {
if (mounted) {
localState = newState;
setState(newState);
}
};

const onEvent = throttle(50, () => {
if (localState) {
set(false);
}

clearTimeout(timeout);
timeout = setTimeout(() => set(true), ms);
});
const onVisibility = () => {
if (!document.hidden) {
onEvent();
}
};

for (let i = 0; i < events.length; i++) {
on(window, events[i], onEvent);
}
on(document, 'visibilitychange', onVisibility);

timeout = setTimeout(() => set(true), ms);

return () => {
mounted = false;

// 销毁 解绑事件
for (let i = 0; i < events.length; i++) {
off(window, events[i], onEvent);
}
off(document, 'visibilitychange', onVisibility);
};
}, [ms, events]);

return state;
};

export default useIdle;

我们接着来看,useLocation hook。

3.2 useLocation

useLocation docs |
useLocation demo

React sensor hook that tracks brower’s location.

主要获取 window.location 等对象信息。

mdn History API

阮一峰老师的网道:history
阮一峰老师的网道:location

自定义事件 mdn 创建和触发 events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
ts复制代码import { useEffect, useState } from 'react';
// 判断浏览器
import { isBrowser, off, on } from './misc/util';

const patchHistoryMethod = (method) => {
const history = window.history;
const original = history[method];

history[method] = function (state) {
// 原先函数
const result = original.apply(this, arguments);
// 自定义事件 new Event 、 dispatchEvent
const event = new Event(method.toLowerCase());

(event as any).state = state;

window.dispatchEvent(event);

return result;
};
};

if (isBrowser) {
patchHistoryMethod('pushState');
patchHistoryMethod('replaceState');
}
// 省略 LocationSensorState 类型

const useLocationServer = (): LocationSensorState => ({
trigger: 'load',
length: 1,
});

const buildState = (trigger: string) => {
const { state, length } = window.history;

const { hash, host, hostname, href, origin, pathname, port, protocol, search } = window.location;

return {
trigger,
state,
length,
hash,
host,
hostname,
href,
origin,
pathname,
port,
protocol,
search,
};
};

const useLocationBrowser = (): LocationSensorState => {
const [state, setState] = useState(buildState('load'));

useEffect(() => {
const onPopstate = () => setState(buildState('popstate'));
const onPushstate = () => setState(buildState('pushstate'));
const onReplacestate = () => setState(buildState('replacestate'));

on(window, 'popstate', onPopstate);
on(window, 'pushstate', onPushstate);
on(window, 'replacestate', onReplacestate);

return () => {
off(window, 'popstate', onPopstate);
off(window, 'pushstate', onPushstate);
off(window, 'replacestate', onReplacestate);
};
}, []);

return state;
};

const hasEventConstructor = typeof Event === 'function';

export default isBrowser && hasEventConstructor ? useLocationBrowser : useLocationServer;

接着我们继续来看 State 状态部分。

  1. State 状态

4.1 useFirstMountState

useFirstMountState docs | useFirstMountState demo

Returns true if component is just mounted (on first render) and false otherwise.
若组件刚刚加载(在第一次渲染时),则返回 true,否则返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码import { useRef } from 'react';

export function useFirstMountState(): boolean {
const isFirst = useRef(true);

if (isFirst.current) {
isFirst.current = false;

return true;
}

return isFirst.current;
}

4.2 usePrevious

usePrevious docs |
usePrevious demo

React state hook that returns the previous state as described in the React hooks FAQ.
保留上一次的状态。

利用 useRef 的不变性。

1
2
3
4
5
6
7
8
9
10
11
ts复制代码import { useEffect, useRef } from 'react';

export default function usePrevious<T>(state: T): T | undefined {
const ref = useRef<T>();

useEffect(() => {
ref.current = state;
});

return ref.current;
}

4.3 useSet

useSet docs |
useSet demo

React state hook that tracks a Set.

new Set 的 hooks 用法。
useSet 可以用来列表展开、收起等其他场景。
返回 [set ,{add, remove, toggle, reset, has }]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ts复制代码import { useCallback, useMemo, useState } from 'react';

export interface StableActions<K> {
add: (key: K) => void;
remove: (key: K) => void;
toggle: (key: K) => void;
reset: () => void;
}

export interface Actions<K> extends StableActions<K> {
has: (key: K) => boolean;
}

const useSet = <K>(initialSet = new Set<K>()): [Set<K>, Actions<K>] => {
const [set, setSet] = useState(initialSet);

const stableActions = useMemo<StableActions<K>>(() => {
const add = (item: K) => setSet((prevSet) => new Set([...Array.from(prevSet), item]));
const remove = (item: K) =>
setSet((prevSet) => new Set(Array.from(prevSet).filter((i) => i !== item)));
const toggle = (item: K) =>
setSet((prevSet) =>
prevSet.has(item)
? new Set(Array.from(prevSet).filter((i) => i !== item))
: new Set([...Array.from(prevSet), item])
);

return { add, remove, toggle, reset: () => setSet(initialSet) };
}, [setSet]);

const utils = {
has: useCallback((item) => set.has(item), [set]),
...stableActions,
} as Actions<K>;

return [set, utils];
};

export default useSet;

4.4 useToggle

useToggle docs |
useToggle demo

tracks state of a boolean.
跟踪布尔值的状态。
切换 false => true => false

1
2
3
4
5
6
7
8
9
10
ts复制代码import { Reducer, useReducer } from 'react';

const toggleReducer = (state: boolean, nextValue?: any) =>
typeof nextValue === 'boolean' ? nextValue : !state;

const useToggle = (initialValue: boolean): [boolean, (nextValue?: any) => void] => {
return useReducer<Reducer<boolean, any>>(toggleReducer, initialValue);
};

export default useToggle;

我们继续来看 Side-effects 副作用部分。

  1. Side-effects 副作用

5.1 useMountedState

useMountedState 属于 lifecycle 模块,但这个 hook 在 useAsyncFn 中使用,所以放到这里讲述。

useMountedState docs |
useMountedState demo

NOTE!: despite having State in its name this hook does not cause component re-render. This component designed to be used to avoid state updates on unmounted components.

注意!:尽管名称中有State,但该钩子不会导致组件重新呈现。此组件设计用于避免对未安装的组件进行状态更新。

Lifecycle hook providing ability to check component’s mount state.
Returns a function that will return true if component mounted and false otherwise.
生命周期挂钩提供了检查组件装载状态的能力。
返回一个函数,如果组件已安装,则返回true,否则返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts复制代码import { useCallback, useEffect, useRef } from 'react';

export default function useMountedState(): () => boolean {
const mountedRef = useRef<boolean>(false);
const get = useCallback(() => mountedRef.current, []);

useEffect(() => {
mountedRef.current = true;

return () => {
mountedRef.current = false;
};
}, []);

return get;
}

5.2 useAsyncFn

useAsyncFn docs |
useAsyncFn demo

React hook that returns state and a callback for an async function or a function that returns a promise. The state is of the same shape as useAsync.
为异步函数或返回promise的函数返回状态和回调的React钩子。状态与useAsync的形状相同。

看了 useMountedState hook,我们继续看 useAsyncFn 函数源码。

主要函数传入 Promise 函数 fn,然后执行函数 fn.then()。
返回 state、callback(fn.then)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ts复制代码// 省略若干代码
export default function useAsyncFn<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = [],
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
const lastCallId = useRef(0);
const isMounted = useMountedState();
const [state, set] = useState<StateFromFunctionReturningPromise<T>>(initialState);

const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
const callId = ++lastCallId.current;

if (!state.loading) {
set((prevState) => ({ ...prevState, loading: true }));
}

return fn(...args).then(
(value) => {
isMounted() && callId === lastCallId.current && set({ value, loading: false });

return value;
},
(error) => {
isMounted() && callId === lastCallId.current && set({ error, loading: false });

return error;
}
) as ReturnType<T>;
}, deps);

return [state, callback as unknown as T];
}

5.3 useAsync

useAsync docs |
useAsync demo

React hook that resolves an async function or a function that returns a promise;
解析异步函数或返回 promise 的函数的 React 钩子;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts复制代码import { DependencyList, useEffect } from 'react';
import useAsyncFn from './useAsyncFn';
import { FunctionReturningPromise } from './misc/types';

export { AsyncState, AsyncFnReturn } from './useAsyncFn';

export default function useAsync<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = []
) {
const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});

useEffect(() => {
callback();
}, [callback]);

return state;
}

5.4 useAsyncRetry

useAsyncRetry docs |
useAsyncRetry demo

Uses useAsync with an additional retry method to easily retry/refresh the async function;
重试

主要就是变更依赖,次数(attempt),变更时会执行 useAsync 的 fn 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ts复制代码import { DependencyList, useCallback, useState } from 'react';
import useAsync, { AsyncState } from './useAsync';

export type AsyncStateRetry<T> = AsyncState<T> & {
retry(): void;
};

const useAsyncRetry = <T>(fn: () => Promise<T>, deps: DependencyList = []) => {
const [attempt, setAttempt] = useState<number>(0);
const state = useAsync(fn, [...deps, attempt]);

const stateLoading = state.loading;
const retry = useCallback(() => {
// 省略开发环境警告提示

setAttempt((currentAttempt) => currentAttempt + 1);
}, [...deps, stateLoading]);

return { ...state, retry };
};

export default useAsyncRetry;

5.5 useTimeoutFn

useTimeoutFn 属于 Animations 模块,但这个 hook 在 useDebounce 中使用,所以放到这里讲述。

useTimeoutFn docs | useTimeoutFn demo

Calls given function after specified amount of milliseconds.
在指定的毫秒数后调用给定的函数。

主要是 useRef 和 setTimeout 结合实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ts复制代码import { useCallback, useEffect, useRef } from 'react';

export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];

export default function useTimeoutFn(fn: Function, ms: number = 0): UseTimeoutFnReturn {
const ready = useRef<boolean | null>(false);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const callback = useRef(fn);

const isReady = useCallback(() => ready.current, []);

const set = useCallback(() => {
ready.current = false;
timeout.current && clearTimeout(timeout.current);

timeout.current = setTimeout(() => {
ready.current = true;
callback.current();
}, ms);
}, [ms]);

const clear = useCallback(() => {
ready.current = null;
timeout.current && clearTimeout(timeout.current);
}, []);

// update ref when function changes
useEffect(() => {
callback.current = fn;
}, [fn]);

// set on mount, clear on unmount
useEffect(() => {
set();

return clear;
}, [ms]);

return [isReady, clear, set];
}

5.6 useDebounce

useDebounce docs |
useDebounce demo

React hook that delays invoking a function until after wait milliseconds have elapsed since the last time the debounced function was invoked.
防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts复制代码import { DependencyList, useEffect } from 'react';
import useTimeoutFn from './useTimeoutFn';

export type UseDebounceReturn = [() => boolean | null, () => void];

export default function useDebounce(
fn: Function,
ms: number = 0,
deps: DependencyList = []
): UseDebounceReturn {
const [isReady, cancel, reset] = useTimeoutFn(fn, ms);

useEffect(reset, deps);

return [isReady, cancel];
}

5.7 useThrottle

useThrottle docs |
useThrottle demo

React hooks that throttle.
节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ts复制代码import { useEffect, useRef, useState } from 'react';
import useUnmount from './useUnmount';

const useThrottle = <T>(value: T, ms: number = 200) => {
const [state, setState] = useState<T>(value);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const nextValue = useRef(null) as any;
const hasNextValue = useRef(0) as any;

useEffect(() => {
if (!timeout.current) {
setState(value);
const timeoutCallback = () => {
if (hasNextValue.current) {
hasNextValue.current = false;
setState(nextValue.current);
timeout.current = setTimeout(timeoutCallback, ms);
} else {
timeout.current = undefined;
}
};
timeout.current = setTimeout(timeoutCallback, ms);
} else {
nextValue.current = value;
hasNextValue.current = true;
}
}, [value]);

useUnmount(() => {
timeout.current && clearTimeout(timeout.current);
});

return state;
};

export default useThrottle;

我们继续来看 UI 用户界面部分。

  1. UI 用户界面

6.1 useFullscreen

useFullscreen docs |
useFullscreen demo

Display an element full-screen, optional fallback for fullscreen video on iOS.
实现全屏

主要使用 screenfull npm 包实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
ts复制代码import { RefObject, useState } from 'react';
import screenfull from 'screenfull';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import { noop, off, on } from './misc/util';

export interface FullScreenOptions {
video?: RefObject<
HTMLVideoElement & { webkitEnterFullscreen?: () => void; webkitExitFullscreen?: () => void }
>;
onClose?: (error?: Error) => void;
}

const useFullscreen = (
ref: RefObject<Element>,
enabled: boolean,
options: FullScreenOptions = {}
): boolean => {
const { video, onClose = noop } = options;
const [isFullscreen, setIsFullscreen] = useState(enabled);

useIsomorphicLayoutEffect(() => {
if (!enabled) {
return;
}
if (!ref.current) {
return;
}

const onWebkitEndFullscreen = () => {
if (video?.current) {
off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
}
onClose();
};

const onChange = () => {
if (screenfull.isEnabled) {
const isScreenfullFullscreen = screenfull.isFullscreen;
setIsFullscreen(isScreenfullFullscreen);
if (!isScreenfullFullscreen) {
onClose();
}
}
};

if (screenfull.isEnabled) {
try {
screenfull.request(ref.current);
setIsFullscreen(true);
} catch (error) {
onClose(error);
setIsFullscreen(false);
}
screenfull.on('change', onChange);
} else if (video && video.current && video.current.webkitEnterFullscreen) {
video.current.webkitEnterFullscreen();
on(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
setIsFullscreen(true);
} else {
onClose();
setIsFullscreen(false);
}

return () => {
setIsFullscreen(false);
if (screenfull.isEnabled) {
try {
screenfull.off('change', onChange);
screenfull.exit();
} catch {}
} else if (video && video.current && video.current.webkitExitFullscreen) {
off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
video.current.webkitExitFullscreen();
}
};
}, [enabled, video, ref]);

return isFullscreen;
};

export default useFullscreen;

我们继续来看 Lifecycles 生命周期部分。

  1. Lifecycles 生命周期

7.1 useLifecycles

useLifecycles docs |
useLifecycles demo

React lifecycle hook that call mount and unmount callbacks, when component is mounted and un-mounted, respectively.
React 生命周期挂钩,分别在组件安装和卸载时调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts复制代码import { useEffect } from 'react';

const useLifecycles = (mount, unmount?) => {
useEffect(() => {
if (mount) {
mount();
}
return () => {
if (unmount) {
unmount();
}
};
}, []);
};

export default useLifecycles;

7.2 useCustomCompareEffect

useCustomCompareEffect docs |
useCustomCompareEffect demo

A modified useEffect hook that accepts a comparator which is used for comparison on dependencies instead of reference equality.
一个经过修改的useEffect钩子,它接受一个比较器,该比较器用于对依赖项进行比较,而不是对引用相等进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ts复制代码import { DependencyList, EffectCallback, useEffect, useRef } from 'react';

const isPrimitive = (val: any) => val !== Object(val);

type DepsEqualFnType<TDeps extends DependencyList> = (prevDeps: TDeps, nextDeps: TDeps) => boolean;

const useCustomCompareEffect = <TDeps extends DependencyList>(
effect: EffectCallback,
deps: TDeps,
depsEqual: DepsEqualFnType<TDeps>
) => {
// 省略一些开发环境的警告提示

const ref = useRef<TDeps | undefined>(undefined);

if (!ref.current || !depsEqual(deps, ref.current)) {
ref.current = deps;
}

useEffect(effect, ref.current);
};

export default useCustomCompareEffect;

7.3 useDeepCompareEffect

useDeepCompareEffect docs |
useDeepCompareEffect demo

A modified useEffect hook that is using deep comparison on its dependencies instead of reference equality.
一个修改后的 useEffect 钩子,它对其依赖项使用深度比较,而不是引用相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码import { DependencyList, EffectCallback } from 'react';
import useCustomCompareEffect from './useCustomCompareEffect';
import isDeepEqual from './misc/isDeepEqual';

const isPrimitive = (val: any) => val !== Object(val);

const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
// 省略若干开发环境的警告提示

useCustomCompareEffect(effect, deps, isDeepEqual);
};

export default useDeepCompareEffect;

最后,我们来看 Animations 生命周期部分。

  1. Animations 动画

8.1 useUpdate

useUpdate docs |
useUpdate demo

React utility hook that returns a function that forces component to re-render when called.
React 实用程序钩子返回一个函数,该函数在调用时强制组件重新渲染。

主要用了 useReducer 每次调用 updateReducer 方法,来达到强制组件重新渲染的目的。

1
2
3
4
5
6
7
8
9
js复制代码import { useReducer } from 'react';

const updateReducer = (num: number): number => (num + 1) % 1_000_000;

export default function useUpdate(): () => void {
const [, update] = useReducer(updateReducer, 0);

return update;
}
  1. 总结

行文至此,我们简单分析了若干 react-use 的自定义 React Hooks。想进一步学习的小伙伴,可以继续学完剩余的 hooks。还可以学习 ahooks、别人写的 ahooks 源码分析、
beautiful-react-hooks、mantine-hooks 等。

学习过程中带着问题多查阅 React 新文档 react.dev,新中文文档 zh-hans.react.dev,相信收获更大。

如果技术栈是 Vue,感兴趣的小伙伴可以学习 VueUse。

如果能看完一些 React Hooks 工具集合库的源码。相信一定能对 React Hooks 有更深的理解,自己写自定义 React Hooks 时也会更加顺利、快速。


如果看完有收获,欢迎点赞、评论、分享支持。你的支持和肯定,是我写作的动力。

最后可以持续关注我@若川。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.7k+人)第一的专栏,写有20余篇源码文章。

我倾力持续组织了一年多每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

本文正在参加「金石计划」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring源码解析-老生常谈Bean ⽣命周期 ,但这个你

发表于 2023-03-22

觉得不错请按下图操作,掘友们,哈哈哈!!!

image.png

一:简述以及目录

Bean 的⽣命周期平常可能我们没太多去了解,但是在面试的时候这也是一个老生常谈的问题。

image.png

二:呕心沥血的流程图,绝对肝货

2.1 简化版

image.png

2.2 详细版

bean的创建过程1.png

三:基础概念扫盲

2.1 什么是IOC

Spring IOC1.png

IOC全称是Inversion of Control,控制反转。它属于一种设计思想,由容器将设计好的对象交给容器控制,而非对象内部直接new。

四:执行流程分析

4.1 测试demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
csharp复制代码package com.xiao.ma.yi;

import lombok.ToString;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;


public class XiaoMaYLifeBean implements InitializingBean, BeanFactoryAware, BeanNameAware, DisposableBean {

/**
* 姓名
*/
private String name;

public XiaoMaYLifeBean() {
System.out.println("1.调用构造方法: 早上好呀 我是一只快乐的小蚂蚁!");
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
System.out.println("2.设置属性:我的名字叫"+name);
}

@Override
public void setBeanName(String s) {
System.out.println("3.调用BeanNameAware#setBeanName方法: 我要起床了!!!");
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("4.调用BeanFactoryAware#setBeanFactory方法:我穿好衣服了");
}

@Override
public void afterPropertiesSet() throws Exception {
System.out.println("6.InitializingBean#afterPropertiesSet方法:费了好大力气,找到了一块好吃的");
}

public void init() {
System.out.println("7.自定义init方法:咯吱咯吱吃东西!!");
}

@Override
public void destroy() throws Exception {
System.out.println("9.DisposableBean#destroy方法:晒晒太阳,准备回家了");
}

public void destroyMethod() {
System.out.println("10.自定义destroy方法:跑了一天好累,要休息了哦 !!!");
}

public void go(){
System.out.println("XiaoMaYLifeBean:生活不就这样吗???");
}
}

同时自定义一个处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码package com.xiao.ma.yi;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class XiaoMaYiLifePostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("5.BeanPostProcessor.postProcessBeforeInitialization方法:准备出门");
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("8.BeanPostProcessor#postProcessAfterInitialization方法:终于吃饱了!");
return bean;
}
}

XML配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--suppress ALL -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

<!--打开@Autowired等注解-->
<context:annotation-config/>
<context:component-scan base-package="com" />
<aop:aspectj-autoproxy proxy-target-class="true"/>

<bean name="myBeanPostProcessor" class="com.xiao.ma.yi.XiaoMaYiLifePostProcessor" />
<bean name="xiaoMaYLifeBean" class="com.xiao.ma.yi.XiaoMaYLifeBean"
init-method="init" destroy-method="destroyMethod">
<property name="name" value="快乐小蚂蚁" />
</bean>

</beans>

测试执行入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
import com.xiao.ma.yi.XiaoMaYLifeBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest {
public static void main(String[] args) throws Exception {
ApplicationContext context =new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
XiaoMaYLifeBean xiaoMaYLifeBean = (XiaoMaYLifeBean) context.getBean("xiaoMaYLifeBean");
xiaoMaYLifeBean.go();
((ClassPathXmlApplicationContext) context) .close();
}
}

整个执行的结果如下:

  • 1.调用构造方法: 早上好呀 我是一只快乐的小蚂蚁!
  • 2.设置属性:我的名字叫快乐小蚂蚁
  • 3.调用BeanNameAware#setBeanName方法: 我要起床了!!!
  • 4.调用BeanFactoryAware#setBeanFactory方法:我穿好衣服了
  • 5.BeanPostProcessor.postProcessBeforeInitialization方法:准备出门
  • 6.InitializingBean#afterPropertiesSet方法:费了好大力气,找到了一块好吃的
  • 7.自定义init方法:咯吱咯吱吃东西!!
  • 8.BeanPostProcessor#postProcessAfterInitialization方法:终于吃饱了!
  • XiaoMaYLifeBean:生活不就这样吗???
  • 9.DisposableBean#destroy方法:晒晒太阳,准备回家了
  • 10.自定义destroy方法:跑了一天好累,要休息了哦 !!!

是不是看到这里就很明显了,和前边Bean ⽣命周期流程图相互应。

4.2 我们能使用的扩展⽅法

通过上边我们可以以发现,在bean的整个声明周期中,我们在不同的时机可以进行拓展,基本上⼤致可以分为 4 类:

  • 实现Aware 接⼝:让 我们的Bean 能拿到容器的⼀些资源,例如实现 BeanNameAware 的 setBeanName() 设置bean的名称, 实现BeanFactoryAware 的 setBeanFactory() 设置bean的工厂;
  • 添加一些我们自定义的后处理器:进⾏⼀些前置和后置的处理,例如 BeanPostProcessor 的 postProcessBeforeInitialization() 和 postProcessAfterInitialization()在初始化前后做一些我们想要的操作;
  • ⽣命周期接⼝:定义初始化⽅法和销毁⽅法的,例如 InitializingBean 的 afterPropertiesSet(),以及 DisposableBean 的 destroy();
  • 配置⽣命周期⽅法:可以通过配置⽂件,或者configuration方式,⾃定义初始化和销毁⽅法,例如配置⽂件配置的 init() 和 destroyMethod()。

五:深入源码

5.1 代码入口

这里我们说的入口其实就是 AbstractApplicationContext#refresh 方法,这里主要列举我们bean声明周期中主要的点:

image.png

image.png

image.png

image.png

因为有一些系统bean也是通过这种方式创建,如上图,这里边有八个,所以这个地方,我们要把这些系统bean跳过去,找到我们的目标bean->XiaoMaYLifeBean.class

image.png

图中就到了我们自己定义的bean了,进入getBean() 方法。

image.png

进⼊ doGetBean(),从 getSingleton() 没有找到对象,进⼊创建 Bean 的逻辑。

image.png

开始创建XiaoMaYLifeBean 对象
image.png

进入createBean()方法:

image.png

5.2 bean的实例化

进⼊ doCreateBean() 后,我们走到 createBeanInstance() 调用处:

image.png

进入createBeanInstance() 方法:

image.png

接着进入instantiateBean() 方法:

image.png

进入instantiate() 方法:

image.png

进入BeanUtils.instantiateClass() 方法:

image.png

进入构造函数newInstance() 方法:
image.png

进入到XiaoMaYLifeBean的无参构造函数中:

image.png

到这里我们的bean就被实例化出来了,但是属性什么的还没填充,我们继续走回到:
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean中:

5.3 填充bean,属性赋值

image.png

进入populateBean()方法:

image.png

进⼊ populateBean() 后,执⾏ applyPropertyValues()

image.png
进⼊ applyPropertyValues(),执⾏ bw.setPropertyValues()

image.png

image.png

image.png

image.png

image.png

进⼊ processLocalProperty(),执⾏ ph.setValue()。
image.png

进入setValue() 方法:
image.png

进入invoke() 方法:

image.png

这时候就进入了我们自定义类的属性set方法:

image.png

到这⾥,populateBean() 就执⾏完毕,下⾯开始初始化 Bean。

5.4 初始化bean

这时候又回到了我们:AbstractAutowireCapableBeanFactory#doCreateBean,此时执行到了initializeBean()方法位置:

image.png

进入initializeBean()方法:

image.png

进入invokeAwareMethods()方法:

image.png

进入我们自己类设置beanName的方法:

image.png

回到 invokeAwareMethods():

image.png

进入我们自己类设置beanFactory的方法:

image.png

第一次回到 initializeBean(),执⾏下⾯逻辑。进入:applyBeanPostProcessorsBeforeInitialization() 方法:

image.png

找到我们的处理器:

image.png

执行我们自定义处理器的postProcessBeforeInitialization方法

image.png

image.png

第⼆次回到 initializeBean(),执⾏下⾯逻辑。

进入invokeInitMethods() 方法:

image.png

image.png

⾛进示例 XiaoMaYLifeBean 的⽅法,执⾏ afterPropertiesSet()。

image.png

返回 invokeInitMethods(),执⾏下⾯逻辑。

image.png

image.png
⾛进示例 XiaoMaYLifeBean 的⽅法,执⾏ init()。

image.png

第三次回到 initializeBean(),执⾏下⾯逻辑。

image.png

进入applyBeanPostProcessorsAfterInitialization()方法:

image.png

执行我们我们⾃⼰定义的后置处理⽅法postProcessAfterInitialization():

image.png

目前到这⾥,XiaoMaYLifeBean初始化的流程全部结束,其实主要流程都是围绕 initializeBean() 展开的。

六:bean的销毁

当 XiaoMaYLifeBean ⽣成后,后⾯就开始执⾏销毁操作,销毁的流程就⽐较简单。
入口close() 方法:
image.png

进入close() 方法:

image.png

进入doClose() 方法,看到我们标注的点:

image.png

image.png

image.png

image.png

image.png

image.png

image.png
这里就可以看到我们自己的close() 方法了:

image.png

⾛进示例 XiaoMaYLifeBean 的⽅法,执⾏ destroy()。

image.png

回到 destroy()方法,执⾏下⾯逻辑。

image.png

进入invokeCustomDestroyMethod() 方法:

image.png

image.png

结果完整出来了:

image.png

七:总要有总结

我们再回顾⼀下⼏个重要的⽅法:

  • doCreateBean():这个是创建bean⼊⼝;
  • createBeanInstance():就如方法名一样是⽤来初始化 Bean,⾥⾯会调⽤对象的构造⽅法,上边源码调试流程我们也有涉及;
  • populateBean():填充对象属性依赖,以及成员变量初始化;
  • initializeBean():这⾥⾯包含了 4 个⽅法,
    • 先执⾏ aware 的 BeanNameAware比如设置beanName、BeanFactoryAware 接⼝ 设置beanFactory;
    • 再执⾏ BeanPostProcessor bean的前置处理器接口;
    • 然后执⾏ InitializingBean 接⼝,自己实现的 init()方法;
    • 最后执⾏ BeanPostProcessor 的后置处理器接口。
  • destory():先执⾏ DisposableBean 接⼝,再执⾏配置的 destroyMethod()。

相信通过这一篇bean的生命周期,我们对Springbean的生命周期有更深入的了解,其实里边也包含了AOP,以及循环依赖的相关东西了,在这个过程中其实对于我们无论是日常Spring相关的问题,还是面试时候的一些问题,如果熟读了一遍我相信很多都会在其中找到答案的。

整个系列目录:

Spring 源码解析-从源码角度看bean的循环依赖

本文正在参加「金石计划」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring 源码解析-JYM你值得拥有,从源码角度看bea

发表于 2023-03-21

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

系列目录:
Spring源码解析-老生常谈Bean ⽣命周期 ,但这个你值得看!!!

一:概述及目录

一直想单独写一遍关于从源码角度看bean的循环依赖,因为现在网上的大部分关于循环依赖的文章都是从理论的角度在讲述,属于一些比较硬背的八股文,我以前也看过,背下来了可能面试的时候还好,能够简单的说下具体流程,但是如果过了很长时间或者面试问的比较深,就gg了。所以抽这个周末下了Spring源码,想记录下源码角度是怎么解决Spring 循环依赖的。后续还会有关于Spring AOP相关的章节。

image.png

二:bean的创建过程

实际上在bean的创建过程中已经包含了循环依赖的相应知识点,话不多少,直接上肝货哦,博主趁着周末时间基于源码简要整理出来了一个关于bean创建的大致流程图。

bean的创建过程1.png

三:什么是循环依赖,什么是三级缓存,那又是怎么执行的?

3.1 什么是循环依赖

循环依赖:大白话的说就是一个或多个对象实例之间存在直接或间接的互相依赖关系,这种依赖关系构成了构成一个环形调用。

  • 第一种情况:自依赖情况

image.png

  • 第二种情况:两个对象直接的相互依赖

image.png

  • 第三种情况:多个对象直接的间接依赖

image.png

前面两种情况是直接循环依赖看起来就比较直观,而第三种属于间接循环依赖的情况有时候因为业务代码调用层级很深,很难排查出来。

3.2 循环依赖的主要场景

image.png

3.3 三级缓存解释

  • 第一级缓存:单例池 singletonObjects,它用来存放经过完整Bean生命周期过程的单例Bean对象,此时bean的已经完成实例化、注⼊、初始化完成。
  • 第二级缓存:earlySingletonObjects,它用来保存那些没有经过完整Bean生命周期的单例Bean对象,用来保证不完整的bean也是单例,简单来说就是刚实例化完成的bean,未初始化的 bean 对象。
  • 第三级缓存:singletonFactories,它保存的就是一个lambda表达式,它主要的作用就是bean出现循环依赖后,某一个bean到底会不会进行AOP操作,也就是我们说的bean工厂,存放ObjectFactory 对象在需要的时候创建代理对象。

image.png

3.4 代码维度展示

1
2
3
4
5
6
7
8
9
typescript复制代码@Service
public class XiaoMaYi1 {

@Autowired
private XiaoMaYi1 xiaoMaYi1;

public void test1() {
}
}
1
2
3
4
5
6
7
8
9
typescript复制代码@Service
public class XiaoMaYi2 {

@Autowired
private XiaoMaYi1 xiaoMaYi1;

public void test2() {
}
}

拿这个循环依赖,它能正常运⾏,为什么能够正常运行,后⾯我们会通过源码的⻆度,解读整体的执⾏流程。

下边还有一个例子:

1
2
3
4
5
typescript复制代码
@Service
public class A {
public A(B b) { }
}
1
2
3
4
5
6
typescript复制代码
@Service
public class B {
public B(C c) {
}
}
1
2
3
4
5
typescript复制代码
@Service
public class C {
public C(A a) { }
}

结果启动发现如下:

image.png

这就是上边所说的构造器注入的方式,Spring没有帮我们解决。

3.5 模块解读

image.png

3.6 相应执行流程

image.png

源码流程:

image.png

    1. 大概流程 先去获取 A 的 Bean,发现没有就准备去创建⼀个,然后将 A 的代理⼯⼚放⼊“三级缓存”(这个A 其实是⼀个半成品,只有实例化还没有对⾥⾯的属性进⾏注⼊),此时发现 A 依赖 B 的创建,就必须先去创建 B;
    1. 这时候准备创建 B,发现 B ⼜依赖 A,需要先去创建 A;
    1. B中创建A的过,因为在前边第⼀层已经创建了 A 的代理⼯⼚,直接从“三级缓存”中拿到 A 的代理⼯⼚,获取 A 的代理对象,放⼊“⼆级缓存”,并清除“三级缓存”;
    1. 回到第 2,现在有了 A 的代理对象,对 A 的依赖完美解决(这⾥的 A 仍然是个半成品),B 初始化成功;
    1. 回到1,现在 B 初始化成功,完成 A 对象的属性注⼊,然后再填充 A 的其它属性,以及 A 的其它步骤(包括 AOP),完成对 A 完整的初始化功能(这⾥的 A 才是完整的 Bean)。
    1. 将 A 放⼊“⼀级缓存”。

四:相应源码解读

本示例:Spring 的版本是 5.2.x.RELEASE。

4.1 代码入口解析

image.png

refresh方法是Spring的核心方法,可以慢慢品下哦!!!

image.png

这里过滤掉一些系统的bean,只关心我们创建的XiaoMaYi相关的bean

image.png

这里就可以看到我们创建的xiaomayi的bean了

image.png

4.2 一级缓存

image.png

进⼊ doGetBean(),从 getSingleton() 实际上也是预先处理处理好的缓存中 没有找到对象,进⼊创建 Bean 的逻辑。

image.png

image.png

我们进入createBean方法找到doCreateBean:

image.png

然后我们就会发现调用了 addSingletonFactory 方法,然后在这里还会获取bean的早期引用getEarlyBeanReference

image.png

将xiaomayi1 工厂对象塞⼊三级缓存 singletonFactories中。

image.png

image.png

进⼊到 populateBean()放置中,然后看执⾏ postProcessProperties(),这⾥是⼀个策略模式,找到下图的策略对象。

image.png

image.png

image.png

下⾯则都是为获取 xiaoMaYi1 的成员对象,然后进⾏属性注⼊。

image.png

image.png

image.png

image.png

image.png

进入doResolveDependency这个方法后就是真正找xiaomayi1 依赖的属性xiaomayi2了。

image.png

image.png

image.png

从beanFactory中正式获取 xiaoMaYi2 的 bean。

image.png

到这⾥,一级缓存已经结束了,doGetBean 层层嵌套被调用了好多次,有种递归算法的感觉. 因为xiaomqyi1依赖 xiaomayi2,下⾯我们进⼊二级缓存处理的逻辑。

4.3 二级缓存

image.png

圈着的就是二级缓存的依赖逻辑,其实也是先从一级缓存中获取->创建实例->提前暴露,添加到三级缓存->再添加自己依赖的属性, 这个流程,其实在这里就是xiaoMaYi2 依赖xiaoMaYi1的流程。

所以前边和xiaoMaYi1相同的流程我们就直接省略了!!!

image.png

image.png

image.png

image.png

这里正式获取 xiaoMaYi1 的 bean。

到这⾥,二级缓存已经结束了,因为 xiaoMaYi2 依赖 xiaoMaYi1,所以我们进⼊三级缓存看是怎么处理的。

4.3 三级缓存

image.png

图中圈出的为这次说明的流程!!!

获取 xiaoMaYi1 的 bean,在前边第⼀层和第⼆层中每次都会从 getSingleton() 获取依赖的bean,但是由于之前都没有初始化 xiaoMaYi1 和 xiaoMaYi2 的三级缓存,所以获取对象都是空对象。那要怎么处理呢,话不多说,我们开始下边的流程。

image.png

image.png

这里是重中之重 来到了第三层,由于第三级缓存中有了 xiaomayi1 的bean,这⾥使⽤三级缓存中的⼯⼚为 xiaomayi1 创建⼀个代理对象,塞⼊ ⼆级缓存。

image.png

image.png

这⾥就拿到了 xiaomayi1 的代理对象,解决了 xiaomayi2 的依赖关系,返回到二级缓存中。

4.4 返回二级缓存

返回返回二级缓存后,xiaomayi2 初始化结束,这里有个问题,二级缓存就结束了吗???
还有⼆级缓存的数据,啥时候会在⼀级缓存中使用了呢? 心急吃不了热豆腐,我们继续。

看这⾥,还记得在 doGetBean() 中,我们会通过 createBean() 创建⼀个 xiaomayi2 的 bean,当 xiaomayi2 的 bean 创建成功后,我们会执⾏ getSingleton(),它会对 xiaomayi2 的结果进⾏处理这个流程吗???

image.png

我们进⼊ getSingleton(),会看到下⾯这个⽅法中片段。

image.png

这个地方就是处理 xiaomayi2 的 ⼀、⼆级缓存的具体逻辑,那么怎么处理呢??? 答案就是,将⼆级缓存清除,放⼊⼀级缓存,具体看代码哦 .

image.png

4.5 返回一级缓存

同4.4,xiaomayi1 初始化完毕后,会把 xiaomayi1 的⼆级缓存清除,将对象放⼊⼀级缓存,直接上代码。

image.png

到这⾥,所有的流程结束,返回了我们想要的并且完整的 xiaomayi1 对象。

五:什么是单例池,什么是一级缓存?

singletonObjects,结构是 Map,它就是⼀个单例池, 存放已经完全创建好的Bean,那么什么叫完完全全创建好的?就是上面说的是,所有的步骤都处理完了,就是创建好的Bean。一个Bean在产的过程中是需要经历很多的步骤,在这些步骤中可能要处理@Autowired注解,又或是处理@Value ,@Resource, 保存在该缓存中的Bean所实现Aware子接口的方法已经回调完毕,自定义初始化方法已经执行完毕,也经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理等;
当需要处理的都处理完之后的Bean,就是完全创建好的Bean,这个Bean是可以用来使用的,我们平时在用的Bean其实就是创建好的,同时单例池也是一级缓存的一个别称。

六:什么是二级缓存,它的作用是什么?

二级缓存-earlySingletonObjects 结构就Map<String,ObjectFactory<>>,是用来存放早期暴露的Bean,一般只有处于循环引用状态的Bean才会被保存在该缓存中【早期的意思就是没有完全创建好,但是由于有循环依赖,就需要把这种Bean提前暴露出去】。保存在该缓存中的Bean所实现Aware子接口的方法还未回调,自定义初始化方法未执行,也未经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理。如果启用了Spring AOP,并且处于切点表达式处理范围之内,那么会被增强,即创建其代理对象。

七:什么是三级缓存,它的作用是什么?

这里三级缓存就是每个Bean对应的ObjectFactory对象也就是工厂对象,通过调用这个对象的getObject方法,就可以获取到早期暴露出去的Bean了。

八:为什么Spring要用三级缓存来解决循环依赖?

这是我们面试过程中常问的一个问题,前⾯了解了bean创建的执⾏流程和源码解读,但是没提到什么要⽤ 3 级缓存这个问题,这个地方我们解释下!!!

8.1 . 一级缓存能解决吗?

Spring一级缓存1.png

  • 第一级缓存,也就是缓存完全创建好的Bean的缓存,这个缓存肯定是需要的,因为单例的Bean只能创建一次,那么肯定需要第一级缓存存储这些对象,如果有需要,直接从第一级缓存返回。那么如果只能有二级缓存的话,就只能舍弃第二级或者第三级缓存。

8.2 . 如果没有三级缓存?

Spring三级缓存1.png

  • 如果没有三级缓存,也就是没有ObjectFactory,那么就需要往第二缓存放入早期的Bean,那么这个地方就有一个问题,二级缓存应该放什么东西呢 ? 是放没有代理的Bean还是被代理的Bean呢?
+ 1: 如果直接往二级缓存添加没有被代理的Bean,那么可能注入给其它对象的Bean跟最后最后完全生成的Bean是不一样的,因为最后生成的是代理对象,这肯定是不允许的;
+ 2: 那么如果直接往二级缓存添加一个代理Bean呢?


    - 假设没有循环依赖,提前暴露了代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错,
    - 假设没有循环依赖,使用了ObjectFactory,那么就不会提前暴露了代理对象,到最后生成的对象是什么就是什么,就不会报错,
    - 如果有循环依赖,不论怎样都会提前暴露代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错通过上面分析,如果没有循环依赖,使用ObjectFactory,就减少了提前暴露代理对象的可能性,从而减少报错的可能。
    - 那这个对象的代理⼯⼚在这里有什么作⽤呢,它的主要作⽤是存放半成品的单例 Bean,⽬的是为了“打破 循环”
    - 还有一个问题 为什么“三级缓存”不直接存半成品的 XiaoMaYi1,⽽是要存⼀个代理⼯⼚呢 ?这答案就是 AOP了,具体原因我们还是看上边的源码来说

从源码维度我们分析下三级缓存:

image.png

image.png

这里的重点是主要看这个对象⼯⼚是如何得到的,我们进⼊ getEarlyBeanReference() ⽅法。

image.png
进入下边方法

image.png

image.png

从这里就可以看到是处理相应AOP的操作了,也对应了我们上边说的为什么要三级缓存的一些点,同时从源码角度分析:

如果 XiaoMaYi1 有 AOP,就创建⼀个代理对象;
如果 XiaoMaYi1 没有 AOP,就返回原对象。

8.3 如果没有第二级缓存

Spring二级缓存1.png

如果没有第二级缓存, 也就是没有存放早期的Bean的缓存,其实肯定也不行。上面说过,ObjectFactory其实获取的对象可能是代理的对象,那么如果每次都通过ObjectFactory获取代理对象,那么每次都重新创建一个代理对象,这肯定也是不允许的。

从上面的分析,我们知道为什么不能直接使用二级缓存了吧,第三级缓存就是为了避免过早地创建代理对象,从而避免没有循环依赖过早暴露代理对象产生的问题,而第二级缓存就是防止多次创建代理对象,导致对象不同。

  • 有了二级缓存都能解决 Spring 依赖了,怎么要有三级缓存呢。其实我们在前面分析源码时也提到过,三级缓存主要是解决 Spring AOP 的特性。AOP 本身就是对方法的增强,是 ObjectFactory<?> 类型的 lambda 表达式,而 Spring 的原则又不希望将此类类型的 Bean 前置创建,所以要存放到三级缓存中处理。
  • 其实整体处理过程类似,唯独是 B 在填充属性 A 时,先查询成品缓存、再查半成品缓存,最后在看看有没有单例工程类在三级缓存中。最终获取到以后调用 getObject 方法返回代理引用或者原始引用。

至此也就解决了 Spring AOP 所带来的三级缓存问题

九:总要有总结

先回顾下本章主要知识点:三级缓存

  • ⼀级缓存:其实就是个单例池,⽤来存放已经初始化完成的单例 Bean;
  • ⼆级缓存:这个主要是为了解决 AOP,存放的是半成品的 AOP 的单例 Bean
  • 三级缓存:这里主要是解决循环依赖问题,存放的是⽣成半成品单例 Bean 的⼯⼚⽅法。

这是Spring部分循环依赖解决的方法,当然有些方式上边也说了是处理不了的,比如构造器注入的等。

还有一些其他体会:阅读源码刚开始是一个痛苦的过程,但是如果坚持下来自我感觉还是挺有意思,在这个过程中,你可以学习框架的设计模式,同时也理解我们平时用的东西底层是怎么实现的,像Spring可能我们有时候只知道项目中用到了,但是底层具体怎么实现,我们声明一个bean他是怎么生成的,我们添加了@Autowired注解为什么就直接能使用了等等。这些疑问你阅读debug下源码都会有答案,而且非常深刻。

本文正在参加「金石计划」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【干货】常见库存设计方案-各种方案对比总有一个适合你 一:背

发表于 2023-03-14

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

一:背景

某个票务系统比如12306占座,演出等, 流量最高的业务场景是在**查询座位图和锁座**环节,新的票务系统在优化后用了新的扣位占座系统,同时锁座扣位环节用新库存服务支撑,锁座&下单环节分别做预占、扣减库存操作,查询座位图由静态座位图加上实时座位图,静态座位图来自基础数据,实时座位图(预占+已占)来自新库存服务,票务库存与电商库存的区别在于电商库存只要控制加减避免超卖,而票务库存需要精确到座位,关注座位**不重卖和少卖**。

image.png

image.png

二:功能

  • 查询库存:座位图查询库存
  • 预占库存:锁座时预占库存
  • 扣减库存:出票时扣减库存
  • 释放库存:超时未支付释放&退款时释放库存
  • 预留库存:保留座位,预留,或者后边再卖

三:业务流程

image.png

  • 预占库存时机
常规电商都是在下单环节预占库存,支付成功后扣减库存,但票务在线选座有个前置环节是选座,所以预占库存可以前置到选座,而不是在下单环节,在支付成功后进行扣减库存操作。
  • 恶意预占库存
    如果有用户在开始恶意预占大量库存但不下单,导致票导致后续有大量票没有卖出?预占并不是实际扣减,后台系统会自动释放预占超过15分钟的库存,重新放出来售卖,这种在现有电商系统已经很常见了,但用户还是可以重新恶意预占,这种只能通过风控和反作弊行为来限制,具体方式有很多,用户限制,ip限制,手机PIN限制等。
  • 预占失败导致查询库存量变大
在热门场次会出现用户抢座,而抢座失败的用户会高频刷新座位图信息重新选座,导致查询库存的请求量瞬时增加3-5倍。

四: 那么最终系统设计要遵循什么原则???

  • 容忍动态座位图短暂不一致,接受最终一致性,必须保障高可用;

五 :具体设计

  • 请求量最大的是查询库存,避免查库操作;
  • 预占库存需要考虑多并发场景,防止重卖,可以用数据库唯一索引防止重卖。
  • 定时释放超时的预占库存,防止少卖;

5.1 方案一

本方案完全基于同步操作

5.1.1 预占库存

image.png

  • 预占库存不能完全靠mysql索引来防止重卖,需要用redis先做一层防重,这样可以最大保证数据库的请求量等于实际座位数;
  • 在redis出问题时导致流量全部击穿到mysql,此时需要在mysql操作上加入流控熔断,宁可部分预占失败,也要保障服务可用;
  • 先插入mysql后写入redis,防止写redis成功但插入mysql失败导致少卖;

5.1.2 扣减库存

image.png

  • 支付成功后扣减库存,修改库存状态为已占用;

5.1.3 查询库存

image.png

  • 查询库存时只查询redis里面的数据,不允许穿透到数据库出现数据;

5.1.4 超时释放库存

image.png

  • 定时任务扫描状态为预占状态且超过15分钟的库存记录;
  • reids操作失败则释放库存失败;

5.1.5 问题

  • redis和mysql出现不可用情况怎么办?
阶段 redis不可用 MySQL不可用
1 查询库存 部分影响,业务可能出现重卖 无影响
2 预占库存 部分影响,业务可能出现重卖 业务不可用
3 扣减库存 无影响,业务正常出票 业务不可用
4 释放库存 部分影响,可能出现少卖 部分影响,可能出现少卖

mysql不可用的情况是不能容忍的,会完全阻塞业务流程,所以数据库的击穿都需要有流控熔断防范措施;

redis不可用的情况确定可能出现2种业务场景,redis动态座位图数据少了导致部分重卖失败, redis动态座位图多了导致少卖,所以redis的数据准确性至关重要。

  • 重卖情况如何保障redis数据的最终一致性?
只有1和2的业务场景下redis不可用时才会出现重卖,重卖底层有数据库唯一索引做保障,一旦出现重卖数据库会抛出索引重复异常(DuplicateKeyException),只要捕捉到异常再将库存补到redis就可以避免下次重卖,数据出现一次重复后就可达到最终一致性,如果没有异常出现但缓存数据一直不一致,也不影响业务,表示该场次没有用户选此座位。
  • 少卖情况下如何保障redis数据的最终一致性
少卖会直接带来损失,如何保障不出现少卖至关重要,只有4的业务场景才会出现少卖,只要保障定时task能够重试就可以保障少卖的情况。
  • 如何处理场次售罄?
当场次所有座位都预占或扣减,场次状态需要变成售罄,在售罄状态也可以变回售卖状态,此逻辑正常由场次服务负责,但库存可以在库存出现变化时异步周知场次服务。
  • 关于查询库存的性能问题?
当前采用redis的set结构来做库存结构缓存,set的的SMEMBERS操作是一个O(N)的操作,在性能上还需要验证,秒杀最大的流量在于查动态座位图,每个场次的座位数在[200,1000]之间,SMEMBERS的性能问题会带来很大隐患,所以暂时废弃使用SMEMBERS查询动态座位图的方案。

5.2 方案二(异步操作)

本方案是基于异步操作设计
基于方案一的SMEMBERS操作性能问题,考虑到异步操作缓存库存来优化查询性能。

5.2.1 预占库存

image.png

  • 相对于方案一,预占完成后引入MQ来异步记录已售座位图,避免多线程下修改缓存的同步问题;

5.2.2 扣减库存

image.png

  • 类似方案一

5.2.3 查询库存

image.png

  • 直接redis缓存查出已售座位图数量;
  • 缓存动态座位图可能出现少卖情况,所以这里需要触发动态座位图定时更新机制,通过发送MQ异步更新。

5.2.4 取消库存

image.png

5.2.5 超时释放库存

image.png

  • 相对于方案一,引入MQ来异步释放预占位图;

5.2.6 异步更新动态库存

image.png

  • MQ通过taskId来区分不同的partition(不同任务分组);

5.2.7 异步比对缓存

image.png

  • 依赖查询库存触发定时比对动作,从redis拿到库存与数据库做比对,如果出现少卖情况发送mq删除redis中的数据。

5.2.8 问题

  • redis和mysql、MQ出现不可用情况怎么办?
阶段 redis不可用 MySQL不可用 MQ不可用
1 查询库存 部分影响,用户看到已售座位是未售状态,实际不出现重卖 无影响 无影响
2 预占库存 部分锁座流量被拒,实际不出现重卖 库存业务不可用 部分影响,用户看到已售座位是未售状态
3 扣减库存 无影响 库存业务不可用 无影响
4 超时释放库存 无影响 部分影响,可能出现少卖,重试保障 部分影响,可能出现少卖
5 取消库存 部分影响,座位图一定时间内不能售 部分影响,可能出现少卖 部分影响,可能出现少卖
  • 异步更新动态座位图,用户看到的实时座位图会有多久的延迟?
每次异步更新座位图需要做setNX → get →set 3次缓存操作,如果每次按照50ms来计算,一个普通任务有200个座位,在秒杀情况下,最后一个用户看到完整实时座位图的耗时是 200\*50ms=1s,单个场次秒杀最后一个用户抢座失败点击刷新座位图只要超过1秒就能看到准实时座位图,所以可以通过交互来一些优化,避免用户因为座位图更新不及时多次锁座失败的场景。
  • 上述4和5的情况下会出现少卖,如何避免?
可以通过在查询座位图的逻辑,每隔N分钟去校验缓存数据和数据库数据是否一致,只校验少卖的数据,出现缓存中有而数据库没有的座位(少卖),可以发送到MQ移除缓存数据,来释放座位,为什么在查询座位图逻辑重触发?因为没人查询座位图就不会出现少卖的情况。
  • MQ如何保证 add 和 delete 操作的顺序性?
用户下单预占后,取消订单,预占库存和释放库存间隔较短,add和delete操作通过mafka异步同步到动态座位图缓存,无法保证操作顺序性,会有两种情况:1、先add再delete,正确,无影响;2、先delete再add,错误,会导致少卖,因add操作后就无法释放。这种情况通过定时更新机制来做。
  • 满座如何做?
在创建对应任务时写入库存总量,每次出票时去修改库存量,当库存为0时主动发送MQ通知到管理系统,提供查询库存余量的接口。

5.3 方案三

本方案基于 异步+MQ
方案二是通过步骤7异步比对来达到缓存和数据库最终一致,从而防止少卖,但整个流程过于复杂,库存的各个步骤之间耦合很严重,不利于系统维护,方案二中会出现少卖情况都是因为预占库存没有释放,而已售库存不会导致少卖,所以是不是可以把缓存分为预占库存和已售库存,缓存的预占库存可以定时失效,从而保证数据定时刷新达到最终一致性。

  • redis存入2个key,value分别是已售座位和预占座位信息
  • 缓存预占座位信息设置N分钟的过期时间。

5.3.1 预占库存

image.png

  • 相对于方案二,插入数据库前先从缓存获取已售座位,判断座位是否已售,减少数据库的压力;
  • 发送MQ异步修改缓存中的预占库存;

5.3.2 扣减库存

image.png

  • 同步修改缓存的已售库存,修改已售库存失败需要上游做轮训保障;

5.3.3 查询库存

image.png

  • 查询缓存中的已售库存+预占库存;

5.3.4 取消库存

image.png

  • 发送MQ异步更新缓存中的预占库存

5.3.5 超时释放库存

image.png

5.3.6 MQ异步更新预占库存

image.png

  • 同方案二,预占库存设置了N分钟超时时间,每次更新做一次N分钟续签;

5.3.7 问题

  • 预占缓存失效了怎么办?

1、如果是热门任务预占库存的频率会很高,而MQ异步更新预占缓存会做续签操作,可以避免预售场次缓存失效导致大量因动态座位图显示不准确锁座失败的情况;

2、在低峰区,如果用户A预占了场次,N分钟没操作,同时N分钟内也无其他用户预占库存导致释放了库存,此时B预占相同座位会出现预占失败的情况,这种情况刷新座位后该座位就会变成预占状态,所以低峰期会出现小概率的锁座失败。

  • 预占缓存N设置几分钟合适?

1、正常预占座位有效时长是15分钟,比如12306的扣位时效是15分钟,如果N设置成15分钟最合理,但要考虑开始抢票前前15分钟会出现少卖case或恶意预占的情况导致场次真实少卖,所以建议N设置越小越好,但设置太小就会导致虚假重卖(用户锁座失败)的情况,伤害用户体验,需要在2者之间权衡,可以根据具体场景摸索设置。

  • 如何保障缓存已售座位和数据库最终一致?

1、在退票和扣减库存操作时,保证redis的操作是同步的,操作redis失败就返回扣减、退票失败,由上游系统做重试保证数据最终一致性。

  • redis和mysql出现不可用情况怎么办?
接口/功能 redis不可用 MySQL不可用
接口/功能 redis不可用 MySQL不可用
1 queryStockByTask/查询动态座位图 接口不可用,c端展示静态座位图; 无影响
queryStockDetail/下单前查询库存座位信息 直接查询db(考虑主从问题) 缓存可用情况下无影响;缓存不可用时,业务不可用
2 lockStock/预占库存 重卖问题由db索引保证;(注意db限流) 业务不可用
3 unlockStock/解锁预占库存
4 submitStock/扣减库存 现状:获取分布式锁失败;预期:由db保证,业务无影响 业务不可用
5 releaseStock/释放库存 不可用期间:由db保证购票流程正常短暂不可用:部分影响,缓存未及时更新出现少卖情况 接口不可用,恢复后会有少卖情况
6 keepStock/保留库存 无影响 业务不可用
7 cancelKeepStock/取消保留 部分影响,少卖情况 不可用期间:业务不可用恢复后,业务恢复正常,无少卖与重卖

【干货】使用Canal 解决数据同步场景中的疑难杂症!!!

JYM来一篇消息队列-Kafaka的干货吧!!!

设计模式:

JYM 设计模式系列- 单例模式,适配器模式,让你的代码更优雅!!!

JYM 设计模式系列- 责任链模式,装饰模式,让你的代码更优雅!!!

JYM 设计模式系列- 策略模式,模板方法模式,让你的代码更优雅!!!

JYM 设计模式系列-工厂模式,让你的代码更优雅!!!

Spring相关:

Spring源码解析-老生常谈Bean ⽣命周期 ,但这个你值得看!!!

Spring 源码解析-JYM你值得拥有,从源码角度看bean的循环依赖!!!

Spring源码解析-Spring 事务

本文正在参加「金石计划」

转载请注明来源,否则追究法律责任

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring源码解析-Spring 事务 一:概述及目录 二

发表于 2023-03-09

觉得不错请按下图操作,掘友们,哈哈哈!!!

image.png

一:概述及目录

这个是源码系列的第 4 篇,感觉不错的小伙伴请一键三连哦!!!

这篇源码解析,和 Spring AOP 中的知识有很多重合的地⽅,但是⽐ AOP 要稍微简单⼀些,建议两篇⽂章对⽐学 习。

下⾯我会简单介绍⼀下 Spring 事务的基础知识,以及使⽤⽅法,然后直接对源码进⾏拆解。

目录:

image.png

二. 项⽬准备

下⾯是 DB 数据和 DB 操作接⼝:

Id uname usex
1 小王 男
2 小李 女
1 小赵 男
1
2
3
4
5
6
arduino复制代码@Data
public class MyUser {
private int id;
private String uname;
private String usex;
}
1
2
3
4
5
6
sql复制代码public interface UserDao {
// select * from user_test where id = "#{id}"
MyUser selectUserById(Integer uid);
// update user_test set uname =#{uname},usex = #{usex} where id = #{id}
int updateUser(MyUser user);
}

基础测试代码,testSuccess() 是事务⽣效的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Service
public class Model {
@Autowired
private UserDao userDao;
public void update(Integer id) {
MyUser user = new MyUser();
user.setId(id);
user.setUname("张三-testing");
user.setUsex("⼥");
userDao.updateUser(user);
}
public MyUser query(Integer id) {
MyUser user = userDao.selectUserById(id);
return user;
}
// 正常情况
@Transactional(rollbackFor = Exception.class)
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("事务⽣效");
}
}

执⾏⼊⼝:

1
2
3
4
5
6
7
8
9
ini复制代码public class SpringMyBatisTest {
public static void main(String[] args) throws Exception {
String xmlPath = "applicationContext.xml";
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext(xmlPath);
Model uc = (Model) applicationContext.getBean("model");
uc.testSuccess();
}
}

输出:

image.png

三:Spring 事务⼯作流程

为了⽅便⼤家能更好看懂后⾯的源码,我先整体介绍⼀下源码的执⾏流程,让⼤家有⼀个整体的认识,否则容易被 绕进去。

整个 Spring 事务源码,其实分为 2 块,我们会结合上⾯的示例,给⼤家进⾏讲解。

image.png

第⼀块是后置处理,我们在创建 Model Bean 的后置处理器中,⾥⾯会做两件事情:

获取 Model 的切⾯⽅法:⾸先会拿到所有的切⾯信息,和 Model 的所有⽅法进⾏匹配,然后找到 Model 所有需 要进⾏事务处理的⽅法,匹配成功的⽅法,还需要将事务属性保存到缓存 attributeCache 中。

创建 AOP 代理对象:结合 Model 需要进⾏ AOP 的⽅法,选择 Cglib 或 JDK,创建 AOP 代理对象。

image.png

第⼆块是事务执⾏,整个逻辑⽐较复杂,我只选取 4 块最核⼼的逻辑,分别为从缓存拿到事务属性、创建并开启事 务、执⾏业务逻辑、提交或者回滚事务。

四. 源码解读

注意:Spring 的版本是 5.2.15.RELEASE,否则和我的代码不⼀样!!!

上⾯的知识都不难,下⾯才是我们的重头戏,让我们一起⾛⼀遍代码流程。

4.1 代码⼊⼝

image.png

image.png

这⾥需要多跑⼏次,把前⾯的 beanName 跳过去,只看 model。

image.png

image.png

进⼊ doGetBean(),进⼊创建 Bean 的逻辑。

image.png

进⼊ createBean(),调⽤ doCreateBean()。

image.png

进⼊ doCreateBean(),调⽤ initializeBean()。

image.png

image.png

image.png

image.png

如果看过我前⾯⼏期系列源码的同学,对这个⼊⼝应该会⾮常熟悉,其实就是⽤来创建代理对象。

4.2 创建代理对象

image.png

这⾥是重点!敲⿊板!!!

    1. 先获取 model 类的所有切⾯列表;
    1. 创建⼀个 AOP 的代理对象。

image.png

4.2.1 获取切⾯列表

image.png

这⾥有 2 个重要的⽅法,先执⾏ findCandidateAdvisors(),待会我们还会再返回 findEligibleAdvisors()。

image.png

image.png

image.png

依次返回,重新来到 findEligibleAdvisors()。

image.png

image.png

image.png

image.png
进⼊ canApply(),开始匹配 model 的切⾯。

image.png

这⾥是重点!敲⿊板!!!
这⾥只会匹配到 Model.testSuccess() ⽅法,我们直接进⼊匹配逻辑。

image.png

如果匹配成功,还会把事务的属性配置信息放⼊ attributeCache 缓存。

image.png

image.png

image.png

image.png

image.png

我们依次返回到 getTransactionAttribute(),再看看放⼊缓存中的数据。

image.png

再回到该⼩节开头,我们拿到 mdoel 的切⾯信息,去创建 AOP 代理对象。

image.png

4.2.2 创建 AOP 代理对象

创建 AOP 代理对象的逻辑,在上⼀篇⽂章【Spring源码解析-Spring AOP】讲解过,我是通过 Cglib 创建,感兴趣的同学可以翻⼀下我的历史⽂章。

4.3 事务执⾏

回到业务逻辑,通过 model 的 AOP 代理对象,开始执⾏主⽅法。

image.png

因为代理对象是 Cglib ⽅式创建,所以通过 Cglib 来执⾏。

image.png

image.png

image.png

image.png

这⾥是重点!敲⿊板!!!

下⾯的代码是事务执⾏的核⼼逻辑 invokeWithinTransaction()。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
//获取我们的事务属源对象
TransactionAttributeSource tas = getTransactionAttributeSource();
//通过事务属性源对象获取到我们的事务属性信息
final TransactionAttribute txAttr = (tas != null ?
tas.getTransactionAttribute(method, targetClass) : null);
//获取我们配置的事务管理器对象
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
//从tx属性对象中获取出标注了@Transactionl的⽅法描述符
final String joinpointIdentification = methodIdentification(method,
targetClass, txAttr);
//处理声明式事务
if (txAttr == null || !(tm instanceof
CallbackPreferringPlatformTransactionManager)) {
//有没有必要创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr,
joinpointIdentification);
Object retVal;
try {
//调⽤钩⼦函数进⾏回调⽬标⽅法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//抛出异常进⾏回滚处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//清空我们的线程变量中transactionInfo的值
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
//编程式事务
else {
// 这⾥不是我们的重点,省略...
}
}

4.3.1 获取事务属性

在 invokeWithinTransaction() 中,我们找到获取事务属性的⼊⼝。

image.png

从 attributeCache 获取事务的缓存数据,缓存数据是在 “3.2.1 获取切⾯列表” 中保存的。

image.png

4.3.2 创建事务

image.png

image.png

image.png

通过 doGetTransaction() 获取事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码protected Object doGetTransaction() {
//创建⼀个数据源事务对象
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
//是否允许当前事务设置保持点
txObject.setSavepointAllowed(isNestedTransactionAllowed());
/**
* TransactionSynchronizationManager 事务同步管理器对象(该类中都是局部线程变量)
* ⽤来保存当前事务的信息,我们第⼀次从这⾥去线程变量中获取 事务连接持有器对象 通过数据源为key
去获取
* 由于第⼀次进来开始事务 我们的事务同步管理器中没有被存放.所以此时获取出来的conHolder为null
*/
ConnectionHolder conHolder =
(ConnectionHolder)
TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
//返回事务对象
return txObject;
}

通过 startTransaction() 开启事务。

image.png

下⾯是开启事务的详细逻辑,了解⼀下即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
scss复制代码protected void doBegin(Object transaction, TransactionDefinition definition) {
//强制转化事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject)
transaction;
Connection con = null;
try {
//判断事务对象没有数据库连接持有器
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//通过数据源获取⼀个数据库连接对象
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC
transaction");
}
//把我们的数据库连接包装成⼀个ConnectionHolder对象 然后设置到我们的txObject对象
中去
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
//标记当前的连接是⼀个同步事务
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
//为当前的事务设置隔离级别
Integer previousIsolationLevel =
DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
最后返回到 invokeWithinTransaction(),得到 txInfo 对象。
//关闭⾃动提交
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual
commit");
}
con.setAutoCommit(false);
}
//判断事务为只读事务
prepareTransactionalConnection(con, definition);
//设置事务激活
txObject.getConnectionHolder().setTransactionActive(true);
//设置事务超时时间
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// 绑定我们的数据源和连接到我们的同步管理器上 把数据源作为key,数据库连接作为value 设
置到线程变量中
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(),
txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
//释放数据库连接
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection

}
}

最后返回到 invokeWithinTransaction(),得到 txInfo 对象。

image.png

4.3.3 执⾏逻辑

还是在 invokeWithinTransaction() 中,开始执⾏业务逻辑。

image.png

image.png

image.png

进⼊到真正的业务逻辑。

image.png

执⾏完毕后抛出异常,依次返回,⾛后续的回滚事务逻辑。

4.3.4 回滚事务

还是在 invokeWithinTransaction() 中,进⼊回滚事务的逻辑。

image.png

执⾏回滚逻辑很简单,我们只看如何判断是否回滚。

image.png

image.png

如果抛出的异常类型,和事务定义的异常类型匹配,证明该异常需要捕获。

之所以⽤递归,不仅需要判断抛出异常的本身,还需要判断它继承的⽗类异常,满⾜任意⼀个即可捕获。

image.png

到这⾥,所有的流程结束。

五. 总要有总结

我们再⼩节⼀下,⽂章先介绍了事务的使⽤示例,以及事务的执⾏流程。

之后再剖析了事务的源码,分为 2 块:

  • 先匹配出 model 对象所有关于事务的切⾯列表,并将匹配成功的事务属性保存到缓存;
  • 从缓存取出事务属性,然后创建、启动事务,执⾏业务逻辑,最后提交或者回滚事务。

这篇⽂章,是 Spring 源码解析的第 4 篇,如果之前已经看过 AOP 的源码解析,这篇理解起来就容易很多,但是如果上来 就直接肝,可能会有一丢丢难度哦。

本文正在参加「金石计划」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…787980…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%