由浅入深,聊聊 LeakCanary 的那些事

news2024/11/17 16:04:23

引言

关于内存泄漏,Android 开发的小伙伴应该都再熟悉不过了,比如最常见的静态类间接持有了某个 Activity 对象,又比如某个组件库的订阅在页面销毁时没有及时清理等等,这些情况下多数时都会造成内存泄漏,从而对我们App的 流畅度 造成影响,更有甚者造成了 OOM 的情况。

在现代化开发以及多人协作的背景下,如何能做到开发中快速的监测内存泄漏,从而尽可能杜绝上述问题,此时就显得更加尤为重要。

LeakCanary 就是一个可以帮助开发者快速排查上述问题的工具,几乎所有的Android开发者都曾使用过这个工具,其背后的设计也是各厂自研相应组件的借鉴思想。

而理解 LeakCanary 背后的设计思想与原理,也更是每个应用层开发者所必不可少的技能点。

故此,本篇将以最新的视角,与你一起用力一瞥 LeakCanary

LeakCanary 版本:2.10

本篇定位 中等,将从背景到使用方式,再到源码解析,尽可能全面、易懂。

基础概念

在开始之前,我们还是要解释一些常见的基础问题,以便更好的理解本篇。🤔

什么是内存泄漏?

当我们App无法释放不需要的对象引用时,即为内存泄漏。也可以理解为,生命周期长的持有了生命周期短的对象所导致

常见内存泄漏场景?

  • 非静态内部类与匿名内部类(导致的持有外部类引用时,比如Act中的非静态Handler);
  • 异步线程持有外部 context(非AppContext)引用所导致的内存泄漏;
  • service 忘记了解绑或者广播没有解除订阅等;
  • stream 流忘记关闭;

LeakCanary 使用方式

关于 LeakCanary 的使用方式,新手小伙伴可以从 官方文档 得到更多,这里仅仅只是作为一个简单概要。

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'

LeakCanary 使用很简单,只需要在gradle中添加依赖即可,就是这么 Easy 😃

然后当我们app运行后,桌面会安装一个 名为 Leask 的软件,icon是一个小鸟的图标。

如果 app 在使用中出现内存泄漏并且达到一定数量时,其会自动弹出一个通知,提示我们进行内存泄漏分析。当点击通知后,LeakCanary 会进行泄漏堆栈分析,并将其显示到 Leask 的泄漏列表中。开发者可以通过具体的 item 从而了解相应的泄漏信息,当然也通过查看 log 日志进行分析。具体如下图所示(官方截图):

在这里插入图片描述

源码分析

这一章节,我们将从 LeakCanary 的源码出发,从而探索其背后的设计思想。

如何初始化

问起这个问题,稍有经验的开发者肯定都会猜到,既然不需要手动初始化,那肯定是 ContentProvider 了。😉

如下所示:

internal class MainProcessAppWatcherInstaller : ContentProvider() {
  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    AppWatcher.manualInstall(application)
    return true
  }
 }

其内部增加一个 ContentPrvider ,并在 onCreate() 进行初始化。

不过 LeakCanary 也提供了 JetPack-startup 的方式,如下所示:

petterp-image

在上面我们能看到,上述的初始化时会调用 AppWatcher.manualInstall(application) 方法,而我们的插入点也即从这里开始 📌

manualInstall(application)

顾名思义,用于进行初始化组件的安装。
在这里插入图片描述

上述的逻辑中,会先通过反射去给 AppWatcher.objectWatcher 进行赋值,然后安装具体的组件观察者,具体的源码分析如下所示。


appDefaultWatchers()

创建默认组件观察者列表。
petterp-image

用于初始化我们具体的观察者列表,目前是支持 ActivityFragmentViewService ,并且这些观察者都传入了 一个静态的 ReachabilityWatcher 对象 objectWatcher

ReachabilityWatcher 是干什么的呢?

中文翻译过来时 可达性观察者

简单理解就是 用于监听我们的对象是否将要立刻变为弱可达,其本身只是一个接口,具体实现类为 ObjectWatcher ,也即我们上述初始化插件时传递的对象。

这里可能不是很好理解,关于具体的逻辑,我们下面还会再进行解释,暂时先有个印象即可。 😶‍🌫️


loadLeakCanary(application)

val loadLeakCanary by lazy {
  try {
    val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
    leakCanaryListener.getDeclaredField("INSTANCE")
      .get(null) as (Application) -> Unit
  } catch (ignored: Throwable) {
    NoLeakCanary
  }
}

这里是用于初始化 InternalLeakCanary ,不过因为 InternalLeakCanary 属于上层模块,无法直接调用到,所以使用了 [反射] 去创建。

对于sdk开发者而言,这也是一个小技巧,使用反射的方式进行初始化,从而避免模块间的耦合。

InternalLeakCanary 相当于 LeakCanary 的内部具体实现,即也就是在这里进行具体的初始化工作。

我们直接去看其源码即可:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q80zttnS-1675075107665)(https://cdn.staticaly.com/gh/Petterpx/ImageRespoisty@main/img/petterp-image.5togpblwoig0.png)]

上述源码主要做了一些初始化的工作,具体的内容,我们在源码中增加了注释,具体不必过于深追。

不过对于sdk初始化部分,还是有值得我们学习的一个小地方,这里单独提出来:

在这里插入图片描述
如上所示,这是用于监听App是否处于前台,相比普通的使用Act全局监听,这里还是用了广播,并监听了 ACTION_SCREEN_ON(屏幕唤醒并正在交互) 与 ACTION_SCREEN_OFF(屏幕关闭) ,从而实现了更加严谨的判断逻辑,值得我们业务中参考。👏

LeakCanary 初始化部分到这里就结束了,相关的细节逻辑在上面都有描述,这里我们就不再做叙述。

如何检测内存泄漏

在本小节,我们将聊聊 LeakCanary 是如何做到监听 ActFragment 等内存泄漏,即具体的实现逻辑是怎样的,从而理解其设计的思想。

本小节不会涉及具体的对象是否泄漏的判断,所以更多的是框架的封装思考。

在上面的初始化的源码分析中,我们可以发现,其最终会去调用下述方法去执行各组件的监听:

  • ActivityWatcher(application, reachabilityWatcher);
  • FragmentAndViewModelWatcher(application, reachabilityWatcher);
  • RootViewWatcher(reachabilityWatcher);
  • ServiceWatcher(reachabilityWatcher);

所以我们本节的插入点就从这里开始🔺。

ActivityWatcher

用于监听Activity的观察者,具体实现如下所示:
petterp-image

如上述逻辑所示:内部注册了一个 Activity 的全局生命周期监听,从而在 onDestory() 时将 activity 的引用交给 ReachabilityWatcher 去处理判断。


FragmentAndViewModelWatcher

用于监听 FragmentViewModel 的观察者,具体源码如下:
在这里插入图片描述

上述逻辑中,我们可以发现,对于 Fragment 的可达性监听方案,其和 Act 一样,先注册 Act-Lifecycle 监听,然后在 onCreate() 时进行 Fragment-Lifecycle 注册监听,内部调用了 FragmentManager 进行生命周期监听注册。

🔺 但因为我们的 FragmentManager 实际上是有三个版本:

  • android.app.FragmentManager (Deprecated);
  • android.support.v4.app.FragmentManager;
  • androidx.fragment.app.FragmentManager;

上述版本,经历过的开发同学相必都很清楚,过往的教训,这里就不多提了。一句话,都是泪 👾 。

碍于一些历史原因,所以要针对三个版本都做一些判断处理。上述逻辑中,因为 app.FragmentManager 绑定生命周期时有限制,必须 8.0 之后才可以进行绑定,后两者则是分别判断了 AndroidXSupport

我们这里随便拎一个具体的处理代码, 以 AndroidX 为例
在这里插入图片描述

如上所示,分别在 onFragmentViewDestroyed()onFragmentDestroyed()view对象fragment对象 进行了可达性追踪。

需要注意的是,在 invoke()onFragmentCreated() 方法中,内部还对 ViewModel 进行了可达性追踪,这也是支持追踪ViewModel 内存泄漏的逻辑所在

相应的,我们在看一眼ViewModel中具体的实现思路。


ViewModelClearedWatcher

用于监听 ViewModel 是否清除的观察者,具体源码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dDUQebIw-1675075112660)(null)]

在初始化时,会调用 install() 插入一个 ViewModel ,这个 ViewModel 类似一个 [间谍] 的作用,目的是在 ViewModel 销毁 时,即 onCleard() 方法执行时,通过反射拿到 ViewModelStore 中保存的 ViewModel数组 ,从而去对每个 ViewModel 对象进行可达性追踪,从而判断是否存在内存泄漏。

结合在 Fragment 中的逻辑,所以完整的流程大致如下:

petterp-image


RootViewWatcher

用于监听 根视图 对象是否泄漏的观察者,具体源码如下:
在这里插入图片描述

初始化时创建了一个 OnRootViewAddedListener ,用于拦截所有根视图的创建,具体使用了 curtains 库实现。

当前窗口类型 是 DialogTooltipToast 或者 未知类型 时添加 View.OnAttachStateChangeListener 监听器,并初始化了一个 runable 用于执行view对象可达性追踪的回调,从而当这个View添加到窗口时,从Handler中移除该回调;在窗口移除时再添加到Handler中,从而触发view对象的可达性追踪。


ServiceWatcher

用于监听 服务 对象是否泄漏的观察者,具体源码如下:
在这里插入图片描述

上述的流程相对来说比较复杂,源码部分我们做了大量删减,具体逻辑如下:

  • ServiceWatcherinstall() 时,会通过反射的方式取出 ActivityThread 中的 mH(Handler),并使用自定义的 CallBack 替换 Handler 中原来的 mCallBack ,并缓存原来的 mCallBack ,从而做到监听 service 的停止,并且延续原 callBack 流程的继续。当 Handler 中收到的消息是 msg.what == STOP_SERVICE 时,则证明当前 service 即将停止,则将该 service 加入要追踪的服务集合中。
  • 接下来 hook ActivityManagerService ,并使用动态代理的方式去代理该 IActivityManager 对象,从而监听该对象的方法调用。如果当前调用的方法是 serviceDoneExecuting(),则证明 service 已真正结束。即从当前待追踪的服务集合中取出该 service 并对其进行可达性追踪,并从该集合中移除该service对象。

如何判定内存泄漏

本小节将要来到我们的重头戏,即如何判断一个对象是否真的内存泄漏🧐。

在上述分析中,我们不难发现,对于对象的可达性追踪,即是否内存泄漏,最终都是调用了该方法:

reachabilityWatcher.expectWeaklyReachable(view,xxx)

reachabilityWatcher 只有一个具体的实现类,即 ObjectWatcher,所以我们的插入点从这里开始🔺 ->

我们去看看相应的 expectWeaklyReachable 源码,如下所示:

ObjectWatcher.expectWeaklyReachable()

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  ...
  // 因为所有追踪的对象默认都认为是即将被销毁,即弱可达对象。
  // 这里这里再次对ReferenceQueue出现的弱引用进行移除
  removeWeaklyReachableObjects()
  // 生成一个随机的UUID
  val key = UUID.randomUUID().toString()
  // 记录当前的监测开始时间
  val watchUptimeMillis = clock.uptimeMillis()
  // 使用一个弱引用持有当前要追踪的 弱可达对象
  // 并且调用了基类的 WeakReference<Any>(referent, referenceQueue)构造器
  // 这样的话,弱引用在被回收之前会出现到 referenceQueue 中
  val reference =
    KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
  // 将该引用对象存入观察Map中
  watchedObjects[key] = reference
  
  // 延迟检测当前弱引用对象,从而判断对象是否被回收,如果没有,则证明可能存在内存泄漏
  // 默认延迟5s后执行,具体参见上述 manualInstall()
  // this.retainedDelayMillis = TimeUnit.SECONDS.toMillis(5)
  checkRetainedExecutor.execute {
    moveToRetained(key)
  }
}

  @Synchronized private fun moveToRetained(key: String) {
    // 先将引用队列中的对象从队列中删除
    removeWeaklyReachableObjects()
    // 获取指定key对应的弱引用对象
    val retainedRef = watchedObjects[key]
    // 如果当前的弱引用对象不为null,则证明可能发生了内存泄漏
    if (retainedRef != null) {
      // 记录内存泄漏时间,并通知所有对象,当前已发生内存泄漏
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }

  private fun removeWeaklyReachableObjects() {
    // 将引用队列中的对象从队列中删除
    var ref: KeyedWeakReference?
    do {
      // 如果不为null,则证明该对象已经被回收
      // 
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }

上述方法中,先调用 removeWeaklyReachableObjects() 方法 对当前的引用队列进行了清除。然后生成了 KeyedWeakReference 弱引用对象,内部持有者当前要追踪的对象,并且记录了当前的时间,key等信息。需要注意的是,这里在初始化 KeyedWeakReference 时,构造函数中还传入了 queue ,而这样的目的是为了 再进行一遍对象是否回收的check 。然后将创建好的弱引用观察对象添加到我们的观察Map中,并使用 Handler 延迟5s 后再去检测该对象是否真的被回收。

初始化 KeyedWeakReference ,为什么要传入队列 queue ?

当我们弱引用中所持有的对象被回收时,即相当于我们弱引用本身也没有用了,此时,java会将我们当前的弱引用对象,添加到我们所传递的队列(queue)中去。即我们可以通过某些逻辑去判断队列是否存在我们指定的弱引用对象,如果存在,则证明对象已经被回收,否则即存在泄漏的风险。

当5s延迟结束后,调用 moveToRetained() 方法再次去检测该对象。检测时,依然先调用 removeWeaklyReachableObjects() 将可能已经被回收的对象进行清除,避免误判。此时如果当前我们要检测的 key 所对应弱引用对象依然存在,则证明该对象没有被正常回收,可能发生了内存泄漏。此时记录内存泄漏的发生的时间,并通知所有对象

所以接下来我们去看看 onObjectRetained() 方法即可。

onObjectRetained()

InternalLeakCanary.onObjectRetained()

用于检测对象是否真的存在泄露,具体源码如下:
petterp-image

上述逻辑如下,先判断当前是否正在检查对象是否泄漏中,如果正在检查,则直接跳过,否则获得当前系统时间+需要延迟的时间(这里是0s),并在后台线程延迟指定时间后,再去检测是否泄漏。


checkRetainedObjects()

再次去检查当前仍未回收的对象,如果这次依然存在,则证明真的泄漏了,这里相当于是最终审判。

在这里插入图片描述

上述逻辑如下,我们分为三步来看:

  1. 内部会先调用 objectWatcher.retainedObjectCount 获得当前已经泄漏的对象个数;

    petterp-image

    如果你还记得我们上面 延迟 5s 再去检测对象是否泄漏的 moveToRetained() 方法,就会记得,该方法内部对 retainedUptimeMillis 字段进行了设置。

  2. 如果泄漏的数量>0,则 GC 一次后再次获取泄漏个数;

    这里的 gcTrigger.runGc() 实则是调用 GcTrigger.Default.runGc()

    petterp-image

    在系统的注释中,使用 Runtime.getRuntime().gc() 可以比 System.gc() 更容易触发;(因为java的垃圾回收更多只是通知执行,至于是否真的执行,实则是不确定的)。

    需要注意是,该方法内部在GC后还延迟了100ms ,从而以便使得虚拟机真的 GC 后,从而将弱引用移动到我们传递引用队列中去。(因为我们在初始化 KeyedWeakReference 时,内部传递了一个引用队列),这里仍然在保底check。

  3. 接着再次调用 checkRetainedCount() 判断当前泄漏的对象是否到达阈值,如果达到了,则直接 dump heap ,并发出一个内存泄漏的通知,否则则只打印一下泄漏的日志。
    在这里插入图片描述

总结

在本篇中,我们通过对于 LeakCanary 的使用方式以及应用层的实现原理做了较完整的分析,从而以一个直观的视角理解其应用层的设计思想。最后让我们我们再次去回顾一下上述整个流程:

  1. 初始化做了什么?

    因为 LeakCanary 使用了 ContentProvider,所以初始化的逻辑不需要开发者手动介入,默认在初始化的内部,其会注册App全局的生命周期监听,并且初始化了相应的监听插件,比如 对于 Activity 的 ActivityWatcher,Fragment和ViewModel的 FragmentAndViewModelWatcher 等。

  2. 各组件的内存泄漏监听方案是怎样设计的呢?

    • Activity(ActivityWatcher)

      内部注册了一个 Activity 的全局生命周期监听,从而在 onDestory() 时去追踪当前 activity 对象是否内存泄漏。

    • Fragment(FragmentAndViewModelWatcher)

      先注册 Act-Lifecycle 监听,然后在 onCreate() 时进行 Fragment-Lifecycle 注册监听,并在 onFragmentViewDestroyed()onFragmentDestroyed() 对 view对象 与 fragment对象 进行了内存泄漏追踪。

    • RootViewWatcher(RootViewWatcher)

      使用 curtains 库监听所有根 View 的创建与销毁,并初始化了一个 runable 用于监听视图是否泄漏。在当前view被添加到窗口时,则从handler中移除该handler;如果当前view从窗口移除时,则触发该runable的执行。

    • 其他组件可在具体的源码分析末尾,查看总结即可,这里就不再复述了😉

  3. 如何判定内存泄漏呢?

    对于要监听的对象,使用 KeyedWeakReference 与其进行关联(初始化时传入了一个引用队列queue),并将其保存到专门的 观察Map 中。这样当该对象被Gc回收时,就会出现在 相应的引用队列中。然后,在主线程延迟5s后去判断是否存在内存泄漏。

    在具体的判断逻辑中,会先将引用队列中出现的对象从要观察的Map中移除,从而避免误判。然后再判断当前要观察的对象是否存在,如果不存在,则说明没有内存泄漏;否则意味着可能出现了内存泄漏,则调用 Runtme.getRunTime().gc() 进行GC通知,并且等待100ms后再次执行判断,若该观察的对象仍然存在于 观察者Map 中,则证明该对象真的已经泄漏,此时就会根据内存泄漏的个数 弹出通知 或者开始 dump hprof

    至此,关于 LeakCanary 的应用层分析,到这里就结束了。

    更深层的如何生成 hprof文件 以及其解析方式,这并非本篇所要探索的方向,当然如果你也比较感兴趣,可以通过查阅其他同学的资料从而得到更加深入的理解🧐。

参阅

  • LearkCanary 文档
  • Yorkek’s - LeakCanary2源码解析

关于我

我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮助,欢迎 点赞、评论、收藏,你的支持是我持续创作的最大鼓励!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186164.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux内核-内存管理

linux内核内存管理 注意&#xff01;内核空间和用户空间都是处于虚拟空间中 Linux的虚拟地址空间范围为0&#xff5e;4G&#xff0c;Linux内核将这4G字节的空间分为两部分 内核空间&#xff1a; 最高的1G字节&#xff08;从虚拟地址0xC0000000到0xFFFFFFFF&#xff09;&…

RTSP,RTP,RTCP协议

一 RTSP 1 简介 实时流传输协议&#xff0c;是一个应用层协议&#xff08;TCP/IP网络体系中&#xff09;&#xff0c;它是一个多媒体播放控制协议&#xff0c;主要用来使用户在播放流媒体时可以像操作本地的影碟机一样进行控制&#xff0c;即可以对流媒体进行暂停/继续、后退和…

SAP FICO 关于资产的详细解析

SAP资产模块概述 一、概述 资产&#xff08;AA&#xff09;模块是资产会计模块的简称&#xff0c;是财务会计&#xff08;FI&#xff09;模块的一个子模块&#xff0c;主要处理与各类长期资产相关业务的模块。不单指固定资产&#xff0c;也不泛指资产负债表中的资产&#xff0c…

Week4

1.试题 历届真题 时间显示【第十二届】【省赛】【B组】 思路 不难发现,应该从小时往秒处理,这样可以用O(1)的时间复杂度求出,不过有比较麻烦的进位处理。 先看里面可以拼成几个小时,然后得到的小时%24,然后把总时间减去小时的时间,再看有多少分钟,分钟%60,都是此时判断分…

vue多环境配置之 .env配置文件

Vue之.env环境配置文件 .env文件是运行项目时的环境配置文件。但是在实际开发过程中&#xff0c;有本地环境、测试环境、预生产、生产环境等等&#xff0c;不同环境对应的配置会不一样。因此&#xff0c;需要通过不同的.env文件实现差异化配置。 * 文章目录Vue之.env环境配置文…

【JAVA核心知识】46:什么是零拷贝Zero-copy

零拷贝相较于传统的IO流程拥有更高的数据发送效率&#xff0c;无论是RocketMq,Kafka还是Netty等都用到了零拷贝技术&#xff0c;那究竟什么是零拷贝呢&#xff0c;零拷贝又是通过什么方式提升数据发送效率呢&#xff1f; 首先我们要明白&#xff0c;一次数据发送过程就是将磁盘…

Java基础--方法

前言&#xff1a;介绍 Java 中方法的基本语法、分类、执行并分析参数传值。 关键字&#xff1a;方法、形参、实参、返回值、实例方法、静态方法、参数传值 程序引例–为什么要「方法」 public class IntroduceOfMethod {// 入口主方法。public static void main(String[] args…

docker 高级篇

一、DockerFile 1.1、概述 dockerfile 是用来构建docker镜像的文本文件&#xff0c;是由一条条构建镜像所需的指令和参数构成的脚本。 为什么要有dockerfile呢 在基础篇我们讲过&#xff0c;比如我们下载个 ubuntu镜像里面不包含 vim、ifconfig等组件&#xff0c;这个时候 新增…

DES加密算法

DES算法原理 对称密码算法中的分组加密算法&#xff08;对应于流密码&#xff09; 密钥64位&#xff0c;56位参与运算8位校验位&#xff08;校验位为&#xff1a;8、16、24、32、40、48、56、64&#xff09; 加密原理 1. IP置换 将明文数据转化为二进制数&#xff0c;并将它…

Lnix文件权限的修改

首先我们要清楚Linux文件的权限信息 在Linux中输入ls -l 或者 ll查看文件和目录的详细信息 文件详情实例中&#xff0c;a目录的第一个属性用“d”标识这个a是一个目录。 anaconda-ks.cfg第一个属性用“-”标识他是一个文件。 在Linux文件详情的后面属性需要分为三组查看 rwx&am…

三种调用机制: 同步调用、异步调用、回调(同步/异步)

c并发编程-01-并发与并行_发如雪-ty的博客-CSDN博客 c并发编程02-什么是I/O_发如雪-ty的博客-CSDN博客 c并发编程03-I/O多路复用_发如雪-ty的博客-CSDN博客 c并发编程04-同步与异步_发如雪-ty的博客-CSDN博客_c同步和异步 c并发编程05-什么是回调函数_发如雪-ty的博客-CSDN…

Web前端:使用ReactJS构建的应用类型

使用ReactJS&#xff0c;你可以构建各种各样的应用程序&#xff0c;包括单页应用程序、渐进式web应用程序、移动应用程序、仪表板、电子商务平台、企业web应用程序以及社交媒体和消息应用程序。1.单页应用程序(spa)单页应用程序(SPA)基本上是一个网页&#xff0c;它通过使用从w…

MySQL(七):undo日志——保证事务的原子性

目录一、前言1.1 如何回滚事务1.2 事务id1.3 roll pointer 隐藏列1.4 trx_id 隐藏列二、undo日志2.1 undo日志的格式2.2 insert 对应的undo日志2.3 delete 操作对应的undo日志2.4 update操作对应的undo日志2.5 Undo页面链表2.6 undo日志写入过程2.6.1 Undo Log Header2.7 回滚段…

springboot请求参数绑定原理篇

上篇文章写了SpringBoot 参数接收只看这一篇文章就够了&#xff0c;只是写了使用方法&#xff0c;没有写为什么&#xff0c;原理是什么&#xff0c;这篇文章也是之前的预先的计划&#xff0c;稍微花点时间整理下&#xff0c;知其然知其所以然&#xff0c;才算是能彻底掌握&…

如何用IDEA创建SpringBoot项目

一、创建一个 Spring Initializr 工程 next后选择2.7.8版本&#xff0c;勾选以下几个 Web里的spring bootTemplate Engines 里的 ThymeleafSQL里的MyBatis Framework 和 Mysql Driver 然后finish完成 二、配置resources文件 2.1、 application.properties&#xff1a; #??…

Docker容器命令无权限,WEB访问403

问题背景(描述) 部署dockerWeb后&#xff0c;重启访问403,详细如下 docker容器正常运行,且开机自启 #通过如下命令开机自启 docker update --restart always 容器id但是访问web服务出现403. 进入容器后,输入命令提示如下: 解决方案 关闭selinux SELinux(Security-Enhanced…

【网络安全】记一次红队渗透实战项目

前言 【一一帮助安全学习&#xff08;网络安全面试题学习路线视频教程工具&#xff09;一一】 一、信息收集 信息收集非常重要&#xff0c;有了信息才能知道下一步该如何进行&#xff0c;接下来将用nmap来演示信息收集 1、nmap扫描存活IP 由于本项目环境是nat模式需要项目…

【Java基础】——面向对象:封装

【Java基础】——面向对象:封装一、类和对象二、类的结构&#xff1a;属性、方法、构造器1、属性2、方法2.1、方法的定义2.2、方法的重载2.3、可变个数的形参2.4、方法参数的值传递机制3、构造器3.1、构造器的特征3.2、构造器的作用&#xff1a;3.3、构造器重载三、封装与隐藏1…

细菌,真菌,病毒——感染,免疫反应以及治疗用药差异

谷禾健康 与人类密切相关的微生物 我们的世界大到浩瀚宇宙&#xff0c;小到微观下的生物分子。我们总说漫天繁星&#xff0c;其实身边微生物数量可能更多。动物、植物、真菌、细菌、病毒等&#xff0c;共同构成了丰富多彩的生命世界。 细菌、真菌、病毒是其中的三个大类&#x…

spring integration使用:消息路由

系列文章目录 …TODO spring integration开篇&#xff1a;说明 …TODO spring integration使用&#xff1a;消息路由 spring integration使用&#xff1a;消息路由系列文章目录前言消息路由的概念二、路由的分类基于内容的路由器spring integration中的实现RecipientListRoute…