【Android 字节码插桩】Gradle插件基础 Transform API的使用

前言

啪~我给大家开个会(手机扔桌子上)
什么叫做 客户无感的数据脱敏!?
师爷给翻译翻译什么叫做客户无感的数据脱敏?
什么特么的叫做客户无感数据脱敏?
举个栗子~
客户端Sdk新升级了一个版本,增加针对客户的数据的脱敏,但是客户不需要重新调用新的api,且旧的api执行的性能还是不变的,那么大家可能就会问, 切入点在哪呢? 你不调用新的api或者改动旧的api,如何获取用户数据呢?

字节码的插桩就是做这个用的, 一句话描述~
我(字节码插桩)来这,就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码


一、Gradle插件基础

在Gradle官方文档上是这么描述的:

我的理解:
Gradle 是一种开源构建自动化工具,依赖管理目前只支持 Maven 和 Ivy 兼容的存储库和文件系统

当然, 如果你更喜欢gradle 官方文档上面的描述的话,也行…

在日常写bug的时候,我们每一次新建的工程,最常见的是:
build.gradle 中的 apply plugin: ‘com.android.application’
而apply plugin: ‘com.android.application’ 就是Android提供出来构建APK的一个gradle插件

在该篇文章中,我们主要使用 静态类型的 Java 或 Kotlin 实现的插件,实际测试java 或 kotlin实现的插件, 比 groovy 实现的性能更好一些,当然只要你喜欢, 可以使用任何你喜欢的语言来实现gradle插件, 当然前提是最终可以被编译为jvm字节码~

Gradle插件编写方式

一般来讲,比较流行的是以下三种编写Gradle 插件的编写方式:

  • 项目中编写脚本
    直接在构建脚本中包含插件的源代码。这样做的好处是插件会自动编译并包含在构建脚本的类路径中,而您无需执行任何操作。但是,该插件在构建脚本之外不可见,因此您不能在定义它的构建脚本之外重用该插件
  • 项目中编写buildSrc 项目 (module)
    插件的源代码放在rootProjectDir/buildSrc/src/main/java目录中(rootProjectDir/buildSrc/src/main/groovy或rootProjectDir/buildSrc/src/main/kotlin取决于您喜欢的语言)。Gradle 将负责编译和测试插件,并使其在构建脚本的类路径上可用。该插件对构建使用的每个构建脚本都是可见的。但是,它在构建之外不可见,因此不能在定义它的构建之外重用插件
  • 独立项目 (SDK)
    插件创建一个单独的项目。该项目生成并发布一个 JAR,然后您可以在多个构建中使用它并与他人共享。通常,这个 JAR 可能包含一些插件,或者将几个相关的任务类捆绑到一个库中。或者两者的某种组合

简单介绍完成之后,就开始了我们写实际的操作了,请看VCR ~

为了方便,文中均使用Java来开发Gradle插件,当然,你也可以用Groovy或者Kotlin来试试

1. Gradle 的插件编写之构建脚本

构建脚本这种方式是最简单的,简单到只需要修改build.gradle文件即可,而不需要其他特殊的编码操作,当然功能也是有限
首先,创建新项目,然后我们可以在 项目的 app/build.gradle文件末尾添加~
示例代码:

class PHPluginExtension {
	// 为插件扩展定义一个字符串类型的变量
    String message = "hello,this is my custom plugin..."
}

// 自定义的插件必须继承Plugin接口
class PH2Plugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
    	// 创建插件扩展,greeting为插件扩展的名称,可以在gradle文件其他地方使用
        def extension = project.extensions.create('greeting', MyPluginExtension)

        project.task('hello') {
            doLast {
                 //执行其他代码或者打印相关信息
                println extension.message
            }
        }
    }
}

// 使用这个自定义的插件
apply plugin: PH2Plugin

然后去sync项目,在AndroidStudio右侧的Gradle视图里,可以发现Tasks/others下面出现了一个hello任务,我们双击该任务执行它,会发现控制台中出现如下信息:

这样,就表示我们的gradle插件已经正常执行了,还可以在外部单独写一个文件,跟 app/build.gradle 同级目录, 比如说叫panghu.gradle, 再在app/build.gradle文件中引用panghu.gradle文件即可,类似于下面代码:

apply plugin: 'com.android.application'

// 这一行可以引用外部的gradle文件
apply from: './panghu.gradle'

android {
	...
}
// 通过这种配置方式,修改自定义插件中配置的message的值
PH2{
    message = "更新你想要的文字"
}

buildSrc编写gradle插件项目主要也是用在当前项目中,不能被外部的项目引用,它的创建有一套固定的流程,步骤如下:

第二种是使用buildSrc,首先再项目的根目录创建一个buildsrc目录, 然后点击make按钮, as会自动在 buildSrc文件下面生成一些文件
会有, build.gradle ,.gradle,src,build,
在 build.gradle 中指向对应的包名的类, src/main/java 目录下新建mplugin 类,继承 Plugin类,
然后实现对应的方法,apply…

2. Gradle 的插件编写之buildSrc

buildSrc编写gradle插件项目的是在当前项目下使用,不能也没办法给外部使用,创作流程如下:

1.首先在项目根目录下新建一个buildSrc目录,然后点击AS的make 编译,AS就会自动在buildSrc目录下创建一些文件,如下图所示:

在这里插入图片描述

2.在buildSrc目录下新建build.gradle文件并加入如下代码:

apply plugin: 'java-library'

sourceSets {
    main {
        java{
            srcDir 'src/main/java'
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}

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

3.在buildSrc目录下创建src目录,并在src目录下分别创建main/java和main/resources目录

4.在src/main/java目录下编写插件代码,比如这里我们创建一个简单的插件类,代码如下

package com.panghu.mplugin;

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

public class MPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        System.out.println("Hello from MPlugin...");
    }
}

5.配置插件
在src/main/resources/目录下再创建META-INF/gradle-plugins目录,并在该目录中添加一个文件,文件的命名需要根据插件所在类的名称来,比如上面我们编写的插件类在com.panghu.mplugin包下面,那么就需要创建一个com.panghu.mplugin.properties文件,该文件内容如下:

implementation-class=com.panghu.mplugin.MPlugin

项目整体的结构如下图:
在这里插入图片描述
以上所有步骤都做完之后,即可在app module中引用插件了,引用插件的方法是直接在app/build.gradle文件头部通过apply plugin: 'com.panghu.mplugin’的方式即可,然后我们通过AndroidStudio make图标编译项目,在输出的日志中会发现该插件打印的消息~ 诸君可以把打出来的放在评论区~

3. Gradle 的插件编写之独立项目(SDK)

当然,以上内容只适合在自己的项目中使用,那么我如果想打成sdk供其他人使用呢? 看下面:

如果要使我们编写的gradle插件被外部项目所引用,比如每个AndroidStudio创建的项目都依赖了’com.android.application’这个插件,那么我们就需要使用这种独立项目来完成gradle插件的开发了,开发步骤如下:

  1. 在Android项目上右键,选择New - Module - Java or Kotlin Library创建一个Java library,这里我们取名为plugin
  2. 在该library module的build.gradle文件中编写如下代码:
apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    mavenLocal()
}

dependencies {
    implementation gradleApi()
}

//publish to local directory
group "com.panghu.plugin"
version "1.0.0"

uploadArchives{ //当前项目可以发布到本地文件夹中
    repositories {
        mavenDeployer {
            repository(url: uri('./repo')) //定义本地maven仓库的地址
        }
    }
}

  1. 同第二种实现gradle插件的方式一样,在library的根目录下创建src目录,并在src目录下分别创建main/java main/resources目录

  2. 在src/main/java目录下编写插件代码,这里测试用的代码如下:

package com.panghu.plugin;

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

public class MPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        System.out.println("Hello this is a single gradle plugin...");
    }
}

  1. 在src/main/resources/META-INF/gradle-plugins目录下创建文件,文件名为com.panghu.plugin.properties,文件内容为:
implementation-class=com.panghu.plugin.MPlugin

然后再去同步 sync, 在AndroidStudio右侧的Gradle视图中,我们会看到该插件对应的任务,如下图所示:

在这里插入图片描述
ps:上面这个图片中没有对应的task,编写博客和code不在一个时空,但是位置在这里,仅作参考~

我们双击Tasks - 对应的任务名,AS会自动将该插件打包并上传到maven仓库,注意在上面的第2步中,我们设置了maven仓库为本地地址./repo,则任务执行成功后,会在library的根目录下生成repo目录~

最后,为了引用该插件,我们需要在Android项目中做如下配置:

buildscript {
    repositories {
        google()
        mavenCentral()
        maven { // 本机的repo目录地址
            url '/.../repo'
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.2"
        classpath "com.panghu.plugin:plugin:1.0.0" //配置的版本号

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

在app module的build.gradle文件头部引用该插件:

apply plugin: 'com.panghu.plugin'

然后,编译项目,可以看到该插件输出的日志信息~

那么以上就是三种gradle插件的编写方式,第二种第三种大同小异, 打的包不同而已;第一种是在对应的gradle文件中直接写了,其实都可~ 依据项目需求而定~


二、 TransformAPI是什么

简介

这里是对 TransformAPI 的相关介绍, 有兴趣的哥们可以去看看

其实理解起来就是,TransformAPI可以让我们在编译打包安卓项目时,在源码编译为class字节码后,处理成dex文件前,对字节码做一些操作。

实现自定义的Transform一般要复写如下几个方法,下面对每个方法做一下详细解释~

TransformAPI常用复写方法

getName()

getName()方法用于指明自定义的Transform的名称,在gradle执行该任务时,会将该Transform的名称再加上前后缀,如上面图中所示的,最后的task名称是transformClassesWithXXXForXXX这种格式。

getInputTypes()

用于指明 Transform 的输入类型,可以作为输入过滤的手段。在 TransformManager 类中定义了很多类型:

// 代表 javac 编译成的 class 文件,常用
public static final Set<ContentType> CONTENT_CLASS;
public static final Set<ContentType> CONTENT_JARS;
// 这里的 resources 单指 java 的资源
public static final Set<ContentType> CONTENT_RESOURCES;
public static final Set<ContentType> CONTENT_NATIVE_LIBS;
public static final Set<ContentType> CONTENT_DEX;
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES;
public static final Set<ContentType> DATA_BINDING_BASE_CLASS_LOG_ARTIFACT;

getScopes()

用于指明 Transform 的作用域。同样,在 TransformManager 类中定义了几种范围:


// 注意,不同版本值不一样
public static final Set<Scope> EMPTY_SCOPES = ImmutableSet.of();
public static final Set<ScopeType> PROJECT_ONLY;
public static final Set<Scope> SCOPE_FULL_PROJECT; // 常用
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING;
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES;
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_AND_FEATURES;
public static final Set<ScopeType> SCOPE_FEATURES;
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS;
public static final Set<ScopeType> SCOPE_IR_FOR_SLICING;

常用的是 SCOPE_FULL_PROJECT ,代表所有 Project 。

确定了 ContentType 和 Scope 后就确定了该自定义 Transform 需要处理的资源流。比如 CONTENT_CLASS 和 SCOPE_FULL_PROJECT 表示了所有项目中 java 编译成的 class 组成的资源流。

isIncremental()

指明该 Transform 是否支持增量编译。需要注意的是,即使返回了 true ,在某些情况下运行时,它还是会返回 false 的。

transform(TransformInvocation transformInvocation)

一般在这个方法中对字节码做一些处理。

TransformInvocation是一个接口,源码如下:

public interface TransformInvocation {

    /**
     * Returns the context in which the transform is run.
     * @return the context in which the transform is run.
     */
    @NonNull
    Context getContext();

    /**
     * Returns the inputs/outputs of the transform.
     * @return the inputs/outputs of the transform.
     */
    @NonNull
    Collection<TransformInput> getInputs();

    /**
     * Returns the referenced-only inputs which are not consumed by this transformation.
     * @return the referenced-only inputs.
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * Returns the list of secondary file changes since last. Only secondary files that this
     * transform can handle incrementally will be part of this change set.
     * @return the list of changes impacting a {@link SecondaryInput}
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    /**
     * Returns the output provider allowing to create content.
     * @return he output provider allowing to create content.
     */
    @Nullable
    TransformOutputProvider getOutputProvider();


    /**
     * Indicates whether the transform execution is incremental.
     * @return true for an incremental invocation, false otherwise.
     */
    boolean isIncremental();
}

我们既可以通过TransformInvocation来获取输入,同时也获得了输出的功能,举个例子:

public void transform(TransformInvocation invocation) {
    for (TransformInput input : invocation.getInputs()) {
        input.getJarInputs().parallelStream().forEach(jarInput -> {
        File src = jarInput.getFile();
        JarFile jarFile = new JarFile(file);
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            //处理
        }
    }

TransformAPI 实际操作

本文通过编写一个TransformAPI实例来介绍如何在Android项目中使用TransformAPI~ 请看VCR~

首先 使用Android Studio创建Android项目,这里我取名为TransformDemo~
按照buildSrc的形式,创建一个文件夹,上文有描述~ 格式如下:
在这里插入图片描述

在buildSrc目录下创建build.gradle文件,加入如下代码:

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.2.2'
}

sourceSets {
    main {
        java {
            srcDir 'src/main/java'
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}

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

在插件包com.panghu.plugin中创建一个MPlugin类,代码如下:

package com.panghu.plugin;

import com.android.build.gradle.BaseExtension;

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

public class MPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
    	// 将Transform注册到插件中,执行插件时就会自动执行该Transform
        BaseExtension ext = project.getExtensions().findByType(BaseExtension.class);
        if (ext != null) {
            ext.registerTransform(new MTransform());
        }
    }
}


创建MyTransform类,代码如下~

package com.panghu.plugin;

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.TransformInvocation;
import com.android.build.gradle.internal.pipeline.TransformManager;

import java.io.IOException;
import java.util.Set;

public class MTransform extends Transform {

    @Override
    public String getName() {
        // 最终执行时的任务名称为transformClassesWithMyTestFor[XXX] (XXX为Debug或Release)
        return "MTest";
    }

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

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

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        System.out.println("Hello MTransform...");
    }
}


然后就是注册插件,使用插件了~
这些已经讲过了就不再赘述了~

总结

对于APM & 数据隐私行业,字节码插码是一个比较好用工具~ 完结撒花,有问题随时评论@
文中部分素材取用: https://blog.csdn.net/yubo_725/article/details/118545829~

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

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

相关文章

UnityShader(九)Unity中的基础光照(下)

目录 标准光照模型 自发光 高光反射 &#xff08;1&#xff09;Phong模型 &#xff08;2&#xff09;Blinn模型 漫反射 环境光 逐顶点还是逐像素 逐像素光照 逐顶点光照 总结 标准光照模型 光照模型有许多种&#xff0c;但在早期游戏引擎中&#xff0c;往往只使用一…

linux -- 并发 -- 并发来源与简单的解决并发的手段

互斥与同步 当多个执行路径并发执行时&#xff0c;确保对共享资源的访问安全是驱动程序员不得不面对的问题 互斥&#xff1a;对资源的排他性访问 同步&#xff1a;对进程执行的先后顺序做出妥善的安排 一些概念&#xff1a; 临界区&#xff1a;对共享的资源进行访问的代码片段…

1、缓存击穿背后的问题

当面试官问&#xff1a;你知道什么是缓存击穿吗&#xff0c;你们是如何解决的&#xff1f; 首先我们要了解什么是缓存击穿&#xff1f;以及缓存击穿会引发什么问题&#xff1f; 缓存击穿就是redis中的热点数据过期&#xff0c;缓存失效&#xff0c;导致大量的请求直接打到数据…

【免费分享】数据可视化-银行动态实时大屏监管系统,含源码

一、动态效果展示 1. 动态实时更新数据效果图 ​ 2. 鼠标右键切换主题 二、确定需求方案 1. 屏幕分辨率 这个案例的分辨率是16:9&#xff0c;最常用的的宽屏比。 根据电脑分辨率屏幕自适应显示&#xff0c;F11全屏查看&#xff1b; 2. 部署方式 B/S方式&#xff1a;支持…

使用了不受支持的协议。 ERR_SSL_VERSION_OR_CIPHER_MISMATCH的问题解决办法

windwos 2008 R2 使用IIS部署的项目申请使用https协议的时候&#xff0c;通过安全加密协议访问网站提示不受支持的协议 错误原因分析 这种错误通常表示客户端和服务器之间存在协议版本或加密套件不兼容导致在SSL&#xff08;Secure Socket Layer&#xff09; 1.协议版本不兼容&…

壹[1],Xamarin开发环境配置

1&#xff0c;环境 VS2022 注&#xff1a; 1&#xff0c;本来计划使用AndroidStudio&#xff0c;但是也是一堆莫名的配置让人搞得很神伤&#xff0c;还是回归C#。 2&#xff0c;MAUI操作类似&#xff0c;但是很多错误解来解去&#xff0c;且调试起来很卡。 3&#xff0c;最…

哪个牌子的头戴式耳机好?推荐性价比高的头戴式耳机品牌

随着科技的不断发展&#xff0c;耳机市场也呈现出百花齐放的态势&#xff0c;从高端的奢侈品牌到亲民的平价品牌&#xff0c;各种款式、功能的耳机层出不穷&#xff0c;而头戴式耳机作为其中的一员&#xff0c;凭借其优秀的音质和降噪功能&#xff0c;受到了广大用户的喜爱&…

C++文件操作(2)

文件操作&#xff08;2&#xff09; 1.二进制模式读取文本文件2.使用二进制读写其他类型内容3.fstream类4.文件的随机存取文件指针的获取文件指针的移动 1.二进制模式读取文本文件 用二进制方式打开文本存储的文件时&#xff0c;也可以读取其中的内容&#xff0c;因为文本文件…

Flask 入门3:Flask 请求上下文与请求

1. 前言 Flask 在处理请求与响应的过程&#xff1a; 首先我们从浏览器发送一个请求到服务端&#xff0c;由 Flask 接收了这个请求以后&#xff0c;这个请求将会由路由系统接收。然后在路由系统中&#xff0c;还可以挂入一些 “勾子”&#xff0c;在进入我们的 viewFunction …

【C++】开源:Windows图形库EasyX配置与使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Windows图形库EasyX配置与使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#…

✅Redis 常见数据类型和应用场景(详解)

Redis 提供了丰富的数据类型&#xff0c;常见的有五种&#xff1a;String&#xff08;字符串&#xff09;&#xff0c;Hash&#xff08;哈希&#xff09;&#xff0c;List&#xff08;列表&#xff09;&#xff0c;Set&#xff08;集合&#xff09;、Zset&#xff08;有序集合&…

揭开时间序列的神秘面纱:特征工程的力量

目录 写在开头1. 什么是特征工程?1.1 特征工程的定义和基本概念1.2 特征工程在传统机器学习中的应用1.3 时间序列领域中特征工程的独特挑战和需求3. 时间序列数据的特征工程技术2.1 数据清洗和预处理2.1.1 缺失值处理2.1.2 异常值检测与处理2.2 时间特征的提取2.2.1 时间戳解析…

循环——枚举算法2(c++)

目录 找和为K的两个元素 描述 在一个长度为n(n < 1000)的整数序列中&#xff0c;判断是否存在某两个元素之和为k。 输入 第一行输入序列的长度n和k&#xff0c;用空格分开。 第二行输入序列中的n个整数&#xff0c;用空格分开。 输出 如果存在某两个元素的和为k&…

个人建站前端篇(一)项目准备初始化以及远程仓库连接

云风的知识库 云风网前端重构&#xff0c;采用vue3.0vite antd框架&#xff0c;实现前后端分离&#xff0c;实现网站的SEO优化&#xff0c;实现网站的性能优化 vite创建vue项目以及前期准备 Vite 需要 Node.js 版本 18&#xff0c;20。然而&#xff0c;有些模板需要依赖更高…

STM32存储左右互搏 QSPI总线读写FLASH W25QXX

STM32存储左右互搏 QSPI总线读写FLASH W25QXX FLASH是常用的一种非易失存储单元&#xff0c;W25QXX系列Flash有不同容量的型号&#xff0c;如W25Q64的容量为64Mbit&#xff0c;也就是8MByte。这里介绍STM32CUBEIDE开发平台HAL库Qual SPI总线操作W25Q各型号FLASH的例程。 W25Q…

游泳耳机要怎么选购?一篇文章告诉你如何选购游泳耳机

在进行运动时享受音乐的乐趣是许多人的喜好&#xff0c;对于在地面展开的一般运动&#xff0c;选择耳机相对简单&#xff0c;但若是涉及水中游泳&#xff0c;我们就需要一款具备防水性能的专业游泳耳机。市面上已有数款针对游泳设计的防水耳机&#xff0c;本文将为您详细介绍如…

【解刊】审稿人极其友好!中科院2区SCI,3个月录用,论文质量要求宽松!

计算机类 • 高分快刊 今天带来Springer旗下计算机领域高分快刊&#xff0c;有投稿经验作者表示期刊审稿人非常友好&#xff0c;具体情况一起来看看下文解析。如有投稿意向可重点关注&#xff1a; 01 期刊简介 Complex & Intelligent Systems ✅出版社&#xff1a;Sprin…

光杆司令如何部署大模型?

1、背景 今天这种方式非常贴合低配置笔记本电脑的小伙伴们, 又没有GPU资源, 可以考虑使用api方式,让模型服务厂商提供计算资源 有了开放的api,让你没有显卡的电脑也能感受一下大模型管理知识库,进行垂直领域知识的检索和问答.算是自己初步玩一下AI agent 之前有写过一篇《平民…

Java二维码图片识别

前言 后端识别二维码图片 代码 引入依赖 <dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.2.1</version></dependency><dependency><groupId>com.google.zxing<…

软件压力测试:探究其目的与重要性

随着软件应用在各行各业中的广泛应用&#xff0c;确保软件在高负载和极端条件下的稳定性变得至关重要。软件压力测试是一种验证系统在不同负载条件下的性能和稳定性的方法。本文将介绍软件压力测试的目的以及为什么它对软件开发和部署过程至关重要。 验证系统性能的极限&#x…