自定义 gradle plugin,教你如何 hook 系统 task 和字节码

栏目: 编程工具 · 发布时间: 5年前

内容简介:大家在自己写当时这个问题确实困惑了我一段时间,总不能自己为了不对外暴露,把说时迟那时快,就想着自己搞个什么骚操作 hook 一下

大家在自己写 library 的时候估计也遇到过这种困惑:一个 library 中的某个类中有些方法或类只想给该 library 中的类使用,并不想暴露出去,但是由于项目的包的层级关系,不得不把方法写为 public ,导致暴露给了外界!!!

当时这个问题确实困惑了我一段时间,总不能自己为了不对外暴露,把 方法/类 写为 非public 吧?那我自己的 library 如何去调用呢?难道自己写反射?太蠢了吧。

说时迟那时快,就想着自己搞个什么骚操作 hook 一下 library 生成的 jar/aar 包吧。脑袋一热大腿一拍,妈的,写个插件吧!

于是,这边就有了本篇文章的主角 Seeker(Github 传送门)

二、自我反思

在开始之前,先在这里认个错,之前脑袋热的有点快,其实这个问题早就有了解决的方案, @RestrictTo ,有兴趣的可以点进去了解一下。

在解决问题之前,建议大家多去搜一下有没有已有的解决方案,我是马上写完的时候才发现有 @RestrictTo ,吐血ing,中途有点难受,差点憋出内伤,最后还是自我安慰,就当学习 gradle 了 TAT...

三、解决思路

在我看来要解决这个问题有两个方向:

  • hook library 最后打包成 aar/jar 的源码,改变方法的 modifier
  • build 过程直接报错,告诉用户这个方法不可以调用。

由于第二种方案有点暴力,太过不近人情,既然不让我用,你为啥要暴露出来?暴露出来又报错是什么鬼?处于以上考虑,我选择了一条艰难的道路。

有了大致方向后,开始准备撸代码,首先,需要先设计供用户使用的 Api 层,毕竟大佬们用的好才是真的好 ;)

我定义了一个 @Hide 注解,参数是一个 enum 类型,可以指定 modifier ,代码如下:

public enum Modifier {
    /** The modifier {@code public} */ PUBLIC,
    /** The modifier {@code protected} */ PROTECTED,
    /** The modifier {@code private} */ PRIVATE,
    /** The modifier with the default value */ DEFAULT;
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Hide {
    Modifier value() default Modifier.PRIVATE;
}
复制代码

添加 @Hide 注解到需要 hook 的方法上面,你也可以指定为不同的 modifier ,最后在你的 library build.gradleapply 一下我的插件即可!!!

Api 设计的很简洁,对业务也没有什么侵入性,因为我们的 library 最后是需要打包成 aar/jar 给其他人调用的,所以归根结底我们需要 hook 一下 uploadArchives task 的执行过程

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

四、获取 @Hide

我们给方法加上 @Hide 之后,需要找到这些方法,给后面 hook 字节码的时候用,要做到这一步还有什么比 APT 更加合适的呢。

APT 的使用较为简单,没什么需要注意的地方,在此处省略,有兴趣的可以自行了解一下。

总之,我们需要在这一步获取到所有含有 @Hide 的方法,然后保存一份到本地,这里我保存的是 json 文件。

五、hook 过程

这里我们需要拆分为两步:

uploadArchives

因为我们最终希望打包出来的 jar/aar 发生改变,而打包是通过 uploadArchives task 做的,所以我们需要对这个 task 进行分析并在某一步。

5.1、寻找需要 hook 的 task

要分析这个 task ,我们需要先知道这个 task 依赖了哪些 task

含有 uploadArchives taskbuild.gradle 中加入以下代码,打印下 uploadArchives 的依赖。

void printTaskDependency(Task task) {
    task.getTaskDependencies().getDependencies(task).any() {
        println(">>${it.path}")
        printTaskDependency(it)
    }
}
gradle.getTaskGraph().whenReady {
    printTaskDependency project.tasks.findByName('uploadArchives')
}
复制代码

接着,随便运行一个 gradle 命令,为了方便,直接运行 ./gradlew clean ,查看打印的日志。

uploadArchives 依赖的 tasks:点击查看详细内容
>>:mock-lib:sourcesJar
>>:mock-lib:bundleRelease
>>:mock-lib:mergeReleaseConsumerProguardFiles
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:prepareLintJar
>>:mock-lib:extractReleaseAnnotations
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:transformClassesAndResourcesWithSyncLibJarsForRelease
>>:mock-lib:extractReleaseAnnotations
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:transformResourcesWithMergeJavaResForRelease
>>:mock-lib:processReleaseJavaRes
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:packageReleaseRenderscript
>>:mock-lib:transformNativeLibsWithSyncJniLibsForRelease
>>:mock-lib:transformNativeLibsWithMergeJniLibsForRelease
>>:mock-lib:mergeReleaseJniLibFolders
>>:mock-lib:generateReleaseAssets
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
>>:mock-lib:compileReleaseNdk
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:packageReleaseAssets
>>:mock-lib:generateReleaseAssets
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
复制代码

通过上面打印的信息可以看到依赖的 task 还是蛮多的,我们从前往后一步步排查。 注:每个人打印出来的内容可能不太一样,定义的 task 可能不同。

sourceJar: 先看第一个 task sourceJar ,这个 task 是,我这边自己定义的,用于打包 java 源代码的 task ,因为是自定义的,所以可以忽略,直接看下一个 task 。

bundleRelease: 这个 task 是做什么的呢?大概从字面意思可以猜出和打包有关,我们在 build.gradle 中输入 bundle 看看 IDE 的提示。

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

幸运!果然有相应的提示,直接看到了这个对应的是 AndroidZip 类,毋庸置疑,这个肯定和打包有关。

再往前看看其他的 task: 放眼望去,基本上都是 package*/compile*/generate*/ 之类开头的,看名字就可以才出来这些是做什么的,(手动滑稽脸),我们应该是找到了需要 hook 的 task 了!!!

结果上面的分析和大胆的猜测,我们需要 hook 一下 bundle* 这个task,这个 task 既然是打包用的,那么我们需要在这个打包之前找到字节码存放的位置,然后去 hook 它!!!

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

5.2 hook task

自定义 gradle plugin 的过程和 gradle 的生命周期等等在此处不进行叙述了,有兴趣可以去网上自行了解。

我们在自定义的插件的 afterEvaluate 中寻找 bundle* task:

mProject.afterEvaluate {
    processVariant()
}
void processVariant() throws NotFoundException {
    // variant 一般有 debug 和 release
    mProject.android.libraryVariants.all { variant ->
        process(variant)
    }
}
void process(variant){
    String taskPath = 'bundle' + mVariant.name.capitalize()
    Task bundleTask = mProject.tasks.findByPath(taskPath)
    if (bundleTask == null) {
        throw new RuntimeException("Can not find task ${taskPath}!")
    }
    bundleTask.doFirst {
        // do hook
    }    
}
复制代码

我们在打包之前执行字节码的 hook 即可。

5.3 hook 字节码文件deng

要 hook 字节码文件,我们这边需要考虑以下几个事情。

  • 字节码文件的存储路径在哪?json file
  • 如何改变字节码文件?
  • 要如何改变?

字节码文件的存储路径在哪?

通过一系列查找(我没有找到如何在 gradle 中获取该路径的方法,有大佬知道麻烦告知),最终找到了相对路径: /intermediates/packaged-classes/(release/debug)

如何改变字节码文件?

这边引入了一个第三方库 javassist 去改变字节码文件。

要如何改变?

通过之前 APT 期间生成的 json 文件,遍历字节码文件,找到相应的方法后,改变 modifier@Hide 对应的 modifier ,然后删除 @Hide .

以上问题我们都知道解决的方案了,剩下的就是实施过程了, javassist 的使用方式也在此不再叙述了,有兴趣可以自行去看下,下面列出一些我在写这个插件过程中遇到的一些问题.

问题一、javassist 寻找类的问题

javassist 中,我们去寻找某一个类需要通过一个类 ClassPool 来进行,再次之前我们需要把需要用到的类的 字节码路径 导入到 ClassPool 中,在这里,遇到了第一个问题,在 gradle 项目中有的类是直接缓存在 ~/.gradle/ 文件夹下的,有的类引用的是项目 libs 目录下的,并且有的是 .jar 包,有的是 .aar 包,我们如何去把这些类一一导入?

回答:获取 gradle 的 dependencies 依赖,然后获取依赖的路径,然后加上本地的字节码文件,如果是 .jar 文件,则直接解压到某一个特定的临时文件夹中(task执行完毕后需要删除这些临时文件),如果是 .aar 文件,则先解压 .aar 后再解压其中的 classes.jar 文件.

// 获取 gradle dependencies 的过程
   private List<Configuration> mCopyDependencies
   private void copyDependencies(Configuration configuration) {
       if (configuration == null) {
           return
       }
       Configuration copyConf = null
       try {
           copyConf = mProject.configurations.getByName("${configuration.name}Copy")
       } catch (Exception ignore) {
       }
       if (copyConf == null) {
           copyConf = mProject.configurations.create("${configuration.name}Copy")
       }
       copyConf.visible = false
       copyConf.extendsFrom configuration
       mCopyDependencies.add(copyConf)
   }
   private void configureDependencies() {
       mCopyDependencies = new ArrayList<>()
       copyDependencies(mProject.configurations.getByName("implementation"))
       copyDependencies(mProject.configurations.getByName("api"))
       copyDependencies(mProject.configurations.getByName("compile"))
       copyDependencies(mProject.configurations.getByName("compileOnly"))
       copyDependencies(mProject.configurations.getByName("provided"))
   }
复制代码
// 获取 dependencies 的本地路径
    // 该方法执行在 afterEvaluate 中
    private void resolveArtifacts() {
       def set = new HashSet<>()
       mCopyDependencies.forEach({
           it.each {
               set.add(it.path)
           }
       })
       // ...
   }
复制代码

在此期间,你可以获取/更改/删除你依赖的第三方库,根据需求不同,可以做任何操作.

问题二、方法变为非public了,调用该方法的地方怎么办?

对于这个问题,没有很优雅的处理方式,我这边在 APT 过程中生成了一个反射代理类,一个 @Hide 对应一个反射的方法,并且会对反射进行缓存,保证了每个方法的反射只会调用一次,保证性能.

六、效果演示

library 的目录结构

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

其中的部分类

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

通过该插件生成的 .jar 的目录结构

自定义 gradle plugin,教你如何 hook 系统 task 和字节码
可以看到,这边多了两个 _*RefDelegate 类

,这就是生成的反射代理类.

打出的 jar 包中的部分源码

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

调用 @Hide 的新旧类对比

自定义 gradle plugin,教你如何 hook 系统 task 和字节码

从上面的图片可以看出,生成的 aar/jar 的字节码中,方法的 modifier 已经变为指定的 modifier 了,并且调用的地方也使用反射代理类去进行调用了.

七、总结

对于这次开源来说,总体是失败的,但是在写这个开源的过程中,确实学到了很多东西,知道了如何去 hook 系统的 task ,如何去 hook 字节码等,我觉得更重要的是解决问题的思路,有了问题,如何一步步的去解决它,想自定义一个 gradle 插件,应该从什么地方入手等.

最后,如果大家在看 Seeker 源码的过程中遇到任何问题,可以直接提交 issue ,如果对于文章里面某些内容感兴趣的也可以直接评论哈,我会看情况抽时间写出相应的内容,如果遇到关于 gradle 的一些疑问或者遇到问题,咱们也可以进行探讨~互相学习,互相伤害~

再次厚颜无耻的放上自己的 Seeker Github 传送门 .


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spring实战(第4版)

Spring实战(第4版)

Craig Walls 沃尔斯 / 张卫滨 / 人民邮电出版社 / 2016-4-1 / CNY 89.00

《Spring实战(第4版)》是经典的、畅销的Spring学习和实践指南。 第4版针对Spring 4进行了全面更新。全书分为四部分。第1部分介绍Spring框架的核心知识。第二部分在此基础上介绍了如何使用Spring构建Web应用程序。第三部分告别前端,介绍了如何在应用程序的后端使用Spring。第四部分描述了如何使用Spring与其他的应用和服务进行集成。 《Spring实战(第4......一起来看看 《Spring实战(第4版)》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具