为 Compose MultiPlatform 添加 C/C++ 支持(2):在 jvm 平台使用 jni 实现桌面端与 C/C++ 互操作
前言
在上篇文章中我们已经介绍了实现 Compose MultiPlatform 对 C/C++ 互操作的基本思路。
并且先介绍了在 kotlin native 平台使用 cinterop 实现与 C/C++ 的互操作。
今天这篇文章将补充在 jvm 平台使用 jni。
在 Compose MultiPlatform 中,使用 jvm 平台的是 Android 端和 Desktop 端,而安卓端可以直接使用安卓官方的 NDK 实现交叉编译,但是 Desktop 不仅不支持交叉编译,甚至连使用 Gradle 自动编译都没有。
所以本文重点主要在于实现 Desktop 的 jni 编译以及调用编译出来的二进制库。
Android 使用 jni
在介绍 Desktop 使用 jni 之前,我们先回顾一下在 Android 中使用 jni,并复用 Android 端的 C++ 代码给 Desktop 使用。
感谢谷歌的工作,在安卓中使用 jni 非常简单,我们只需要在 Android Studio 随便打开一个已有的项目,然后依次选择菜单 File - New - New Module - Android Native Library,保持默认参数,点击 Finish 即可完成创建安卓端的 jni 模块。
这里我们以 jetBrains 的官方 Compose MultiPlatform 模板 项目作为示例:
创建完成后需要注意,Android studio 会自动修改项目 settings.gradle.kts 在其中添加一个插件 org.jetbrains.kotlin.android
,这会导致编译错误 java.lang.IllegalArgumentException: Cannot provide multiple default versions for the same plugin.
,所以需要我们删掉新添加的这个插件:
然后在 shared
模块中的 build.gradle.kts 文件的 Android 依赖部分引入 nativelib
模块:
kotlin {// ……sourceSets {// ……val androidMain by getting {dependencies {// ……api(project(":nativelib"))}}// ……}
}
接着,需要注意 nativelib
模块的两个文件 native.cpp 和 NativeLib.kt:
我们看一下 nativelib
模块中的 nativelib.cpp 文件的默认内容:
#include <jni.h>
#include <string>extern "C" JNIEXPORT jstring JNICALL
Java_com_equationl_nativelib_NativeLib_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello = "C++";return env->NewStringUTF(hello.c_str());
}
代码很简单,就是返回一个字符串 “Hello from C++”,我们改成返回 “C++”。
这里需要注意这个函数的名称: Java_com_equationl_nativelib_NativeLib_stringFromJNI
开头的 “Java” 是固定字符,后面的 “com_equationl_nativelib_NativeLib” 表示从 java 调用时的类的包名+类名,最后的 “stringFromJNI” 才是这个函数的名称。
通过 jni 从 java(kt)中调用这个函数时必须确保其包名和类名与其一致才能成功调用。
然后查看 NativeLib.kt 文件:
class NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.loadLibrary("nativelib")}}
}
其中 external fun stringFromJNI(): String
表示需要调用的 c++ 函数名。
System.loadLibrary("nativelib")
表示加载 C++ 编译生成的二进制库,这里我们无需关心具体的编译过程和编译产物,只需要直接加载 nativelib
即可,剩下的工作 NDK 已经替我们完成了。
最后,我们来调用一下这个 C++ 函数。
不过在此之前先简单介绍一下我们用作示例的这个 Compose MultiPlatform 的内容,它的 UI 就是一个按钮,按钮默认显示 “Hello, World!”,当点击按钮后会通过一个 expect
函数获取当前平台的名称然后显示到按钮上:
@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {MaterialTheme {var greetingText by remember { mutableStateOf("Hello, World!") }var showImage by remember { mutableStateOf(false) }Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = {greetingText = "Hello, ${getPlatformName()}"showImage = !showImage}) {Text(greetingText)}AnimatedVisibility(showImage) {Image(painterResource("compose-multiplatform.xml"),contentDescription = "Compose Multiplatform icon")}}}
}expect fun getPlatformName(): String
所以接下来我们修改安卓平台的 getPlatformName
函数的 actual
实现,由:
actual fun getPlatformName(): String = "Android"
修改为:
actual fun getPlatformName(): String = NativeLib().stringFromJNI()
这样,它获取的名称就是来自 C++ 代码的 “C++” 了。
运行代码,可以看到完美符合预期:
Desktop 使用 jni
上一节我们已经完成了在 Android 中使用 jni,本节我们将在 Desktop 中也实现使用 jni,并且复用上节中的 nativelib.cpp 文件。
因为直接使用 Gradle 编译 C++ 代码不是很方便,而且还不支持交叉编译,所以这里我们首先手动编译,验证可行后再自己编写 gradle 脚本实现自动编译。
有关编写 gradle 脚本的基础知识可以阅读我之前的文章 Compose Desktop 使用中的几个问题(分平台加载资源、编写Gradle 任务下载平台资源、桌面特有组件、鼠标&键盘事件) 了解。
首先,我们可以使用命令 g++ nativelib.cpp -o nativelib.bin -shared -fPIC -I C:\Users\equationl\.jdks\corretto-19.0.2\include -I C:\Users\equationl\.jdks\corretto-19.0.2\include\win32
编译我们的 C++ 文件为当前平台可用的二进制文件。
上述命令中 nativelib.cpp
即需要编译的文件,nativelib.bin
为输出的二进制文件,C:\Users\equationl\.jdks\corretto-19.0.2\
为你电脑上安装的任意的 jdk 目录。
输入 “ j d k P a t h / i n c l u d e " 和 " jdkPath/include" 和 " jdkPath/include"和"jdkPath/include/win32” 是因为这两个目录下有我们的 C++ 文件导入所需的头文件,如 “jni.h” 。
切换到我们的 C++ 文件所在目录后执行上述命令编译:
此时我们可以看到在 “./nativelib/src/main/cpp” 目录下已经生成了 nativelib.bin 文件。
注意:在 macOS 上系统自带了 g++ 命令,但是一般来说 Windows 系统没有自带 g++ 命令,所以需要先自己安装 g++
然后,我们在 sahred 模块下的 desktopMain 包中新建一个文件 NativeLib.kt ,注意该文件的包名需要和 C++ 定义的一致:
然后编写该文件内容为:
package com.equationl.nativelibclass NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.load("D:\\project\\ideaProject\\compose-multiplatform-c-test\\nativelib\\src\\main\\cpp\\nativelib.bin")}}
}
可以看到在 Desktop 中加载二进制库和 Android 中略有不同,它使用的是 System.load()
而不是 System.loadLibrary()
,并且加载二进制文件时使用的是绝对路径。
这是因为我们无法在 Desktop 中像 Android 一样直接把二进制文件打包到指定的路径下并且直接使用库名通过 System.loadLibrary()
加载,所以只能使用绝对路径加载外部二进制文件。
这里我们把加载的文件路径写为了先前生成的 nativelib.bin
的路径。
接着,依旧是修改 dektop 的 getPlatformName
函数的实现为:
actual fun getPlatformName(): String = NativeLib().stringFromJNI()
然后运行 Desktop 程序:
运行结果完美符合预期。
为 Desktop 实现自动编译 C++
在上一节中我们已经实现了 Desktop 使用 jni 并验证了可行性,但是目前还是手动编译代码,这显然是不现实的,所以我们本节将讲解如何自己编写脚本实现自动编译。
另外,上一节中我们说过, Dektop 加载二进制文件使用的是绝对路径,所以我们需要将编译生成的二进制文件放到指定位置并打包进 Desktop 程序安装包中,Desktop 在安装时会自动将这个文件解压到指定路径,关于这个的基础知识还是可以看我的文章 Compose Desktop 使用中的几个问题(分平台加载资源、编写Gradle 任务下载平台资源、桌面特有组件、鼠标&键盘事件) 了解。
首先,需要指定一下资源文件目录,在 desktopApp
模块的 buiuld.gradle.kts 文件中添加以下内容:
compose.desktop {application {// ……nativeDistributions {// ……appResourcesRootDir.set(project.layout.projectDirectory.dir("resources"))}}
}
指定资源目录为 resources
。
然后依旧是在这个文件中,添加一个函数 runCommand
,用于执行 shell 命令:
fun runCommand(command: String, timeout: Long = 120): Pair<Boolean, String> {val process = ProcessBuilder().command(command.split(" ")).directory(rootProject.projectDir).redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()process.waitFor(timeout, TimeUnit.SECONDS)val result = process.inputStream.bufferedReader().readText()val error = process.errorStream.bufferedReader().readText()return if (error.isBlank()) {Pair(true, result)}else {Pair(false, error)}
}
代码很简单,接收一个字符串表示的 shell 命令,返回一个 Pair
,第一个 booean 数据表示是否执行成功;第二个 String 是输出内容。
接着注册一个 task:
tasks.register("compileJni") { }
修改原有的 prepareAppResources
task,添加上我们刚注册的 compileJni
为它的依赖:
gradle.projectsEvaluated {tasks.named("prepareAppResources") {dependsOn("compileJni")}
}
这里的修改依赖需要加在 gradle.projectsEvaluated
语句中,因为 prepareAppResources
这个 task 推迟了注册,如果不在项目配置完成后再修改依赖的话会报 prepareAppResources
不存在。
注:这里的 prepareAppResources
是 task 模块中用于执行复制和打包资源文件的 task,所以我们把自定义的 compileJni
添加成它的依赖,以保证在它之前执行。
另外,这里必须明确保证 compileJni
在 prepareAppResources
之前执行,否则由于我们的 compileJni
任务的输出路径和 prepareAppResources
任务的输出路径冲突,会导致编译失败,具体后面详细解释。
接着,在 compileJni
task 中编写我们的编译逻辑,我们先看一下完整的代码,然后再逐一解释:
tasks.register("compileJni") {description = "compile jni binary file for desktop"val resourcePath = File(rootProject.projectDir, "desktopApp/resources/common/lib/")val binFilePath = File(resourcePath, "nativelib.bin")val cppFileDirectory = File(rootProject.projectDir, "nativelib/src/main/cpp")val cppFilePath = File(cppFileDirectory, "nativelib.cpp")// 指定输入、输出文件,用于增量编译inputs.dir(cppFileDirectory)outputs.file(binFilePath)doLast {project.logger.info("compile jni for desktop running……")val jdkFile = org.gradle.internal.jvm.Jvm.current().javaHomeval systemPrefix: Stringval os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()if (os.isWindows) {systemPrefix = "win32"}else if (os.isMacOsX) {systemPrefix = "darwin"}else if (os.isLinux) {systemPrefix = "linux"}else {project.logger.error("UnSupport System for compiler cpp, please compiler manual")return@doLast}val includePath1 = jdkFile.resolve("include")val includePath2 = includePath1.resolve(systemPrefix)if (!includePath1.exists() || !includePath2.exists()) {val msg = "ERROR: $includePath2 not found!\nMaybe it's because you are using JetBrain Runtime (Jbr)\nTry change Gradle JDK to another jdk which provide jni support"throw GradleException(msg)}project.logger.info("Check Desktop Resources Path……")if (!resourcePath.exists()) {project.logger.info("${resourcePath.absolutePath} not exists, create……")mkdir(resourcePath)}val runTestResult = runCommand("g++ --version")if (!runTestResult.first) {throw GradleException("Error: Not find command g++, Please install it and add to your system environment path\n${runTestResult.second}")}val command = "g++ ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}"project.logger.info("running command $command……")val compilerResult = runCommand(command)if (!compilerResult.first) {throw GradleException("Command run fail: ${compilerResult.second}")}project.logger.info(compilerResult.second)project.logger.lifecycle("compile jni for desktop all done")}
}
首先,在 task 顶级定义了四个路径: resourcePath
、 binFilePath
、cppFileDirectory
和 cppFilePath
,分别表示需要存放二进制文件的资源目录、二进制文件输出路径、C++文件存放目录和需要编译的具体 C++ 文件路径。
rootProject.projectDir
返回的是当前项目的根目录。
接着,我们通过 inputs.dir()
方法添加了该 task 的输入路径。
outputs.file
方法添加了该 task 的输出文件。
定义输入路径和输出文件与我们这里需要执行的编译没有直接关联,这里定义这个两个路径是为了让 Gradle 实现增量编译,即只有在上次编译完成后输入路径的中的文件内容发生了变化或输出文件发生了变化才会继续执行这个 task,否则会认为这个 task 没有变化,不会执行,表现在编译输出日志则为:
> Task :desktopApp:compileJni UP-TO-DATE
接下来,我们的代码写在了 doLast { }
语句中,则表示里面的代码只有在编译阶段才会执行,在配置阶段不会执行。
在其中的 org.gradle.internal.jvm.Jvm.current().javaHome
返回的是当前项目 Gradle 使用的 jdk 根目录。
然后,我们需要拼接出编译时需要导入的两个 jdk 路径 includePath1
和 includePath2
,其中的 includePath2
不同的系统名称不一样,所以需要判断一下当前编译使用的系统并更改该值。 可以通过 DefaultNativePlatform.getCurrentOperatingSystem().isXXX
判断当前是否是某个系统。
接着,检查存放二进制文件的目录是否存在,不存在则创建。
下一步是使用 g++ --version
测试是否安装了 g++ 。
最后,拼接出编译命令后执行编译:
g++ ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}
此时如果编译成功,那么二进制文件会输出到我们指定的 dektop 资源目录下。
我们现在只需要修改 dektop 加载二进制文件的代码为:
val libFile = File(System.getProperty("compose.application.resources.dir")).resolve("lib").resolve("nativelib.bin")
System.load(libFile.absolutePath)
上述代码中 System.getProperty("compose.application.resources.dir")
返回的是我们最开始在 Gradle 中定义的资源打包安装解压后在系统上的绝对路径。
至此,我们的自动编译已经完成!
最后来说一下我们前面提到的为什么我们的 compileJni
task 必须在 prepareAppResources
之前执行,我们现在直接把原本的修改 prepareAppResources
依赖于 compileJni
改成 Desktop 模块执行的第一个 task compileKotlinJvm
依赖 compileJni
:
tasks.named("compileKotlinJvm") {dependsOn("compileJni")
}
运行后会看到报错:
A problem was found with the configuration of task ':desktopApp:prepareAppResources' (type 'Sync').- Gradle detected a problem with the following location: '/Users/equationl/AndroidStudioProjects/life-game-compose/desktopApp/resources/common'.Reason: Task ':desktopApp:prepareAppResources' uses this output of task ':desktopApp:compileJni' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.Possible solutions:1. Declare task ':desktopApp:compileJni' as an input of ':desktopApp:prepareAppResources'.2. Declare an explicit dependency on ':desktopApp:compileJni' from ':desktopApp:prepareAppResources' using Task#dependsOn.3. Declare an explicit dependency on ':desktopApp:compileJni' from ':desktopApp:prepareAppResources' using Task#mustRunAfter.
简单说就是 prepareAppResources
和 compileJni
都声明了同一个输出路径,除非明确指定它们两个之间的依赖关系,否则编译会出现问题。
其实也很好理解,他们的输出路径都是一个,如果不明确依赖关系的话增量编译就永远不会触发了,永远都将是全量编译。
而在这里我们的需求是首先使用 compileJni
生成二进制文件后,由 prepareAppResources
将其打包,所以自然应该是写成 prepareAppResources
依赖于 compileJni
。
最后,还是需要强调一点,Desktop 编译 C++ 是不支持交叉编译的,也就是说在 Windows 只能编译 Windows 的程序,在 macOS 只能 编译 macOS 的程序。
其实即使 C++ 可以交叉编译也没用,因为 Compose Desktop 并不支持交叉编译,哈哈哈。
参考资料
- Native dependency in Kotlin/Multiplatform — part 2: JNI for JVM & Android
- Kotlin JNI for Native Code
相关文章:

为 Compose MultiPlatform 添加 C/C++ 支持(2):在 jvm 平台使用 jni 实现桌面端与 C/C++ 互操作
前言 在上篇文章中我们已经介绍了实现 Compose MultiPlatform 对 C/C 互操作的基本思路。 并且先介绍了在 kotlin native 平台使用 cinterop 实现与 C/C 的互操作。 今天这篇文章将补充在 jvm 平台使用 jni。 在 Compose MultiPlatform 中,使用 jvm 平台的是 An…...

【PyTorch】卷积神经网络
文章目录 1. 理论介绍1.1. 从全连接层到卷积层1.1.1. 背景1.1.2. 从全连接层推导出卷积层 1.2. 卷积层1.2.1. 图像卷积1.2.2. 填充和步幅1.2.3. 多通道 1.3. 池化层(又称汇聚层)1.3.1. 背景1.3.2. 池化运算1.3.3. 填充和步幅1.3.4. 多通道 1.4. 卷积神经…...

qt可以详细写的项目或技术
1.QT 图形视图框架 2.QT 模型视图结构 3.QT列表显示大量信息 4.QT播放器 5.QT 编解码 6.QT opencv...

操作系统笔记——储存系统、文件系统(王道408)
文章目录 前言储存系统地址转换内存扩展覆盖交换 储存器分配——连续分配固定大小分区动态分区分配动态分区分配算法 储存器分配——非连续分配页式管理基本思想地址变换硬件快表(TLB)多级页表 段式管理段页式管理 虚拟储存器——基于交换的内存扩充技术…...
基于Html+腾讯云播SDK开发的m3u8播放器
周末业余时间在家无事,学习了一下腾讯的云播放sdk,并制作了一个小demo(m3u8播放器),该在线工具是基于腾讯的云播sdk开发的,云播sdk非常牛,可以支持多种播放格式。 预览地址 m3u8player.org 源码…...
uniapp小程序分享为灰色
引用:https://www.cnblogs.com/panwudi/p/17074172.html uniapp开发的微信小程序,没有转发,分享: 创建一个mixin:common/share.js export default {onShareAppMessage(res) { //发送给朋友return {}},onShareTimeline(res) {//…...

python:五种算法(OOA、WOA、GWO、PSO、GA)求解23个测试函数(python代码)
一、五种算法简介 1、鱼鹰优化算法OOA 2、鲸鱼优化算法WOA 3、灰狼优化算法GWO 4、粒子群优化算法PSO 5、遗传算法GA 二、5种算法求解23个函数 (1)23个函数简介 参考文献: [1] Yao X, Liu Y, Lin G M. Evolutionary programming made…...

DIP——添加运动模糊与滤波
1.运动模糊 为了模拟图像退化的过程,在这里创建了一个用于模拟运动模糊的点扩散函数,具体模糊的方向取决于输入的motion_angle。如果运动方向接近水平,则模糊效果近似水平,如果运动方向接近垂直,则模糊效果近似垂直。具…...
SQL Server查询计划(Query Plan)——SQL处理过程
6. 查询计划(Query Plan) 6.1. SQL处理过程 就SQL语句的处理过程而言,各关系库间大同小异,尤其是商业库之间实现机制和细节差别更小些,其功能及性能支持方面也更加强大和完善。SQL Server作为商业库中的后起之秀,作为SQL语句处理过程的主要支撑和保障,其优化器及相关机…...

【动手学深度学习】(十二)现代卷积神经网络
文章目录 一、深度卷积神经网络AlexNet1.理论知识 一、深度卷积神经网络AlexNet 1.理论知识 ImageNet(2010) 图片自然物体的彩色图片手写数字的黑色图片大小468 * 38728*28样本数1.2M60K类数100010 AlexNet AlexNet赢了2012ImageNet竞赛更深更大的LeNet主要改进ÿ…...

【小沐学Python】Python实现TTS文本转语音(speech、pyttsx3、百度AI)
文章目录 1、简介2、Windows语音2.1 简介2.2 安装2.3 代码 3、pyttsx33.1 简介3.2 安装3.3 代码 4、ggts4.1 简介4.2 安装4.3 代码 5、SAPI6、SpeechLib7、百度AI8、百度飞桨结语 1、简介 TTS(Text To Speech) 译为从文本到语音,TTS是人工智能AI的一个模组…...

TCP通信
第二十一章 网络通信 本章节主要讲解的是TCP和UDP两种通信方式它们都有着自己的优点和缺点 这两种通讯方式不通的地方就是TCP是一对一通信 UDP是一对多的通信方式 接下来会一一讲解 TCP通信 TCP通信方式呢 主要的通讯方式是一对一的通讯方式,也有着优点和缺点…...
2023济南大学acm新生赛题解
通过答题情况的难度系数: 签到:ACI 铜牌题:BG 银牌题:EF 金牌题:DHJKO 赛中暂未有人通过:LMNP A - AB Problem 直接根据公式计算就行。 #include<stdio.h> int main(){int a,b;scanf("%…...

docker-compose安装教程
1.确认docker-compose是否安装 docker-compose -v如上图所示表示未安装,需要安装。 如上图所示表示已经安装,不需要再安装,如果觉得版本低想升级,也可以继续安装。 2.离线安装 下载docker-compose安装包,上传到服务…...

【rabbitMQ】rabbitMQ用户,虚拟机地址(添加,修改,删除操作)
rabbitMQ的下载,安装和配置 https://blog.csdn.net/m0_67930426/article/details/134892759?spm1001.2014.3001.5502 rabbitMQ控制台模拟收发消息 https://blog.csdn.net/m0_67930426/article/details/134904365?spm1001.2014.3001.5502 目录 用户 添加用户…...
Python高级算法——动态规划
Python中的动态规划:高级算法解析 动态规划是一种解决多阶段决策问题的数学方法,常用于优化问题。它通过将问题分解为子问题,并在解决这些子问题的基础上构建全局最优解。在本文中,我们将深入讲解Python中的动态规划,…...

MySQL在Centos7环境安装
说明: • 安装与卸载中,⽤⼾全部切换成为root,⼀旦 安装,普通⽤⼾能使⽤的 1. 卸载不要的环境 [roothcss-ecs-1036 ~]# ps ajx |grep mariadb # 先检查是否有mariadb存在 13134 14844 14843 13134 pts/0 14843 S 1005 0:00 gr…...

halcon视觉缺陷检测常用的6种方法
一、缺陷检测综述 缺陷检测是视觉需求中难度最大一类需求,主要是其稳定性和精度的保证。首先常见缺陷:凹凸、污点瑕疵、划痕、裂缝、探伤等。常用的手法有六大金刚(在halcon中的ocv和印刷检测是针对印刷行业的检测,有对应算子封装): 1.blob+特征 2.blob+差分+特征 3.光度…...

openGauss学习笔记-151 openGauss 数据库运维-备份与恢复-物理备份与恢复之gs_basebackup
文章目录 openGauss学习笔记-151 openGauss 数据库运维-备份与恢复-物理备份与恢复之gs_basebackup151.1 背景信息151.2 前提条件151.3 语法151.4 示例151.5 从备份文件恢复数据 openGauss学习笔记-151 openGauss 数据库运维-备份与恢复-物理备份与恢复之gs_basebackup 151.1 …...
报错:Uncaught ReferenceError: Cannot access ‘l‘ before initialization
在文件 .babelrc 或 babel.config.js ,webpack.config.js 下配置 .babel 或 babel.config.js "plugins": ["babel/plugin-transform-runtime" ] webpack.config.js,详见 Webpack target module.exports {target: [web, es5], }...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

C++:多态机制详解
目录 一. 多态的概念 1.静态多态(编译时多态) 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1).协变 2).析构函数的重写 5.override 和 final关键字 1&#…...

解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...
Kafka主题运维全指南:从基础配置到故障处理
#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...

springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
32位寻址与64位寻址
32位寻址与64位寻址 32位寻址是什么? 32位寻址是指计算机的CPU、内存或总线系统使用32位二进制数来标识和访问内存中的存储单元(地址),其核心含义与能力如下: 1. 核心定义 地址位宽:CPU或内存控制器用32位…...