Android 编译插桩操纵字节码

news2025/2/22 0:04:46

本文讲解如何编译插桩操纵字节码。 就使用 ASM 来实现简单的编译插桩效果,通过插桩实现在每一个 Activity 打开时输出相应的 log 日志。实现思路
过程主要包含两步:

1、遍历项目中所有的 .class 文件​

        如何找到项目中编译生成的所有 .class 文件,是我们需要解决的第一个问题。众所周知,Android Studio 使用 Gradle 编译项目中的 .java 文件,并且从 Gradle1.5.0 之后,我们可以自己定义 Transform,来获取所有 .class 文件引用。但是 Transform 的使用需要依赖 Gradle Plugin。因此我们第一步需要创建一个单独的 Gradle Plugin,并在 Gradle Plugin 中使用自定义 Transform 找出所有的 .class 文件。

2、遍历到目标 .class 文件 (Activity)之后,通过 ASM 动态注入需要被插入的字节码

        如果第一步进行顺利,我们可以找出所有的 .class 文件。接下来就需要过滤出目标 Activity 文件,并在目标 Activity 文件的 onCreate 方法中,通过 ASM 插入相应的 log 日志字节码。

创建主项目 ASMLifeCycleDemo,当前项目中只有一个 MainActivity,如下:

package com.jscode.asmlifecycledemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}

创建自定义 Gradle 插件
首先在 ASMLifeCycleDemo 项目中创建一个新的 module,并选择 Android Library 类型,命名为 asm_lifecycle_plugin。将 asm_lifecycle_plugin module 中除了 build.gradle 和 main 文件夹之外的所有内容都删除。然后在 main 目录下分别创建 groovy 和 java 目录,结构如下:

因为 Gradle 插件是使用 groovy 语言编写的,所以需要新建一个 groovy 目录,用来存放插件相关的.groovy类。 
但 ASM 是 java 层面的框架,所以在 java 目录里存放 ASM 相关的类。 然后,在 groovy 中创建目录com.jscode.plugin,并在此目录中创建类 LifeCyclePlugin.groovy 文件。
在 LifeCyclePlugin 中重写 apply 方法,实现插件逻辑,因为是 demo 演示,所以我只是简单的打印 log 日志。 目录结构如上图,代码如下

package com.jscode.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        System.out.println("========LifeCyclePlugin========")
        def android = project.extensions.getByType(AppExtension)

        LifeCycleTransform lt = new LifeCycleTransform()

        android.registerTransform(lt)

    }
}

以看出 LifeCyclePlugin 实现了 gradle api 中的 Plugin 接口。当我们在 app module 的 build.gradle 文件中使用此插件时,其 LifeCyclePlugin 的 apply 方法将会被自动调用。
接下来,将 asm_lifecycle_plugin module 的 build.gradle 中的内容全部删掉,改为如下内容:

plugins {
    id 'groovy'
    id 'maven-publish'   //本文用到maven本地仓库来管理plugin
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation("com.android.tools.build:gradle:4.1.3")

    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}

publishing {
    repositories {
        maven {
            // $rootDir 表示你项目的根目录
            // 这里配置发布到的本地目录
            url = "$rootDir/repo"
        }
    }

    publications {
        maven(MavenPublication) {

            // 插件的组ID,建议设置为插件的包名
            groupId = 'com.jscode.lifecycleplugin'
            // 插件的名字,后续在引用时会用到
            artifactId = 'myplugin'
            version = '1.0.0'
            // 组件类型
            from components.java
        }
    }
}

group 和 version 都需要在 app module 引用此插件时使用。 所有的插件都需要被部署到 maven 库中,我们可以选择部署到远程或者本地。
本文只是演示,所以只是将插件部署到本地目录中。具体地址通过 repository 属性配置,如图所示我将其配置在项目根目录下的 asm_lifecycle_repo 目录下。 
最后一步,创建 properties 文件。 在 plugin/src/main 目录下新建目录 resources/META-INF/gradle-plguins,然后在此目录下新建一个文件:lifecycleplugin.properties,其中文件名 lifecycleplugin 就是我们自定义插件的名称,稍后我们在 app module 中会使用到此名称。
在 .properties 文件中,需要指定我们自定义的插件类名 LifeCyclePlugin,如下所示:

implementation-class=com.jscode.plugin.LifeCyclePlugin

至此,自定义 Gradle 插件就已经写完,现在可以在 Android Studio 的右边栏找到 Gradle 中点击 publishToMavenLocal,执行 plugin 的部署任务:

可以看到,构建成功之后,在 Project 的根目录下将会出现一个 repo 目录,里面存放的就是我们的插件目标文件。测试 asm_lifecycle_plugin
为了测试自定义的 Gradle 插件是否可用,可以在工程项目中的 build.gradle 中引用此插件。

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.3'
        classpath 'com.jscode.lifecycleplugin:myplugin:1.0.0'
    }
}

plugins {
    id 'com.android.application' version '7.2.0' apply false
    id 'com.android.library' version '7.2.0' apply false
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

app module 中的 build.gradle

plugins {
    id 'com.android.application'
}

apply plugin: 'lifecycleplugin'

android {
    compileSdk 33

    defaultConfig {
        applicationId "com.jscode.asmlifecycledemo"
        minSdk 23
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

setting.gradle文件配置如下:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
        mavenLocal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "ASMLifeCycleDemo"
include ':app'
include ':asm_lifecycle_plugin'

然后在命令行中使用 gradlew 执行构建命令,如果打印出我们自定义插件里的 log,则说明自定义 Gradle 插件可以使用

自定义 Transform,实现遍历 .class 文件

自定义 Gradle 插件已经写好,接下来就需要实现遍历所有 .class 的逻辑。这部分功能主要依赖 Transform API。什么是 Transform ?
Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。自定义 Transform
在com.jscode.plugin目录中,新建 LifeCycleTransform.groovy,并继承 Transform 类

package com.jscode.plugin

import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.jscode.asm.LifecycleClassVisitor
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter

public class LifeCycleTransform extends Transform {

    @Override
    String getName() {
        return "LifeCycleTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY
    }

    /**
     * 表示当前 Transform 是否支持增量编译
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

        Collection<TransformInput> inps = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider

        for (TransformInput ti : inps) {
            for (DirectoryInput di: ti.directoryInputs) {
                File dir = di.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File f ->
                        // 对class文件读取解析
                        ClassReader reader = new ClassReader(f.bytes)
                        // 对class文件的写入
                        ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                        // 访问class文件的相应内容
                        ClassVisitor visitor = new LifecycleClassVisitor(writer)
                        // 依次调用ClassVisitor接口的各个方法
                        reader.accept(visitor, ClassReader.EXPAND_FRAMES)
                        // toByteArray方法最终修改字节码并以byte数组形式返回
                        byte[] bytes = writer.toByteArray()
                        // 通过文件流写入方式覆盖掉原先的内容,实现class文件的改写
                        FileOutputStream fos = new FileOutputStream(f.path)
                        fos.write(bytes)
                        fos.close()

                    }
                }
                def dest = outputProvider.getContentLocation(di.name, di.contentTypes, di.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(di.file, dest)

            }

            // 由于从Android Gradle插件3.6.0-alpha01开始,不再生成R.java,并且将R片段与其他源分开编译为R.jar
            // 所以要把R.jar复制过来
            ti.jarInputs.each { JarInput jarInput ->
                File file = jarInput.file
                def dest = outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                FileUtils.copyFile(file, dest)
            }

        }

    }
}

解释说明:Transform 主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义 Transform 设置一些遍历规则,具体如下: getName:设置我们自定义的 Transform 对应的 Task 名称。
Gradle 在编译的时候,会将这个名称显示在控制台上。比如:Task :app:transformClassesWithXXXForDebug。 getInputType:在项目中会有各种各样格式的文件,通过 getInputType 可以设置 LifeCycleTransform 接收的文件类型,此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。 ContentType 有以下 2 种取值。

    enum DefaultContentType implements ContentType {
        /**
         * The content is compiled Java code. This can be in a Jar file or in a folder. If
         * in a folder, it is expected to in sub-folders matching package names.
         */
        CLASSES(0x01),

        /** The content is standard Java resources. */
        RESOURCES(0x02);

        private final int value;

        DefaultContentType(int value) {
            this.value = value;
        }

        @Override
        public int getValue() {
            return value;
        }
    }
  1. CLASSES:代表只检索 .class 文件;
  2. RESOURCES:代表检索 java 标准资源文件。

getScopes()这个方法规定自定义 Transform 检索的范围,具体有以下几种取值:

    enum Scope implements ScopeType {
        /** Only the project (module) content */
        PROJECT(0x01),
        /** Only the sub-projects (other modules) */
        SUB_PROJECTS(0x04),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
        /** Code that is being tested by the current variant, including dependencies */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40),

        /**
         * Only the project's local dependencies (local jars)
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        PROJECT_LOCAL_DEPS(0x02),
        /**
         * Only the sub-projects's local dependencies (local jars).
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(0x08);

        private final int value;

        Scope(int value) {
            this.value = value;
        }

        @Override
        public int getValue() {
            return value;
        }
    }

isIncremental() 表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。 transform()在 自定义Transform 中最重要的方法就是 transform()。在这个方法中,可以获取到两个数据的流向。

inputs:inputs 中是传过来的输入流,其中有两种格式,一种是 jar 包格式,一种是 directory(目录格式)。
outputProvider:outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错。
我们可以实现一个简易 LifeCycleTransform,功能是打印出所有 .class 文件。代码如上LifeCycleTransform所示

解释说明:

  1. 自定义的 Transform 名称为 LifeCycleTransform;
  2. 检索项目中 .class 类型的目录或者文件;
  3. 设置当前 Transform 检索范围为当前项目;
  4. 设置过滤文件为 .class 文件(去除文件夹类型),并打印文件名称。

将自定义的 LifeCycleTransform 注册到 Gradle 插件中
在 LifeCyclePlugin 中添加如下代码:

public class LifeCyclePlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        System.out.println("========LifeCyclePlugin========")
        def android = project.extensions.getByType(AppExtension)

        LifeCycleTransform lt = new LifeCycleTransform()

        android.registerTransform(lt)

    }
}

再次在命令行中执行 build 命令,可以看到 LifeCycleTransform 检索出的所有 .class 文件。

从图中可以看出,Gradle 编译时多了一个我们自定义的 LifeCycleTransform 类型的任务,并且将所有 .class 文件名打印出来,其中包含了我们需要的目标文件 MainActivity.class。

使用 ASM,插入字节码到 Activity 文件

ASM 是一套开源框架,其中几个常用的 API 如下:

ClassReader:负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter。
ClassVisitor:负责访问 .class 文件中各个元素,还记得上一课时我们介绍的 .class 文件结构吗?ClassVisitor 就是用来解析这些文件结构的,当解析到某些特定结构时(比如类变量、方法),它会自动调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者修改 .class 文件内容。
ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组。

添加 ASM 依赖
在 asm_lifecycle_plugin 的 build.gradle 中,添加对 ASM 的依赖,如下:

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation("com.android.tools.build:gradle:4.1.3")

    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}

创建自定义 ASM Visitor 类
在 asm_lifecycle_plugin module 中的 src/main/java 目录下创建包 danny.jiang.asm,并分别创建 LifecycleClassVisitor.java 和 LifecycleMethodVisitor.java。代码如下: LifecycleClassVisitor.java

package com.jscode.asm;

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

public class LifecycleClassVisitor extends ClassVisitor {

  private String className;
  private String superName;

  public LifecycleClassVisitor(ClassVisitor cv) {
    super(Opcodes.ASM5, cv);
  }


  @Override
  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    super.visit(version, access, name, signature, superName, interfaces);
    this.className = name;
    this.superName = superName;
  }

  @Override
  public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
    if (superName.equals("androidx/appcompat/app/AppCompatActivity")) {
      if (name.startsWith("onCreate")) {
        return new LifecycleMethodVisitor(mv, className, name);
      }
    }
    return mv;

  }

  @Override
  public void visitEnd() {
    super.visitEnd();
  }
}

红框中,在 visitMethod 方法中,过滤出继承自 AppCompatActivity 的文件,并在 LifeCycleMethodVisitor.java 中对 onCreate 进行改造。 LifeCycleMethodVisitor.java

package com.jscode.asm;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleMethodVisitor extends MethodVisitor {

    private String className;
    private String methodName;

    public LifecycleMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitLdcInsn("==========>");
        mv.visitLdcInsn(className + "." + methodName + "()");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
}

上述是真正执行插入字节码的逻辑。可以看出 ASM 都是直接以字节码指令的方式进行操作的,所以如果想使用 ASM,需要程序员对字节码有一定的理解。如果对字节码不是很了解,也可以借助三方工具 ASM Bytecode Outline 来生成想要的字节码。

修改 LifeCycleTransform 的 transform 方法,使用 ASM

各种 Visitor 都定义好之后,我们就可以修改 LifeCycleTransform 的 transform 方法,并将需要插桩的字节码插入到 MainActivity.class 文件中:

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

        Collection<TransformInput> inps = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider

        for (TransformInput ti : inps) {
            for (DirectoryInput di: ti.directoryInputs) {
                File dir = di.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File f ->
                        System.out.println("find file name = " + f.name)
                        // 对class文件读取解析
                        ClassReader reader = new ClassReader(f.bytes)
                        // 对class文件的写入
                        ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                        // 访问class文件的相应内容
                        ClassVisitor visitor = new LifecycleClassVisitor(writer)
                        // 依次调用ClassVisitor接口的各个方法
                        reader.accept(visitor, ClassReader.EXPAND_FRAMES)
                        // toByteArray方法最终修改字节码并以byte数组形式返回
                        byte[] bytes = writer.toByteArray()
                        // 通过文件流写入方式覆盖掉原先的内容,实现class文件的改写
                        FileOutputStream fos = new FileOutputStream(f.path)
                        fos.write(bytes)
                        fos.close()

                    }
                }
                def dest = outputProvider.getContentLocation(di.name, di.contentTypes, di.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(di.file, dest)

            }

            // 由于从Android Gradle插件3.6.0-alpha01开始,不再生成R.java,并且将R片段与其他源分开编译为R.jar
            // 所以要把R.jar复制过来
            ti.jarInputs.each { JarInput jarInput ->
                File file = jarInput.file
                def dest = outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                FileUtils.copyFile(file, dest)
            }

        }

    }

重新部署自定义 Gradle 插件,并运行主项目

上面几步如果一切执行顺利,那接下来就可以在点击 publishToMavelLocal 重新部署 LifeCyclePlugin。
注意:重新部署时,需要先在 app module 的 build.gradle 中将插件依赖注释,否则报错。
部署成功之后,重新在 app 中依赖自定义插件并运行主项目,当 MainActivity 被打开时,会在 logcat 中看到如下


读到这里你可能会有疑虑,如果在项目中打开了混淆,那注入的字节码还会正常 work 吗? 其实无需担心,因为混淆其实也是一个 Transform,叫作 ProguardTransform,它是在自定义的 Transform 之后执行。

总结

本文详细操作了一遍编译插桩的流程。期间涉及了几个知识点:

  • 自定义 Gradle 插件;
  • Transform API 的使用;
  • ASM 的使用。

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

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

相关文章

Win/Mac版Scitools Understand教育版申请

这里写目录标题 前言教育版申请流程教育账号申请 前言 上篇文章为大家介绍了Scitools Understand软件&#xff0c;通过领取的反馈来看有很多朋友都想用这个软件&#xff0c;但是我的网盘里只存了windows的pojie版&#xff0c;没有mac版的&#xff0c;我没有去网上找相关的资源…

支持笔记本电脑直插直充,TOWE 65W智能快充PDU超级插座

电源插排在我们的生活中是必不可少的电器配件。今天&#xff0c;我们日常生活中所使用的电子设备越来越多&#xff0c;无论是手机、平板、笔记本电脑还是各种家用电器&#xff0c;都需要电源来驱动。虽然相对于其他电器来说&#xff0c;插排结构比较简单&#xff0c;但现代家庭…

LabVIEW开发具有栅极感应漏极电流的电荷泵

LabVIEW开发具有栅极感应漏极电流的电荷泵 由操作压力引起的接口陷阱一直是一个长期问题&#xff0c;因为它们会降低栅极电介质的质量&#xff0c;引起器件参数的不必要变化&#xff0c;例如导通状态电流、亚阈值摆幅、阈值电压和跨导。因此&#xff0c;表征界面疏水阀对于确保…

broadcast自动扩展

broadcast&#xff1a; 1、能够进行维度扩展&#xff0c;是自动的2、在扩展的时候不需要拷贝数据要点&#xff1a; - 从最小的维度开始匹配&#xff0c;如果前面没有维度了&#xff0c;在前面插入一个新的维度。- 插入的新维度size是1&#xff0c;再将其扩展为与目标相同大小si…

【超详细】Fastjson 1.2.24 命令执行漏洞复现-JNDI简单实现反弹shell(CVE-2017-18349)

前言&#xff1a; 看了很多别人关于漏洞复现过程&#xff0c;很多博客过程简洁&#xff0c;有的过程过于复杂&#xff0c;比如看到写java代码&#xff0c;用javac进行编译等等。所以我想写出比较详细的漏洞复现过程。 一&#xff0c;漏洞介绍 1-1 fastjson是什么 fastjson是…

xorm数据库操作之Join、Union

golang的数据库操作xorm使用起来非常方便&#xff0c;不用再自己写SQl语句&#xff0c;而且xorm自己给我们做了SQL防注入等操作&#xff0c;用起来既方便又安全。此次文章我不会记录xorm的基本操作&#xff0c;我值记录一些特殊用法问题&#xff0c;包括动态创建表单、基于xorm…

Spring大白话--三级缓存解决循环依赖问题

文章目录 前言一、Spring 循环依赖是什么&#xff1f;二、Spring 三级缓存解决单例的循环依赖&#xff1a;2.1 Bean 单例对象生成的过程&#xff1a;2.2 三级缓存工作过程&#xff1a; 三、Spring 三级缓存无法解决的单例循环依赖情况&#xff1a;3.1 通过构造方法注入的bean &…

如何优化网站排名(百度SEO指南与优化布局方法)

百度SEO指南介绍&#xff1a;蘑菇号-www.mooogu.cn 首先&#xff0c;为了提高网站的搜索引擎优化排名&#xff0c;需要遵循百度SEO指南的规则和标准。这包括使用符合规范的网站结构、页面内容的质量和与目标用户相关的关键词。避免使用非法技术和黑帽SEO的方法。 增加百度SEO…

如何快速重置模型原点

1、什么是模型原点&#xff1f; 模型原点是三维建模中的概念&#xff0c;它是指在一个虚拟三维空间中确定的参考点。模型原点通常位于模型的几何中心或基本组件的中心位置。如图所示&#xff1a; 可以看到模型的原点在模型的几何中心 2、模型原点的作用 知道了什么是模型原点&…

AVL Cruise 2020.1 安装教程

文章目录 安装包安装破解 安装包 链接&#xff1a;https://pan.baidu.com/s/1GxbeDj_SyvKFyPeTsstvTQ?pwd6666 提取码&#xff1a;6666 安装 安装文件&#xff1a; 双击setup.exe&#xff1a; 一直netx&#xff0c;中间要修改两次路径&#xff0c;第一次是安装位置&#xf…

常用螺栓标准、规格、用途汇总表

螺栓 1 常用螺栓标准、用途 常用螺栓标准、规格、用途见表1。 表1 常用螺栓标准、规格、用途汇总表 注&#xff1a;1.冷镦工艺生产的小六角头螺栓具有材料利用率高、生产效率高、机械性能高等优点,但由于头部尺寸较小, 不宜用于多次装拆、被联接件强度较低和易锈蚀等场合。 2.…

Mac多协议传输和文件管理工具ForkLift 4

ForkLift 4 for Mac 是一个强大的文件管理工具&#xff0c;具有直观的界面和强大的功能。它提供了一个直观的界面&#xff0c;使用户能够轻松地管理他们的文件和目录&#xff0c;以及进行各种操作&#xff0c;如复制、移动、重命名、删除等。 它还支持多种文件传输协议&#x…

PHP开发框架及特点

PHP有许多开发框架&#xff0c;每个框架都有其独特的特点和用途。以下是一些常见的PHP开发框架以及它们的特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.Laravel Laravel是一个流行的PHP框架…

踩坑 | vue动态绑定img标签src属性的一系列报错

文章目录 踩坑 | vue项目运行后使用require()图片也不显示问题描述vue中动态设置img的src不生效问题的原因require is not defined 解决办法1&#xff1a;src属性直接传入地址解决办法2 踩坑 | vue项目运行后使用require()图片也不显示 问题描述 在网上查阅之后&#xff0c;发…

攻防世界题目练习——Reverse新手+引导模式

题目目录 1. 6662.Reversing-x64Elf-1003. easyRE14. insanity5. open-source6. game7. Hello, CTF8. re1 1. 666 下载附件 用IDA Pro打开文件&#xff0c;直接看到main入口&#xff0c;反编译查看代码如下&#xff1a; 注&#xff1a;strcmp(a,b)函数当ab时返回值为0。 int …

深度学习|如何确定 CUDA+PyTorch 版本

对于深度学习初学者来说&#xff0c;配置深度学习的环境可能是一大难题&#xff0c;因此本文主要讲解CUDA; cuDNN; Pytorch 三者是什么&#xff0c;以及他们之间的依赖关系。 CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由NVIDIA开发的用于并行计…

【CMU15-445 Part-12】Query Execution I

Part12-Query Execution I Processing Models Processing Model主要指的是明确如何去执行一个查询计划&#xff08;top 2 bottom or bottom 2 top,operator之间的传递&#xff09;。 Iterator Model (volcano model/pipeline model);每个算子实现一个Next( )&#xff0c;父…

Matlab中clear,close all,clc功能详细说明

背景&#xff1a; 我们在写matlab程序时&#xff0c;首行总是先敲入&#xff1a;clear; close all; clc;&#xff0c;但你真的知道这三句话的具体作用嘛&#xff0c;下面进行详细说明和演示。 一、clear的功能 clear的功能&#xff1a;清理工作区变量&#xff0c;不清理前是…

每日一练 | 华为认证真题练习Day115

1、FEC(Forwarding Equivalence Class)转发等价类&#xff0c;是一组具有某些共性的数据流的集合&#xff1b;FEC可以根据地址进行划分&#xff0c;但是不能根据业务类型、QoS等要素进行划分。 A. 对 B. 错 2、关于OSI参考模型中网络层的功能说法正确的是&#xff1f; A. OS…

Linux部署elk日志监控系统

目录 一、简介 二、部署elasticsearch 2.1 安装jdk11&#xff08;jdk版本>11&#xff09; 2.2 下载安装包 2.3 授权elk用户 2.4 配置elasticsearch.yml 2.5 启动elasticsearch 三、部署logstash 3.1 启动测试 3.2 可能出现的报错 3.3 指定配置文件启动logstash 3.4 安装El…