Shadow插件化框架使用

news2024/11/23 9:43:33

作者:CCtomorrow

说明

最近项目想要做模块动态升级,所以了解了最近还在维护的插件化框架 Shadow .

shadow框架的官网的顶置 issue ,里面有非常多的关于框架的解析的文章。想要了解此框架,这个必看。

这里还是截取一张项目代码图。图片取自顶置的issue。

项目解读

shadow框架为了实现复杂的插件化框架本身也动态升级,做了很多复杂操作:

宿主本身只跟plugin-manager插件交互

来说一下plugin-manager插件,依赖core-manager,dynamic-manager。 core-manager:
1、插件信息的存储
2、插件信息的管理
3、 so、dex管理
4、插件包zip释放

dynamic-manager:
1、只提供最基础的 dex、 res、so 的释放的基础API,这些 API 的组合调用需要自己实现
2、只负责加载 业务插件运行需要的 loader 和runtime 插件,业务插件的加载由 loader 插件实 现

宿主和manager插件交互,是直接通过构造ApkClassLoader,加载manager插件,构造插件里面的PluginManagerImpl对象。具体可以看ManagerImplLoader类。

在构造PluginManagerImpl对象的时候,是通过调用manager插件固定类里面的固定方法com.tencent.shadow.dynamic.impl.ManagerFactoryImpl#buildManager,然后这个PluginManagerImpl最终也是我们自己实现的。

我们需要实现PluginManagerImpl,然后根据不同的意图,比如打开activity,启动service,来调用不同的core-manager,或者dynamic-manager的方法,比如安装插件、打开插件activity之类的。

总体而言,自由度比较大,但是弊端也很明显,我们自己也要做很多的工作。

调用插件类,需要通过manager插件和插件zip包里面的loader插件交互

对目前的shadow来说,宿主和manager插件在一个进程,插件和加载插件的loader插件在另一个进程。 所以目前调用插件类需要通过ipc的方式和loader插件交互。manager插件调用到loader插件之后,loader插件通过加载固定类的固定方法com.tencent.shadow.dynamic.loader.impl.CoreLoaderFactoryImpl#build,去构造ShadowPluginLoader插件加载逻辑类,我们需要在这里面去配置宿主占坑组件和插件组件的对应关系。

总体而言,自由度比较大,但是弊端也很明显,我们自己也要做很多的工作。这里例如VirtualApk框架,是根据解析插件的组件在manifest里面的配置,去自动寻找宿主合适的组件的,如果这个逻辑还得我们自己实现的话,也很麻烦。还有个问题在配置宿主占坑组件和插件里面的对应关系的时候,框架给的参数太少了,例如:

public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
    switch (pluginActivity.getClassName()) {
        /**
          * 这里配置对应的对应关系
          */
    }
    return new ComponentName(context, DEFAULT_ACTIVITY);
}

就拿这个方法来说,插件调用只传递来了一个ComponentName对象,里面有用的信息只有ClassName,我怎么根据一个ClassName去知道这个插件activity应该使用宿主的哪个占坑activity去对应呢,一个个的if else写死嘛,起码我要知道这个插件activity的启动模式,配置的主题等等参数,才能决定,所以这里设计的很不合理。可能shadow的逻辑是插件更新了,loader插件也要更新,所以写if else也没问题。

插件打包问题

shadow打包插件,对于manager插件来说就是一个单独的apk,打包之后加载即可,对于业务插件来说就麻烦了,业务插件想要加载需要有loader插件和runtime插件,难道我们每一个业务插件都需要带一个loader插件和runtime插件嘛,虽然loader插件和runtime的插件代码也确实比较小,每个业务插件有一个其实问题也不大,不过如果loader和runtime的代码都差不多的话,还是感觉不好,根据在issue里面找到的方案,shadow是使用UUID相同表示一组apk可以共用工作。这组apk里可以有一个runtime一个loader和多个插件apk。 基于此,如果我们有一些插件可以共用一组loader和runtime的话,可以只在某一个插件zip里面打包loader和runtime,其他的插件不打包,但是他们的uuid必须相同。 可以看这些issue: github.com/Tencent/Sha… 具体配置如下:

//common插件里面包含了runtime和loader
shadow {
    transform {
        //useHostContext = ['abc']
    }
    packagePlugin {
        pluginTypes {
            debug {
                loaderApkConfig = new Tuple2('plugin_loader-debug.apk', ':plugin_loader:assembleDebug')
                runtimeApkConfig = new Tuple2('plugin_runtime-debug.apk', ':plugin_runtime:assembleDebug')
                pluginApks {
                    plugin_1 {
                        //businessName相同的插件,context获取的Dir是相同的。businessName留空,表示和宿主相同业务,直接使用宿主的Dir
                        businessName = ''
                        partKey = 'plugin_common'
                        buildTask = 'assemblePluginDebug'
                        apkPath = 'plugin_common_app/build/outputs/apk/plugin/debug/plugin_common_app-plugin-debug.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        //dependsOn = ['']
                    }
                }
            }

            release {
                loaderApkConfig = new Tuple2('plugin_loader-release.apk', ':plugin_loader:assembleRelease')
                runtimeApkConfig = new Tuple2('plugin_runtime-release.apk', ':plugin_runtime:assembleRelease')
                pluginApks {
                    plugin_1 {
                        businessName = ''
                        partKey = 'plugin_common'
                        buildTask = 'assemblePluginRelease'
                        apkPath = 'plugin_common_app/build/outputs/apk/plugin/debug/plugin_common_app-plugin-release.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        //dependsOn = ['']
                    }
                }
            }
        }

        uuid = "123567"
        loaderApkProjectPath = 'plugin_loader'
        runtimeApkProjectPath = 'plugin_runtime'

        archiveSuffix = System.getenv("PluginSuffix") ?: ""
        archivePrefix = 'plugin_common'
        destinationDir = "${getRootProject().getBuildDir()}"

        version = 1
        compactVersion = [1]
        uuidNickName = "1.0.0"

    }
}

然后插件A里面如下配置:

shadow {
    transform {
        //useHostContext = ['abc']
    }
    packagePlugin {
        pluginTypes {
            debug {
                //这里不配置,最终的zip包里面就不会有loader和runtime了
                //loaderApkConfig = new Tuple2('plugin_loader-debug.apk', ':plugin_loader:assembleDebug')
                //runtimeApkConfig = new Tuple2('plugin_runtime-debug.apk', ':plugin_runtime:assembleDebug')
                pluginApks {
                    plugin_a {
                        //businessName相同的插件,context获取的Dir是相同的。businessName留空,表示和宿主相同业务,直接使用宿主的Dir
                        businessName = ''
                        partKey = 'plugin_a'
                        buildTask = 'assemblePluginDebug'
                        apkPath = 'plugina/build/outputs/apk/plugin/debug/plugina-plugin-debug.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        dependsOn = ['plugin_common']
                    }
                }
            }

            release {
                //loaderApkConfig = new Tuple2('plugin_loader-release.apk', ':plugin_loader:assembleRelease')
                //runtimeApkConfig = new Tuple2('plugin_runtime-release.apk', ':plugin_runtime:assembleRelease')
                pluginApks {
                    plugin_a {
                        businessName = ''
                        partKey = 'plugin_a'
                        buildTask = 'assemblePluginRelease'
                        apkPath = 'plugina/build/outputs/apk/plugin/debug/plugina-plugin-release.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        dependsOn = ['plugin_common']
                    }
                }
            }
        }

        uuid = "123567"
        loaderApkProjectPath = 'plugin_loader'
        runtimeApkProjectPath = 'plugin_runtime'

        archiveSuffix = System.getenv("PluginSuffix") ?: ""
        archivePrefix = 'plugina'
        destinationDir = "${getRootProject().getBuildDir()}"

        version = 1
        compactVersion = [1]
        uuidNickName = "1.0.0"

    }
}

插件依赖问题

shadow block里面的配置,可以通过hostWhiteList配置可以访问宿主的哪些类。但是还是有一些情况需要注意。

  • 插件依赖通过参数dependsOn控制,可以是多个,内容填写插件的partKey
  • 可以通过在参数hostWhiteList配置可以访问宿主的类,默认情况,插件不能访问宿主
  • 插件A dependsOn 插件B,那么插件Shadow会将插件B的ClassLoader作为插件A的parent
  • 插件A dependsOn 插件B,那么插件A配置的hostWhiteList就不起作用了,需要在插件B里面配置
  • 插件A dependsOn 插件B,目前并不支持插件A访问插件B的资源
  • 宿主要访问插件里面的类比较麻烦

具体使用

综合上面的一些描述,我们其实是可以发现,shadow插件化框架是有不少问题的,官方自己的介绍文章里面也说了一些,总体要是直接使用起来其实是很不方便的。 使用shadow,我们最看中的是实现插件化还是没用什么反射。那我们可以按照自己要求进行二次定制。

nodynamic模式

官方Demo里面其实有nodynamic的 sample 的。所谓nodynamic就是插件化框架本身不需要升级,我们直接在宿主里面加载插件。对于shadow来说,就是不需要manager插件了,把loader和runtime插件打包到宿主里面。 我们封装一个sdk给宿主使用,sdk里面直接包含loader和runtime。

首先引入依赖:
//把loader和runtime打包到宿主,不用插件框架自身的升级
//common
implementation "com.tencent.shadow.core:common:$shadow_version"
//包含core:runtime和core:load-parameters
implementation "com.tencent.shadow.core:loader:$shadow_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.32"
//承载插件的容器,runtime
implementation "com.tencent.shadow.core:activity-container:$shadow_version"
//数据库管理插件的
implementation "com.tencent.shadow.core:manager:$shadow_version"

这里之所以我们引入了manger,是因为后续封装过程使用到了manger里面的一些封装好的数据结构。

后续就是一些对shadow的loader sdk的一些封装了。这里就不展示代码了。

对gradle插件进行修改

这一节的内容假定你已经会写gradle插件了,不会的话需要先了解这方面的知识。

由于我们把loader和runtime打入宿主了,不需要之前复杂的插件信息了。但是我们仍然需要知道当前加载的插件的插件信息,没有插件信息怎么去加载呢。我们最终最少只需要如下的插件信息即可。

shadow {
    pluginInfo {
        pluginKey = 'plugina'
        version = android.defaultConfig.versionCode
        hostWhiteList = [
                "com.blankj.utilcode.util",
                "com.blankj.utilcode.constant",
        ]
        dependsOn = [
                "plugin_common_app"
        ]
    }
}

然后我们需要修改shadow的gradle插件,在构建完成插件apk之后,随即生成插件信息的json。

class ShadowPlugin : Plugin<Project> {

    ...

    override fun apply(project: Project) {
        project.afterEvaluate {
            onEachPluginVariant(project) { pluginVariant ->
                checkAaptPackageIdConfig(pluginVariant)
                val appExtension: AppExtension = project.extensions.getByType(AppExtension::class.java)

                //这里是我们新增的代码,其他代码没改
                createPluginInfoTasks(project, shadowExtension, pluginVariant)

                createGeneratePluginManifestTasks(project, appExtension, pluginVariant)
            }
        }
    }

    /**
     * 创建根据用户的配置生成插件信息的task
     */
    private fun createPluginInfoTasks(
        project: Project, shadowExtension: ShadowExtension, pluginVariant: ApplicationVariant
    ) {
        val extension = shadowExtension.pluginInfo
        if (extension.pluginKey.isNotBlank()) {
            //System.err.println("${project.name} pluginInfo===>$extension")
            pluginVariant.outputs?.all { output ->
                //因为前面已经过滤过了,所有这里基本一定是ApkVariantOutputImpl
                if (output is ApkVariantOutputImpl) {
                    //NormalDebug
                    val full = pluginVariant.name.capitalize()
                    //Normal
                    val favor = pluginVariant.flavorName.capitalize()
                    //Debug
                    val type = pluginVariant.buildType.name.capitalize()
                    //System.err.println("name=$full output=${output.outputFile.absolutePath}")
                    //assembleNormalDebug
                    val assembleTask = project.tasks.getByName("assemble$full")
                    assembleTask.doFirst { task ->
                        //直接在doFirst里面操作即可
                        //System.err.println("${task.name} doFirst")
                        //{
                        //    "partKey": "",
                        //    "apkName": "",
                        //    "version": 100,
                        //    "dependsOn": ["",""],
                        //    "hostWhiteList": ["",""]
                        //}
                        //写入outputs的config.json
                        val config = JSONObject()
                        config["pluginKey"] = extension.pluginKey
                        config["apkName"] = output.outputFile.name
                        config["version"] = extension.version
                        if (extension.dependsOn.isNotEmpty()) {
                            val dependsOnJson = JSONArray()
                            for (k in extension.dependsOn) {
                                dependsOnJson.add(k)
                            }
                            config["dependsOn"] = dependsOnJson
                        }
                        if (extension.hostWhiteList.isNotEmpty()) {
                            val hostWhiteListJson = JSONArray()
                            for (k in extension.hostWhiteList) {
                                hostWhiteListJson.add(k)
                            }
                            config["hostWhiteList"] = hostWhiteListJson
                        }
                        val file = File(output.outputFile.parentFile, "config.json")
                        //System.err.println("config json file=" + file.absolutePath)
                        project.logger.info("config json file=" + file.absolutePath)
                        val bizWriter = BufferedWriter(FileWriter(file))
                        bizWriter.write(config.toJSONString())
                        bizWriter.flush()
                        bizWriter.close()
                    }
                }
            }
        }
    }

}

当然ShadowExtension我们需要修改

open class ShadowExtension {
    var transformConfig = TransformConfig()
    fun transform(action: Action<in TransformConfig>) {
        action.execute(transformConfig)
    }

    var pluginInfo = PluginInfoConfig()
    fun pluginInfo(action: Action<in PluginInfoConfig>) {
        action.execute(pluginInfo)
    }
}

//新增PluginInfoConfig类

open class PluginInfoConfig {
    /**
     * 插件我们认为key是唯一的
     */
    var pluginKey = ""
    var apkName = ""

    /**
     * 插件的版本每次如果升级的话,表示是一个新插件
     */
    var version = -1
    var dependsOn: Array<String> = emptyArray()
    var hostWhiteList: Array<String> = emptyArray()

    constructor() {
    }

}

这样我们即在assemblePluginRelease(Debug)的时候生成了插件信息json,路径和生成apk的路径在同一个位置/build/outputs/plugin/release(debug)/config.json。

{"apkName":"plugina-plugin-debug.apk","dependsOn":["plugin_common_app"],"pluginKey":"plugina","hostWhiteList":["com.blankj.utilcode.util","com.blankj.utilcode.constant"],"version":100}

当然这里生成的插件信息是某一个插件的,如果我们需要把几个插件合并在一起去下载或者内置到host里面,我们需要写个脚本把这每个插件的config.json合并一下,变成一个数组即可,当然这个代码也很简单,这里就不放出那个脚本了。

修改CreateResourceBloc支持插件依赖插件的时候也能依赖插件的资源。

修改CreateResourceBloc即可。

object CreateResourceBloc {

    /**
     * 现在插件不能
     */
    fun create(
        archiveFilePath: String,
        hostAppContext: Context,
        loadParameters: LoadParameters,
        pluginPartsMap: MutableMap<String, PluginParts>
    ): Resources {
        ...
        if (Build.VERSION.SDK_INT > MAX_API_FOR_MIX_RESOURCES) {
            fillApplicationInfoForNewerApi(
                applicationInfo,
                hostApplicationInfo,
                archiveFilePath,
                loadParameters,
                pluginPartsMap
            )
        } else {
            fillApplicationInfoForLowerApi(
                applicationInfo,
                hostApplicationInfo,
                archiveFilePath,
                loadParameters,
                pluginPartsMap
            )
        }
        ...
    }

    private fun fillApplicationInfoForNewerApi(
        applicationInfo: ApplicationInfo,
        hostApplicationInfo: ApplicationInfo,
        pluginApkPath: String,
        loadParameters: LoadParameters,
        pluginPartsMap: MutableMap<String, PluginParts>
    ) {
        ...
        // hostSharedLibraryFiles中可能有webview通过私有api注入的webview.apk
        val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
        val paths = arrayListOf<String>()
        val dependsOn = loadParameters.dependsOn
        if (dependsOn != null && dependsOn.isNotEmpty()) {
            dependsOn.forEach {
                pluginPartsMap[it]?.apply {
                    paths.add(pluginPackageManager.archiveFilePath)
                }
            }
        }
        val otherApksAddToResources =
            if (hostSharedLibraryFiles == null)
                arrayOf(
                    *paths.toTypedArray(),
                    pluginApkPath
                )
            else
                arrayOf(
                    *hostSharedLibraryFiles,
                    *paths.toTypedArray(),
                    pluginApkPath
                )

        applicationInfo.sharedLibraryFiles = otherApksAddToResources
    }

    /**
     * API 25及以下系统,单独构造插件资源
     */
    private fun fillApplicationInfoForLowerApi(
        applicationInfo: ApplicationInfo,
        hostApplicationInfo: ApplicationInfo,
        pluginApkPath: String,
        loadParameters: LoadParameters,
        pluginPartsMap: MutableMap<String, PluginParts>
    ) {
        applicationInfo.publicSourceDir = pluginApkPath
        applicationInfo.sourceDir = pluginApkPath
        val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
        val paths = arrayListOf<String>()
        val dependsOn = loadParameters.dependsOn
        if (dependsOn != null && dependsOn.isNotEmpty()) {
            dependsOn.forEach {
                pluginPartsMap[it]?.apply {
                    paths.add(pluginPackageManager.archiveFilePath)
                }
            }
        }
        val otherApksAddToResources = if (hostSharedLibraryFiles == null) {
            arrayOf(*paths.toTypedArray())
        } else {
            arrayOf(
                *paths.toTypedArray(),
                *hostSharedLibraryFiles
            )
        }
        applicationInfo.sharedLibraryFiles = otherApksAddToResources
    }

}

改动其实不多,不过我测试下来,假如插件A依赖common插件,appcompat在common插件里面,有webview的Activity不能是AppCompatActivity。

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

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

相关文章

Drools用户手册翻译——第四章 Drools规则引擎(十二)复杂事件处理(CEP)的时间操作

甩锅声明&#xff1a;本人英语一般&#xff0c;翻译只是为了做个笔记&#xff0c;所以有翻译错误的地方&#xff0c;错就错了&#xff0c;如果你想给我纠正&#xff0c;就给我留言&#xff0c;我会改过来&#xff0c;如果懒得理我&#xff0c;就直接划过即可。 对于事件的时间…

Python数据分析实战-dataframe指定多列去重(附源码和实现效果)

实现功能 Python数据分析实战-利用df.drop_duplicates(subset[,])对dataframe指定多列去重 实现代码 import pandas as pddata{state:[1,1,2,2,1,2,2],pop:[a,b,c,d,b,c,d]} framepd.DataFrame(data)frameframe.drop_duplicates(subset[pop,state]) print(frame) 实现效果 本…

【SpringCloud】RabbitMQ基础

1.初识MQ 1.1.同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&#xff0c;…

Unity Git项目添加子模块

在 当前仓库根目录下执行命令 git submodule add https://github.com/xxx/child.git 检查仓库状态 git status 更新子库 git submodule update --remote 下拉父仓库Git并保住子库也更新 git pull --recurse-submodules 推荐使用 Githubdesktop工具 这样你可以更清楚的看到自己…

前端如何设置表单元素的默认值?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 文本框、文本域⭐ 单选按钮、复选框⭐ 对于下拉列表⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是…

全网最强,Jmeter性能测试-web接口性能压测总结(超细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 压测的目的 对于…

b站视频标题的获取(xpath、jsonpath的一个简单应用)

目录 1.目的2.代码的演示 注&#xff1a;该篇文章为本人原创&#xff0c;由于本人学习有限&#xff0c;若有错误或者笔误或者有问题&#xff0c;欢迎大家进行批评指正&#xff0c;谢谢。 1.目的 在b站大学上&#xff0c;为了更好的写笔记&#xff0c;本人根据学到的Python(即Py…

2. 内存分区模型

一、内存分区模型 C程序在执行时&#xff0c;将内存大方向划分为4个区域 代码区&#xff1a;存放函数体的二进制代码&#xff0c;由操作系统进行管理的全局区&#xff1a;存放全局变量和静态变量以及常量栈区&#xff1a;由编译器自动分配释放&#xff0c;存放函数的参数值&a…

【JVM】垃圾回收 ——自问自答2

Q: System.gc() 的理解 System.gc()底层调用的是 Runtime.getRuntime.gc(),会现实出发FullGC。 但是&#xff0c;它的调用附带一个免责声明&#xff0c;无法保证对垃圾收集器的调用。 Q&#xff1a; 内存溢出和内存泄漏&#xff1f; 内存溢出&#xff1a; 简而言之&#xf…

泊松损坏图像的快速尺度间小波去噪研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

最新ChatGPT网站源码运营版+支持ai绘画(Midjourney)+GPT4.0+GPT官方3.5key绘画+实时语音识别输入+后台一键版本更新!

最新ChatGPT网站源码运营版支持ai绘画GPT4.0GPT官方3.5key绘画实时语音识别输入后台一键版本更新&#xff01; 1.网站系统源码介绍&#xff1a; 程序已支持ChatGPT4.0、Midjourney绘画、GPT3.5 API绘画、语音识别输入、用户会员套餐用户每日签到功能后台管理一键更新版本。支…

docker中的jenkins去配置sonarQube

docker中的jenkins去配置sonarQube 1、拉取sonarQube macdeMacBook-Pro:~ mac$ docker pull sonarqube:8.9.6-community 8.9.6-community: Pulling from library/sonarqube 8572bc8fb8a3: Pull complete 702f1610d53e: Pull complete 8c951e69c28d: Pull complete f95e4f8…

【代码随想录-LeetCode第一题】二分查找及实现

LeetCode刷题第一题&#xff1a;704二分查找法 什么是二分查找&#xff1f;题目思路和边界问题 参考 代码随想录 什么是二分查找&#xff1f; 二分查找&#xff08;Binary Search&#xff09;是一种在有序数组中查找特定元素的查找算法。它通过将目标值与数组的中间元素进行比…

配置虚拟机中常见问题

1.Centos8的问题 用root运行宝塔官方一键安装脚本&#xff0c;结果报错了&#xff0c;提示&#xff1a;为仓库 appstream 下载元数据失败 : Cannot prepare internal mirrorlist&#xff1b; 出现原因&#xff1a; CentOS 8在2022年12月31日将迎来到生命周期终点&#xff0c;…

Spring Boot集成EasyPoi实现导入导出操作

文章目录 Spring Boot集成EasyPoi实现导入导出操作0 简要说明1 环境搭建1.1 项目目录1.2 依赖管理2.3 关于swagger处理2.4 关于切面处理耗时1 自定义注解2 定义切面类3 如何使用 2.5 核心导入操作2.6 核心导出操作 2 最佳实线2.1 导入操作1 实体类说明2 业务层3 效果3 控制层 2…

前端工程师的摸鱼日常(19)

【图为恐子真身】 史记记载恐子九尺六寸高&#xff0c;根据春秋的尺度换算&#xff0c;有一米九至两米多高&#xff0c;人皆畏之。 一米九的山东大汉&#xff0c;手下七十二堂口&#xff08;帮派&#xff09;、三千门生&#xff08;小弟&#xff09;。 由他弟子所编写的《抡…

【数学建模学习(10):遗传算法】

遗传算法简介 • 遗传算法&#xff08;Genetic Algorithms&#xff09;是基于生物进化理论的原理发展起来的一种广为 应用的、高效的随机搜索与优化的方法。其主要特点是群体搜索策略和群体中个体之 间的信息交换&#xff0c;搜索不依赖于梯度信息。它是20世纪70年代初期由美国…

MFC第二十八天 WM_SIZE应用,CFrameWnd类LoadFrame的方法,PreCreateWindow窗口预处理,Frame-view窗口视图

文章目录 WM_SIZE应用通过WM_SIZE消息实现窗口布局管理通过控件属性实现窗口布局管理 CFrameWnd类CFrameWnd类简介OnCreate和OnCreateClient的方法注册时的要素与窗口设置PreCreateWindow创建窗口的预处理函数 附录预处理函数的结构体CFrameWnd::LoadFrame与CreateFrame WM_SIZ…

K8S系列文章之 Traefik快速入门

traefik 与 nginx 一样&#xff0c;是一款优秀的反向代理工具&#xff0c;或者叫 Edge Router。至于使用它的原因则基于以下几点 无须重启即可更新配置自动的服务发现与负载均衡与 docker 的完美集成&#xff0c;基于 container label 的配置漂亮的 dashboard 界面metrics 的支…

周末在家值班,解决几个月前遗忘的Bug

问题&#xff1a; 周末被迫在家值班&#xff0c;无聊之际打开尘封已久的Bug清单&#xff0c;发现有Bug拖了几个月还没解决… 场景是这样子的&#xff0c;有个功能是拿Redis缓存热点数据进行展示&#xff0c;暂且称它为功能A&#xff0c;有个另外的功能B&#xff0c;它会去更新缓…