字节码插桩 -- 入门篇

背景

我们先了解下什么情况下会用到字节码插桩。学技术并不是为了秀技术,而是为了解决业务问题。

我们先想象一个业务场景— 我们需要统计耗时方法,这时,我们会怎么做?

在每个方法开头和结尾处分别记录开始时间与结束时间?在自己写的代码上用还好,但是第三方库类怎么办?

这时就可以用上字节码插桩了!因为 Java 文件编译成 class 后,这时可以获取全部的 class 文件,包含自己写的代码和其它库类的。拿到 class 文件后,就可以进行批量修改,并且对于 Java 文件是无感的,因为我们只针对 class 文件。

在使用字节码插桩之前,我们需要获取到每个 class 文件,这时,需要使用到自定义 Transform,而自定义Transform 需要在自定义 Gradle Plugin 时进行注册,所以,我们需要先学习下如何自定义一个 Gradle Plugin。

一、字节码插桩是什么

字节码插桩是一种在程序的字节码级别进行修改的技术。它通常用于在程序运行过程中动态地修改、分析或监控代码的行为,而无需修改源代码。

1.1 字节码插桩发生的时机

apk 的打包流程如下:

字节码插桩就发生在 .class 文件变成 .dex 文件之前。正是在这样的一个时机,字节码插桩才拥有修改全局 .class 文件的能力。

1.2 字节码插桩的应用场景

通过字节码插桩,我们可以全局替换目标方法的实现、增加目标方法的逻辑,这种处理方式更加通用彻底且具有兼容性,基于这样的能力,字节码插桩具备很大的想象空间:

二、自定义 Gradle 插件流程

2.1 创建插件 Module

Android Studio --> File --> New --> New Module --> Java or Kotlin Library --> plugin(名字自取)

2.2 配置插件 build.gradle

plugins {
    id 'java-library'
    alias(libs.plugins.jetbrainsKotlinJvm)
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    // gradle
    implementation gradleApi()
    // asm
    implementation libs.asm
    implementation libs.asm.commons
    implementation libs.asm.analysis
    implementation libs.asm.util
    implementation libs.asm.tree
}

libs.version.toml配置如下

[versions]
agp = "7.4.0"
kotlin = "1.9.0"
asm = "9.7"
...

[libraries]
...
# asm相关依赖
asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" }
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }

2.3 编写插件代码

package com.lx.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class AsmPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        println("asm plugin apply")
    }
}

2.4 配置插件

lx-plugin.properties 文件名称可以自取,后面会用到

2.5 发布到 maven 仓库

2.5.1 发布本地 maven 仓库

1. 在 plugin 的 build.gradle 中添加本地 maven 仓库配置

2. 双击 publish 将插件发布到本地 maven 仓库

3. 可以看到在 plugin 目录下有以下文件生成:

2.5.2 发布到远程 maven 仓库

我们将插件发布到远程 maven 仓库后,就可以提供所有人使用了。

1. Nexus 搭建远程 maven 仓库

为了演示效果,本文通过在本机搭建远程 maven 仓库。

 参考链接:Nexus本地搭建(MacOS)_mac安装nexus-CSDN博客 ​​​​​

2. 配置远程 maven 仓库地址

修改 plugin 的 build.gradle 中 maven 仓库配置

3. 双击 publish 将插件发布到本地 maven 仓库

4. 在 Sonatype Nexus Repository 中可以看到我们发布的插件了。

2.6 使用插件

1. 在 project 的 build.gradle 添加插件依赖

2. 在 app 的 build.gradle 中引入插件

3. 验证,直接编译该工程

在 Build Output 中可以看到正常的输出语句

三、自定义Gradle 插件实现方法耗时统计

3.1 自定义 MethodTimeAdviceAdapter

package com.lx.plugin

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Type
import org.objectweb.asm.commons.AdviceAdapter

/**
 * Created by lixiong on 2024/4/29.
 */
class MethodTimeAdviceAdapter(
    api: Int,
    methodVisitor: MethodVisitor,
    access: Int,
    name: String?,
    descriptor: String?,
    private val className: String?
) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {
    private val slotIndex = newLocal(Type.LONG_TYPE)

    /**
     * 方法开始执行
     */
    override fun onMethodEnter() {
        super.onMethodEnter()
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
        mv.visitVarInsn(LSTORE, slotIndex)
    }

    /**
     * 方法执行结束
     */
    override fun onMethodExit(opcode: Int) {
        mv.visitLdcInsn("MethodTime")
        mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
        mv.visitInsn(DUP)
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
        mv.visitLdcInsn("${className}.${name} time cost:")
        mv.visitMethodInsn(
            INVOKEVIRTUAL,
            "java/lang/StringBuilder",
            "append",
            "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
            false
        )
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
        mv.visitVarInsn(LLOAD, slotIndex)
        mv.visitInsn(LSUB)
        mv.visitMethodInsn(
            INVOKEVIRTUAL,
            "java/lang/StringBuilder",
            "append",
            "(J)Ljava/lang/StringBuilder;",
            false
        )
        mv.visitMethodInsn(
            INVOKEVIRTUAL,
            "java/lang/StringBuilder",
            "toString",
            "()Ljava/lang/String;",
            false
        )
        mv.visitMethodInsn(
            INVOKESTATIC,
            "android/util/Log",
            "d",
            "(Ljava/lang/String;Ljava/lang/String;)I",
            false
        )
        mv.visitInsn(POP)
        super.onMethodExit(opcode)
    }
}

除了字节码部分其他的代码没什么好说的,都好理解,这部分代码也不需要自己写,可以在 Android Studio 中搜索 ASM bytecode viewer 插件。

3.1.1 使用ASM bytecode Viewer 生成相应的字节码

新建一个Demo.java 文件,编译后,在 Demo.class 右键代码区,点击 ASM Bytecode Viewer 

然后选择 ASMified

记录下这里的代码,后面要用。

然后在 Demo.java 的 test 方法中编写想插入的代码,然后在通过 ASM Bytecode Viewer 查看 ASMified 代码

对比插入代码前后的 ASMified 代码的差异,就可以知道如何通过 MethodVisitor 插入字节码了。

3.2 自定义 MethodTimeClassVisitor

package com.lx.plugin

import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

/**
 * Created by lixiong on 2024/4/29.
 */
class MethodTimeClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
    private var className: String? = null

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        return MethodTimeAdviceAdapter(api, methodVisitor, access, name, descriptor, className)
    }
}

3.3 自定义 MethodTimePlugin

package com.lx.plugin

import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream


/**
 * Created by lixiong on 2024/4/28.
 */
class MethodTimePlugin : Transform() {
    override fun getName(): String {
        return "MethodTimePlugin"
    }

    /**
     * 用于指明Transform的输入类型
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 用于指明Transform的作用域
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 指明该Transform是否支持增量编译
     */
    override fun isIncremental(): Boolean {
        return true
    }

    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
        val inputs = transformInvocation?.inputs
        val outputProvider = transformInvocation?.outputProvider
        inputs?.forEach { transformInput ->
            // 遍历项目目录
            transformInput.directoryInputs.forEach { directoryInput ->
                if (directoryInput.file.isDirectory) {
                    FileUtils.getAllFiles(directoryInput.file).forEach { file ->
                        val name = file.name
                        // 过滤class文件, 排除R.class, BuildConfig.class
                        if (name.endsWith(".class") && !name.startsWith("R\$") &&
                            name != "R.class" && name != "BuildConfig.class"
                        ) {
                            // 找到需要的class文件,进行插桩
                            val path = file.absolutePath
                            val cr = ClassReader(file.readBytes())
                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            val visitor = MethodTimeClassVisitor(cw)
                            cr.accept(visitor, ClassReader.EXPAND_FRAMES)

                            val bytes = cw.toByteArray()
                            var fos: FileOutputStream? = null
                            try {
                                fos = FileOutputStream(path)
                                fos.write(bytes)
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                runCatching { fos?.close() }
                            }
                        }
                    }
                }
                val dest = outputProvider?.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )
                FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)
            }

            // 遍历jar包
            transformInput.jarInputs.forEach { jarInput ->
                val dest = outputProvider?.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

3.4 注册插件

3.5 验证插件

首先需要发布插件,然后依赖插件,这一步可以看上一章的内容。

运行之后,查看 logcat 打印

完美,通过 jadx 工具查看下生成的 .class 文件是否插入成功

Demo.class

MainActivity.class

编译生成的 ActivityMainBinding.class

插入成功,至此简单的Asm字节码插桩就完成了。

3.6 对 jar 包进行插桩

1. 在 app module 的libs 中加入一个 test.jar 文件

2. 修改自定义的 MethodTimePlugin,完整代码如下:

package com.lx.plugin

import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.apache.commons.compress.utils.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream
import java.nio.file.attribute.FileTime
import java.util.Enumeration
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.CRC32
import java.util.zip.ZipEntry


/**
 * Created by lixiong on 2024/4/28.
 */
class MethodTimePlugin : Transform() {

    private val fileTime = FileTime.fromMillis(0)

    override fun getName(): String {
        return "MethodTimePlugin"
    }

    /**
     * 用于指明Transform的输入类型
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 用于指明Transform的作用域
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 指明该Transform是否支持增量编译
     */
    override fun isIncremental(): Boolean {
        return true
    }

    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
        val inputs = transformInvocation?.inputs
        val outputProvider = transformInvocation?.outputProvider
        inputs?.forEach { transformInput ->
            // 遍历项目目录
            transformInput.directoryInputs.forEach { directoryInput ->
                if (directoryInput.file.isDirectory) {
                    FileUtils.getAllFiles(directoryInput.file).forEach { file ->
                        val name = file.name
                        // 过滤class文件, 排除R.class, BuildConfig.class
                        if (name.endsWith(".class") && !name.startsWith("R\$") &&
                            name != "R.class" && name != "BuildConfig.class"
                        ) {
                            // 找到需要的class文件,进行插桩
                            val path = file.absolutePath
                            val cr = ClassReader(file.readBytes())
                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            val visitor = MethodTimeClassVisitor(cw)
                            cr.accept(visitor, ClassReader.EXPAND_FRAMES)

                            val bytes = cw.toByteArray()
                            var fos: FileOutputStream? = null
                            try {
                                fos = FileOutputStream(path)
                                fos.write(bytes)
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                runCatching { fos?.close() }
                            }
                        }
                    }
                }
                val dest = outputProvider?.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )
                FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)
            }

            // 遍历jar包
            transformInput.jarInputs.forEach { jarInput ->
                val dest = outputProvider?.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )
                if (dest != null) {
                    FileUtils.mkdirs(dest.parentFile)
                    // 只对 test.jar 进行插桩
                    if (jarInput.file.name.endsWith("test.jar")) {
                        var jos: JarOutputStream? = null
                        try {
                            val jarFile = JarFile(jarInput.file)
                            jos = JarOutputStream(FileOutputStream(dest))
                            val entries: Enumeration<JarEntry> = jarFile.entries()
                            while (entries.hasMoreElements()) {
                                val entry: JarEntry = entries.nextElement()
                                val name: String = entry.name
                                val outEntry = JarEntry(name)
                                val inputStream = jarFile.getInputStream(entry)
                                // 过滤class文件, 排除R.class, BuildConfig.class
                                val newEntryContent = if (name.endsWith(".class") && !name.startsWith("R\$") &&
                                    name != "R.class" && name != "BuildConfig.class"
                                ) {
                                    // 找到需要的class文件,进行插桩
                                    val cr = ClassReader(inputStream)
                                    val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                                    val visitor = MethodTimeClassVisitor(cw)
                                    cr.accept(visitor, ClassReader.EXPAND_FRAMES)
                                    cw.toByteArray()
                                } else {
                                    IOUtils.toByteArray(inputStream)
                                }
                                // 将处理后的类文件写入 JAR 包
                                val crc32 = CRC32()
                                crc32.update(newEntryContent)
                                outEntry.crc = crc32.value
                                outEntry.method = ZipEntry.STORED
                                outEntry.size = newEntryContent.size.toLong()
                                outEntry.compressedSize = newEntryContent.size.toLong()
                                outEntry.setLastAccessTime(fileTime)
                                outEntry.setLastModifiedTime(fileTime)
                                outEntry.setCreationTime(fileTime)
                                jos.putNextEntry(outEntry)
                                jos.write(newEntryContent)
                                jos.closeEntry()
                            }
                        } catch (e: Exception) {
                            e.printStackTrace()
                        } finally {
                            runCatching {
                                jos?.flush()
                                jos?.close()
                            }
                        }
                    } else {
                        FileUtils.copyFile(jarInput.file, dest)
                    }
                }
            }
        }
    }
}

3. 通过 jadx 查看 apk 中 Test.class 文件

到此,jar 包中的方法也插桩成功。

代码地址

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

学生管理系统[Python语言]

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 学生管理系统是计算机专业最基础的一个作业&#…

高扬程水泵,提升水源新选择!— 恒峰智慧科技

在炎炎夏日&#xff0c;阳光炙烤着大地&#xff0c;森林火灾的发生频率也随之上升。火势猛烈&#xff0c;烟雾弥漫&#xff0c;给森林带来了极大的破坏。为了保护森林资源&#xff0c;我们必须采取有效的措施来扑灭火灾。而在这其中&#xff0c;高扬程水泵成为了提升水源新选择…

智慧旅游驱动行业革新:智能技术引领服务全面升级,匠心打造高品质、个性化旅游新体验

一、引言 随着科技的飞速发展和信息化程度的不断提高&#xff0c;智慧旅游正逐渐成为旅游业发展的新趋势。智慧旅游&#xff0c;顾名思义&#xff0c;是以智能化技术为支撑&#xff0c;通过大数据、云计算、物联网、人工智能等先进技术的应用&#xff0c;实现旅游服务的全面升…

Java项目:基于SSM框架实现的实践项目管理系统(ssm+B/S架构+源码+数据库+毕业论文+开题报告)

一、项目简介 本项目是一套基于SSM框架实现的实践项目管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff…

BIMBase浏览器新功能——碰撞检测

BIMBase浏览器&#xff08;原BIMBase建模软件 Lite&#xff09;全新R1.14版本已经全面上线&#xff0c;新版本下载链接&#xff1a;BIMBase浏览器R1.14。本次给大家介绍一下本次版本的重点功能&#xff1a;碰撞检测。 各位设计院/施工单位/运维单位的伙伴们在模型交付、方案讨论…

第三节课,功能2:开发后端用户的管理接口5min(用户的查询/状态更改)【4】【9开始--本人】

一、代码任务 【录个屏】 二、写代码 2.1 代码文件位置 2.2 代码如下&#xff1a; 2.3 官方文档&#xff1a; 网址&#xff1a; 逻辑删除 | MyBatis-Plus (baomidou.com) 三、代码有bug&#xff0c;没有鉴权&#xff0c;表里添加一个字段。role 管理员 3.1 判断操作的人&am…

【开发工具】pythontutor——在线内存可视化工具

笔者在学习RISC-V时&#xff0c;希望找到一款可视化的内存工具&#xff0c;遗憾目前还未找到。发现了pythontutor这个网站&#xff0c;可以对C、python等多种语言进行内存可视化。结果似乎是x86架构的&#xff0c;符合小端存储。 贴一下网址&#xff0c;原准备依据开源版本进行…

什么是MOM?为什么它是趋势

制造运营管理&#xff08;MOM&#xff09; 制造运营管理&#xff08;MOM&#xff09;旨在优化制造流程的效率和有效性。它涵盖制造执行、质量管理、生产计划和调度以及制造智能等功能。这种解决方案以全面的方式管理和增强制造流程。 MOM的功能特点 对于MOM的功能特点来说&…

爬虫自动调用shell通过脚本运行scrapy爬虫(crawler API)

一、爬虫时如何同时调用shell 1)终端cd项目>>scrapy crawl example 2)打开example.py import scrapy from scrapy.shell import inspect_response#引入shellclass ExampleSpider(scrapy.Spider):name "example"allowed_domains ["example.com"]…

兄弟们,哪一家做智慧校园的公司比较靠谱?

开发技术参数 1、使用springboot框架Javavue2 2、数据库MySQL5.7 3、移动端小程序使用小程序原生语言开发 4、电子班牌固件安卓7.1&#xff1b;使用Java Android原生 5、elmentui &#xff0c;Quartz&#xff0c;jpa&#xff0c;jwt 6、SaaS云平台&#xff0c;私有云部署…

iview 自定义项求和的方法和错误点

这是iview自定义某几项参数合计的方法&#xff0c;其实是蛮简单的&#xff0c;很多人自定义合计的时候&#xff0c;老是会不知道怎么处理除了需要合计的几项的其他项&#xff0c;其实不需要管&#xff0c;不需要合计的项直接返回空就好了&#xff0c;需要的就在计算的里面做key…

Llama 3 ——开源大模型Llama 3从概念到使用

概述 Meta公司自豪地宣布推出其最新的开源大型语言模型——Llama 3&#xff0c;这是一款专为未来AI挑战而设计的先进工具。Llama 3包含两个不同参数规模的版本&#xff0c;以满足多样化的计算需求&#xff1a; 8B版本&#xff1a;优化了在消费级GPU上的部署和开发流程&#xf…

初识MVC

初识MVC 理论部分 今天第一次学MVC&#xff0c;拿到一个练手项目。现在来记录一下学习过程。 项目的背景就是个学生管理系统。我只做后端。 从大的来说MVC将应用程序分为三个主要组件&#xff08;部分&#xff09;&#xff1a; 模型&#xff08;Model&#xff09;是应用程序…

SGP.31-05

6.1.1 eIM触发下载过程 6.1.2 eIM Initiated Direct Profile Download with SM-DS 在文档的“6.1.2 eIM 发起的通过 SM-DS 的直接配置文件下载”部分&#xff0c;描述了两种直接从 SM-DP 到 eUICC 的配置文件下载选项。以下是每个步骤的概述&#xff1a; ### 开始条件&…

​基于Python的在线自主评测系统(django)​

基于Python的在线自主评测系统(django) 开发语言:Python 数据库&#xff1a;MySQL所用到的知识&#xff1a;Django框架工具&#xff1a;pycharm、Navicat、Maven 学生功能模块的实现 学生注册的实现 学生登录界面首页 在线考试界面 考试成绩查看界面 教师功能模块的实现 新建…

以全栈智算拥抱生态,为AIGC种一棵向上生长的巨榕

榕树&#xff0c;被称为百木之王。它既有极深的根&#xff0c;又有繁茂的叶。只要一棵榕树长成&#xff0c;就能够独木成林&#xff0c;遮天蔽日。更可贵的是&#xff0c;榕树可以为树荫下繁茂的生态提供支撑&#xff0c;形成“一榕生&#xff0c;万物长”的格局。 开年以来&am…

MySQL中SELECT语句的执行过程

2.1.1. 一条SELECT语句的执行过程 MySQL 的架构共分为两层&#xff1a;Server 层和存储引擎层 Server层负责建立连接、分析和执行SQL存储引擎层负责数据的存储和提取&#xff0c;支持 InnoDB、MyISAM、Memory 等多个存储引擎&#xff0c;MySQL5.5以后默认使用InnoDB&#xff0…

set_input_delay的理解

1&#xff0c;set_input_delay约束理解 input_delay是指输入的数据到达FPGA的pad引脚时相对于时钟边沿的延迟有多大&#xff0c;单位是ns&#xff0c;数值可以是正&#xff0c;也可以是负。通过set_input_delay约束告诉编译器输入时钟和输入数据的相位关系。如下图所示假设时钟…

RSA加密---java和node兼容版(可直接复制使用)

目录 背景 实现 一、node代码 1、引入依赖 2、生成公钥和私钥 3、生成工具类 二、java代码 背景 本来项目的后端是node&#xff0c;里面登录接口用的是后端生成RSA公钥和私钥&#xff0c;公钥给前端网页用来加密&#xff0c;node后端解密&#xff0c;一切很和谐&#x…

Flexible布局在Web前端开发中的实际应用

随着Web前端技术的不断发展&#xff0c;Flexible布局&#xff08;弹性布局&#xff09;已成为现代网页设计中不可或缺的一部分。它提供了一种高效、灵活的方式来组织和管理页面元素&#xff0c;使开发者能够轻松应对各种复杂的布局需求。本文将通过一个实际的应用案例来介绍Fle…