点击下方“IT牧场”,选择“设为星标”
来源:https://blog.csdn.net/qq_29856589/article/details/121031029
面对逐渐奋力起追的协程,RxJava还能有一战之地吗?难道只能停留在简单用用的情况下不能另RxJava的使用变得更为简便。为了让前后端的同学们能够更加易用RxJava,我将我与沈哲老哥在Kotlin进阶实战书中,我是如何利用抽象思维从搭建RxTask的抽象到实现,一步一步给同学们进行抽丝剥茧。而且RxTask现已开源欢迎使用。
https://github.com/KilleTom/RxTask
每次运用函数响应式编程写起异步时,总感觉欠缺点意思?总感觉不够简便?总感觉有些臃肿?没错同学们你需要变得更强了也需要变得变秃了,什么你不想变秃,那就赶紧躺好且听我来讲解。RxJava即好用也难用。好用在于有过多的操作符以及线程操作空间,但也难用,因为太多的操作符以及线程操作空间这就间接导致学习成本会呈曲线上升。
在开始构建我们的RxTask之前我们先试想一下这句话:“万物皆对象”。看看我们能想到啥:“能否将将大部分需要进行异步处理业务逻辑抽象看成一个抽象运算逻辑?而成功失败只是一个运算结果的输出?”
接下来请看:
在这里我们将学习如何利用抽象思维构建一个抽象的异步框架。
基本状态的构建
在进入RxTask实现前,先将异步的基本状态抽取出来。俗话说的好:“要想轮子玩得转,齿轮得通用。”
基本状态譬如抽象成如下面这张期望图:
那么结合上图我们可以这样去构建我们的抽象代码:
//利用接口抽象ITask
//利用泛型定义Task的目标
interface ITask<RESULT> {
fun start()
fun cancel()
}
当我们构建了基本的奠基石后,那我们还需点什么呢?我们是否缺少了对一个异步的状态管理呢?
状态管理的构建
譬如我们能否这样想象呢基于ITask,构建出一个抽象的ISuperTask它里面存在ITask的异步状态描述以及管理呢?如下图:
那么结合上文以及上图我们可以这样去构建ISuperTask。
abstract class ISuperTask<RESULT> : ITask<RESULT>{
//利用它去针对Task当前进行判断
protected var TASK_CURRENT_STATUS = NORMAL_STATUS
override fun start() {
if (TASK_CURRENT_STATUS == RUNNING_STATUS) {
logD("TASK already start")
return
}
TASK_CURRENT_STATUS = RUNNING_STATUS
}
override fun cancel() {
TASK_CURRENT_STATUS = CANCEL_STATUS
}
protected open fun finalResetAction(){
}
//每一个Task runnning 状态判断可能都是不一样的 抽象成一个方法
abstract fun running(): Boolean
//use this can be judge task can be running, it was throw error when task was stop
fun judgeRunningError() {
if (!running()) {
TASK_CURRENT_STATUS = STOP_STATUS
throw RxTaskRunningException(
"task was stop"
)
}
}
@Throws
fun throwCancelError() {
throw RxTaskCancelException(
"TASK Cancel"
)
}
// 定义Task状态码
companion object {
val NORMAL_STATUS = 0x00
val RUNNING_STATUS = 0x100
val CANCEL_STATUS = 0x200
val ERROR_STATUS = 0x300
val DONE_STATUS = 0x400
val STOP_STATUS = 0x500
}
protected fun logD(message: String) {
if (BuildConfig.DEBUG)
Log.d(this::class.java.simpleName, message)
}
}
运算抽象的构建
写到这里的时候,不禁有些同学就会提出疑问了,“这个ISuperTask为什么这么奇怪呀?,为什么没有产生结果的方法或者概念呢?,有了泛型RESULT但是却没有实现它的抽象方法?”面对同学来自灵魂的三连问。
容我一一解释:“有些Task针对的场景是不需要结果返回的例如定时器TimerTask只针对做一些定时性的任务”;为了更好的应对这一场场景,IEvaluation凭空出世。IEvaluation仅仅负责运算产生RESULT。ISuperTask则负责构建一些底层基类Task的奠基石。
那么我们的IEvaluation可以这样子去实现:
interface IEvaluation<RESULT> {
fun evaluationResult(): RESULT
}
既然IEvaluation、ISuperTask有了那么我们可以这样构建这样一个概念的ISuperEvaluationTask一个负责运算产生结果的的Task。
结合上图那么我们可以这样子去构建我们的ISuperEvaluationTask。
abstract class ISuperEvaluationTask<RESULT> : IEvaluation<RESULT>, ISuperTask<RESULT>() {
override fun start() {
if (TASK_CURRENT_STATUS == RUNNING_STATUS) {
logD("TASK already start")
return
}
TASK_CURRENT_STATUS = RUNNING_STATUS
}
override fun cancel() {
TASK_CURRENT_STATUS = CANCEL_STATUS
}
//继续抽象最终运算结果的动作用来给后面的子类去实现它产生自己专属的运算过程并返回对应结果
//可以是成功的结果也可以抛出运行异常
@Throws
abstract fun evaluationAction(): RESULT
protected var resultAction: ((RESULT) -> Unit)? = null
//Task外部成功后最终执行的动作
open fun successAction(resultAction: (RESULT) -> Unit)
: ISuperEvaluationTask<RESULT> {
this.resultAction = resultAction
return this
}
protected var failAction: ((Throwable) -> Unit)? = null
//Task失败后最终执行的动作
open fun failAction(failAction: (Throwable) -> Unit)
: ISuperEvaluationTask<RESULT> {
this.failAction = failAction
return this
}
// 计算生成Result
override fun evaluationResult(): RESULT {
return evaluationAction()
}
//内部错误回调
protected fun errorAction(t: Throwable) {
if (t is RxTaskCancelException) {
TASK_CURRENT_STATUS = CANCEL_STATUS
} else {
//用标记位判断当前Task是否真正处于运行状态
if (TASK_CURRENT_STATUS == RUNNING_STATUS){
failAction?.invoke(t)
}
TASK_CURRENT_STATUS = ERROR_STATUS
}
finalResetAction()
}
//结果回调
protected fun resultAction(result: RESULT) {
//用标记实现判断当前Task是否真正处于运行的状态并标记位DONE
if (TASK_CURRENT_STATUS == RUNNING_STATUS) {
resultAction?.invoke(result)
TASK_CURRENT_STATUS = DONE_STATUS
}
finalResetAction()
}
}
写到这里的时候我们的RxTask的基础抽象已经实现好了。接下来让我们磨刀霍霍迈向RxJava,利用它实现RxTask。
SingleTask
还记得我们实现的奠基石吗?现在我们就要开始动手使用RxJava于奠基石实现一个专注运算结果的Task。
在实现这个Task之前,还记得我讲解的过的Scheduler,没错这里我们就要把这个Scheduler用上了,利用Scheduler去实现线程的调度以及异步。
那么你还记得五大兄弟的Maybe吗?对了这里我们也会用到Maybe这个大兄弟,Maybe的好处在于:Maybe是一种从RxJava.2.x中出现的一种新的类型,近似的将它理解为Single与Completable的结合!利用这一个特性我们可以封装出只针对运算结果的产生以及运算过程中异常的产生这一问题。
//利用私有的构造函数避免在其他地方使用不规范创建Task
class RxSingleEvaluationTaskTask<RESULT>
private constructor(
private val runnable: (RxSingleEvaluationTaskTask<RESULT>) -> RESULT
) : ISuperEvaluationTask<RESULT>() {
private val resultTask: Maybe<RESULT>
private var disposable: Disposable? = null
init {
//利用Maybe 与 runnable 构建出一个运算的Maybe
resultTask = Maybe.create<RESULT> { emitter ->
try {
if (TASK_CURRENT_STATUS == CANCEL_STATUS) {
emitter.onError(RxTaskCancelException("Task cancel"))
}
val action = evaluationAction()
if (TASK_CURRENT_STATUS == CANCEL_STATUS) {
emitter.onError(RxTaskCancelException("Task cancel"))
}
emitter.onSuccess(action)
} catch (e: Exception) {
emitter.onError(e)
}
}
}
//这里就负责运算的动作
override fun evaluationAction(): RESULT {
if (!running())
throw RxTaskRunningException(
"Task unRunning"
)
return runnable.invoke(this)
}
override fun start() {
super.start()
disposable = resultTask
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ resultAction(it) },
{ errorAction(it) }
)
}
override fun cancel() {
TASK_CURRENT_STATUS = CANCEL_STATUS
finalResetAction()
}
override fun running(): Boolean {
val dis = disposable ?: return false
if (TASK_CURRENT_STATUS == RUNNING_STATUS) {
return !dis.isDisposed
}
return false
}
override fun finalResetAction() {
disposable?.dispose()
disposable = null
}
companion object {
fun <RESULT> createTask(
taskRunnable:
(RxSingleEvaluationTaskTask<RESULT>) -> RESULT
)
: RxSingleEvaluationTaskTask<RESULT> {
return RxSingleEvaluationTaskTask(taskRunnable)
}
}
}
我们已经把RxSingleEvaluationTaskTask实现了那么,怎么使用呢?接下来我们来看看一段请求新闻的Api业务逻辑的实现。
// 创建一个Task
val singleTask = RxSingleEvaluationTaskTask.createTask<JsonObject> {
val result = okHttpClient.newCall(createRequest(createNewUrl("top")))
.execute()
val body = result.body ?: throw RuntimeException("body null")
return@createTask Gson().fromJson(body.string(), JsonObject::class.java)
}
//点击一个叫single的button 触发这个Task真正的请求网络并回调
single.setOnClickListener {
singleTask.successAction {
Log.i("KilleTom", "$it")
}.failAction {
it.printStackTrace()
}.start()
}
看到没有通过利用RxJava去实现我们的奠基石,也可以这么简单清爽实现出一个简单易用RxTask。
ProgressTask
有没有一种这样的一个场景,需要专注于运算某一种事物并返回结果,且过程中需要将一些进度信息返回?
例如:解压超大型文件、上传文件、下载文件、升级硬件(如电脑的BIOS升级)等等都符合上诉场景。
那么利用RxJava我们怎么实现进度信息的返回呢?在RxJava中有这样一个Subject:PublishSubject;
PublishSubject:与普通的Subject不同,在订阅时并不立即触发订阅事件,而是允许我们在任意时刻手动调用onNext,onError(),onCompleted来触发事件。
利用这样一个特性我们可以实现出进度推送功能。然后在利用Maybe去做结果的运算。那么一个ProgressTask就会被实现出来。
部分核心代码:
class RxProgressEvaluationTaskTask<PROGRESS, RESULT> private constructor
(createRunnable: (RxProgressEvaluationTaskTask<PROGRESS, RESULT>) -> RESULT) :
ISuperEvaluationTask<RESULT>() {
private var createRunnable: ((RxProgressEvaluationTaskTask<PROGRESS, RESULT>) -> RESULT)? =
createRunnable
private val resultTask: Maybe<RESULT>
private var resultDisposable: Disposable? = null
private val progressTask: PublishSubject<PROGRESS> = PublishSubject.create<PROGRESS>()
private var progressDisposable: Disposable? = null
private var progressAction: ((PROGRESS) -> Unit)? = null
init {
resultTask = Maybe.create<RESULT> { emitter ->
try {
if (TASK_CURRENT_STATUS == CANCEL_STATUS) {
emitter.onError(RxTaskCancelException("Task cancel"))
}
val action = evaluationAction()
if (TASK_CURRENT_STATUS == CANCEL_STATUS) {
emitter.onError(RxTaskCancelException("Task cancel"))
}
emitter.onSuccess(action)
} catch (e: Exception) {
emitter.onError(e)
}
}
}
override fun start() {
super.start()
resultDisposable = resultTask
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ resultAction(it) },
{ errorAction(it) }
)
progressDisposable = progressTask
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
progressAction?.invoke(it)
},
{
//nothing to do by error
}
)
}
override fun evaluationAction(): RESULT {
if (!running())
throw RxTaskRunningException( "Task unRunning")
val result = runnable.invoke(this)
//throw RunningException when result by evaluation
if (!running())
throw RxTaskRunningException("Task unRunning")
return result
}
override fun cancel() {
super.cancel()
finalResetAction()
}
fun progressAction(action: (PROGRESS) -> Unit): RxProgressEvaluationTaskTask<PROGRESS, RESULT> {
progressAction = action
return this
}
fun publishProgressAction(progress: PROGRESS) {
if (running())
progressTask.onNext(progress)
}
}
同样我们利用请求新闻接口去实现这样一个场景:有多种新闻类型需要循环请求,每请求成功一次就就返回一个JsonObject,循环请求只要发生异常或者失败直接中断整个异步;
val progressTask = RxProgressEvaluationTaskTask
.createTask<JsonObject, Boolean> { task ->
val types = arrayListOf<String>("top", "shehui", "guonei")
types.forEach { value ->
val result = okHttpClient
.newCall(createRequest(createNewUrl(value)))
.execute()
val body = result.body ?: throw RuntimeException("body null")
val jsonObject = Gson().fromJson(body.string(), JsonObject::class.java)
Log.d("KilleTom", "推送新闻类型$value")
task.publishProgressAction(jsonObject)
}
return@createTask true
}
//点击按钮触发网络请求
progress.setOnClickListener {
progressTask.progressAction {
Log.i("KilleTom", "收到进度,message:$it")
}.successAction {
Log.i("KilleTom", "Done")
}.failAction {
Log.i("KilleTom", "error message:${it.message ?: "unknown"}")
}.start()
}
TimerTask
针对于一些定时的异步任务场景,还记得ISuperTask吗?ISuperTask就是这个TimerTask的基类了,当然我们还需要用到Ticker、interval;Ticker?讲到这里同学们肯定心里大有疑问了?
一个TimerTask,循环了多少定时任务以及Timer起始时间、当前的时间,如何确定?这里就运用了Ticker,利用一个Ticker将这个TimerTask的一些信息记录下来并且每次都可以由TimerTask去获取到并针对它(Ticker)去做一些业务逻辑处理。
open class TimerTick {
var startTime: Long = -1
var currentTime: Long = -1
var countTimes: Long = -1
override fun toString(): String {
return "TimerTick(startTime=$startTime, currentTime=$currentTime, currentTimes=$countTimes)"
}
}
我们的Ticker有了那么我怎么去拓展这样TimerTask里面的Ticker呢?
open class RxTimerTask
private constructor(private val timerAction: (RxTimerTask) -> Unit)
: ISuperTask<Long>() {
//通过使用 createTick 创建出相应的Ticker
protected val ticker = createTick()
//可重写Ticker的创建根据自己的需求继承RxTimerTask在实现业务逻辑需要的Ticker
open fun createTicker(): TimerTicker {
return TimerTicker()
}
//把Ticker暴露在外方便调用使用
open fun getTimeTicker(): TimerTicker {
return ticker
}
}
那么Ticker的拓展到这已经完成了,那么我们的的interval,可以这样去实现。
open class RxTimerTask
private constructor(private val timerAction: (RxTimerTask) -> Unit)
: ISuperTask<Long>() {
private var timerDisposable: Disposable? = null
private var workDelayTime: Long = workDelayDefaultTime
private var workIntervalTime: Long = workIntervalDefaultTime
private var workTimeUnit = workDefaultUnit
private var workScheduler = getDefaultWorkScheduler()
//最终启动的时候再去利用Flowable.interval创建出相应的定时器
override fun start() {
super.start()
ticker.startTime = System.currentTimeMillis()
timerDisposable =
Flowable.interval(workDelayTime, workIntervalTime, workTimeUnit)
.observeOn(workScheduler)
.subscribe(
{ times ->
if (running()) {
ticker.countTimes = times
ticker.currentTime = System.currentTimeMillis()
timerAction.invoke(this)
}
},
{ errorAction?.invoke(it) })
}
}
有了上述那一步的时候,是不是感觉我们还少了些什么?对你的直觉是对的,一个TimerTask它的间隔时间以及启动是否延迟还有工作线程难道就这样固定写了吗?对于这样的代码我们要说不,坚决拓展到底,为了简单复用,我们还可以这样子写道:
open class RxTimerTask
private constructor(private val timerAction: (RxTimerTask) -> Unit)
: ISuperTask<Long>() {
fun setDelayTime(time: Long): RxTimerTask {
if (!running()) {
workDelayTime = time
}
return this
}
fun setIntervalTime(time: Long): RxTimerTask {
if (!running()) {
workIntervalTime = time
}
return this
}
fun setTimeUnit(unit: TimeUnit): RxTimerTask {
if (!running()) {
workTimeUnit = unit
}
return this
}
fun setTaskScheduler(scheduler: Scheduler): RxTimerTask {
if (!running()) {
workScheduler = scheduler
}
return this
}
}
在未启动TimerTask之前我们利用这些方法去链式调用并且为延迟时间、间隔时间、工作线程的设置,去一一配置尽量贴合常用的业务场景。
到这我们的TimerTask基本就算大功完成了。那么剩下我们调用呢?
//获取android的网络管理
val manager = application
.getSystemService(Context.CONNECTIVITY_SERVICE)
as ConnectivityManager
val timerTask = RxTimerTask.createTask {task->
if (task.getTimeTicker().countTimes >=10){
task.cancel()
}
//利用TimerTask 监测一段时间的网络变换
if (Build.VERSION.SDK_INT >= 23) {
val network = manager.activeNetwork
if (network == null){
Log.d("KilleTom","network null connect false")
return@createTask
}
val connectInfo = manager
.getNetworkCapabilities(network)
if (connectInfo == null){
Log.d("KilleTom","connectInfo null connect false")
return@createTask
}
val isInterNet =connectInfo
.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
Log.d("KilleTom", "$isInterNet")
}
return@createTask
}.setDelayTime(0L)
.setIntervalTime(1000)
.setTaskScheduler(Schedulers.computation())
//最后点击按钮调用
timer.setOnClickListener {
timerTask.start()
}
至此RxTask的基础实现已经实现完了,那么我们该如何改进试用多平台呢?且听我缓缓描叙。
RxTask 针对 Java、android 平台进行适应
我们的之前完成的RxTask吗?由于使用以下这段代码导致了RxTask仅仅适用于Android平台。
disposable = resultTask
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ resultAction(it) },
{ errorAction(it) }
)
那么这就是导致RxTask也必须依赖了rxandroid,当我们的RxTask可以运行在Java后端上并且不需要额外依赖rxandroid的时候,我们可以这样子改进:
首先我们可以构建出一个概念:有一个抽象的对象里面包含RxTask指定工作的两个线程,具体是哪个线程由开发者决定,最后传入RxTask,由RxTask取出设置其内部线程。
//分别定义 观察者模式的 工作线程
interface RxTaskScheduler {
fun getObserveScheduler(): Scheduler
fun getSubscribeScheduler(): Scheduler
}
抽象的概念有了,那么此时又可能存在这样一个需求,在某些场景下可能需要一个全局的Manager它负责获取一个RxTaskScheduler,用作全局性质的RxTask的线程管理,那么我们可以这样子去实现它:
object RxTaskSchedulerManager {
private var rxTaskScheduler: RxTaskScheduler = RxDefaultScheduler()
//设置全局的线程
fun setLocalScheduler(rxTaskScheduler: RxTaskScheduler) {
RxTaskSchedulerManager.rxTaskScheduler = rxTaskScheduler
}
//获取全局线程
fun getLocalScheduler():RxTaskScheduler{
return rxTaskScheduler
}
}
//默认针对Java平台实现一个RxTaskScheduler
class RxDefaultScheduler : RxTaskScheduler {
override fun getObserveScheduler(): Scheduler {
return Schedulers.computation()
}
override fun getSubscribeScheduler(): Scheduler {
return Schedulers.newThread()
}
}
但我们将Scheduler抽离出来后,那么就可以针对性将rxandroid和Scheduler封装成一个针对android平台拓展出适用于RxTask一个拓展库libRxTaskAndroidExpand。
class RxAndroidDefaultScheduler : RxTaskScheduler {
override fun getObserveScheduler(): Scheduler {
return AndroidSchedulers.mainThread()
}
override fun getSubscribeScheduler(): Scheduler {
return Schedulers.newThread()
}
}
有了我们定义的RxTaskScheduler那么我们的Single、Progress就可以这样子改进来实现。
class RxSingleEvaluationTask<RESULT>
internal constructor(
private val runnable: (RxSingleEvaluationTask<RESULT>) -> RESULT,
private val taskScheduler: RxTaskScheduler
) : ISuperEvaluationTask<RESULT>() {
override fun start() {
super.start()
// 利用rxScheduler去获取工作线程
disposable = resultTask
.subscribeOn(taskScheduler.getSubscribeScheduler())
.observeOn(taskScheduler.getObserveScheduler())
.subscribe(
{ resultAction(it) },
{ errorAction(it) }
)
}
}
class RxProgressEvaluationTask<PROGRESS, RESULT>
private constructor(
private val createRunnable: (RxProgressEvaluationTask<PROGRESS, RESULT>) -> RESULT,
private val rxTaskScheduler: RxTaskScheduler) :
ISuperEvaluationTask<RESULT>() {
override fun start() {
super.start()
resultDisposable = resultTask
.subscribeOn(rxTaskScheduler.getSubscribeScheduler())
.observeOn(rxTaskScheduler.getObserveScheduler())
.subscribe(
{ resultAction(it) },
{ errorAction(it) }
)
progressDisposable = progressTask
.subscribeOn(rxTaskScheduler.getSubscribeScheduler())
.observeOn(rxTaskScheduler.getObserveScheduler())
.subscribe(
{progressAction?.invoke(it)},
{
//nothing to do by error
})
}
}
日志输出的改进
还记得我们之前实现的ISuperTask吗?里面的日志输出使用了android的方法,如下。
abstract class ISuperTask<RESULT> : ITask<RESULT>{
protected fun logD(message: String) {
if (BuildConfig.DEBUG)
Log.d(this::class.java.simpleName, message)
}
}
为了抽离日志输出更好的适配两个平台我们可以采用类似RxTaskScheduler的实现思路。
//定义一个全局的LogManager通过它获取以及设置全局的LogAction达到控制输出的具体实现
class RxTaskLogManager private constructor() {
private var logAction: RxLogAction = object : RxLogAction {
}
fun logD(iSuperTask: ISuperTask<*>, message: String) {
logAction.d(iSuperTask, message)
}
fun set(rxLogAction: RxLogAction) {
this.logAction = rxLogAction
}
companion object {
val instants by lazy { RxTaskLogManager() }
}
}
//定义logAction 负责日志输出
interface RxLogAction {
fun d(objects: ISuperTask<*>, message: String) {
System.out.println("${objects::class.java.simpleName}->Message:$message")
}
}
还记得libRxTaskAndroidExpand库吗?没错在这里我们也需要对RxLogAction进行拓展适配。
实现思路与RxTaskSceduler的拓展大体相同。
class RxAndroidDefaultLogAction private constructor(): RxLogAction {
override fun d(objects: ISuperTask<*>, message: String) {
if (BuildConfig.DEBUG) {
Log.d(objects::class.java.simpleName, message)
}
}
companion object{
val instant by lazy { RxAndroidDefaultLogAction() }
}
}
那么针对android平台,我们如何能够快速的全局初始化我们的一些RxTask的配置呢?
这里就可以使用这样的一种思维专门针对android平台进行初始化的实现:
class RxTaskAndroidDefaultInit private constructor() {
fun defaultInit() {
RxTaskSchedulerManager.setLocalScheduler(RxAndroidDefaultScheduler())
RxTaskLogManager.instants.set(RxAndroidDefaultLogAction.instant)
}
companion object {
val instant by lazy { RxTaskAndroidDefaultInit() }
}
}
通过使用RxTaskAndroidDefaultInit的单例,快速将针对android进行初始化。
通过抽象思维去分解一个异步框架的所需基本动作,如何基于一个基本动作的抽象去横纵向的拓展构建出一个异步框架。
基于抽象的构建,利用RxJava去对前后端进行适配兼容。以此达到前后端通用的效果。
RxTask可用在实际项目运用目前其地址:https://github.com/KilleTom/RxTask。本作者还会继续维护RxTask便于同学们工作上使用。
干货分享
最近将个人学习笔记整理成册,使用PDF分享。关注我,回复如下代码,即可获得百度盘地址,无套路领取!
•001:《Java并发与高并发解决方案》学习笔记;•002:《深入JVM内核——原理、诊断与优化》学习笔记;•003:《Java面试宝典》•004:《Docker开源书》•005:《Kubernetes开源书》•006:《DDD速成(领域驱动设计速成)》•007:全部•008:加技术群讨论
加个关注不迷路
喜欢就点个"在看"呗^_^