ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Gson 与 ProGuard/R8 压缩混淆共存验证:test-shrinker 模块集成测试全解析

Gson 与 ProGuard/R8 压缩混淆共存验证:test-shrinker 模块集成测试全解析 后端序列化【免费下载链接】gsonA Java serialization/deserialization library to convert Java Objects into JSON and back项目地址https://gitcode.com/gh_mirrors/gs/gson点击查看免费下载Gson 依赖反射实现 JSON 与 Java 对象互转而 ProGuard、R8 这类代码压缩与混淆工具恰恰会删除未引用代码、重命名类与字段、甚至改写构造逻辑两者天然存在冲突。本仓库中的 test-shrinker 就是为此设立的独立 Maven 模块它在构建阶段真实地用 ProGuard 与 R8 压缩、混淆一组测试程序再通过集成测试运行压缩产物验证 Gson 在被压缩后的代码中依然能正确序列化与反序列化。读完本文你将掌握该模块的构建流水线、三份 ProGuard/R8 规则文件的逐条语义、Gson 自带通用规则 gson.pro 的作用边界以及如何在自己的应用里排查R8 下 Gson 反序列化失败这类典型问题。模块定位为什么需要一个专门验证压缩场景的测试模块根据 test-shrinker/README.md 的说明这是一个 Maven 集成测试模块目的是检查 Gson 在与 ProGuard 或 R8 等代码压缩/混淆工具组合使用时的行为是否正确。其核心设计包含两条硬性约束被压缩的代码放在src/main/java且不应包含任何重要断言。原因在于代码压缩工具可能以意外方式改动这些断言本身例如把assertTrue之类的调用折叠、内联或删除导致断言结果失真。测试的结论判定必须全部放到src/test/java中由测试代码去执行压缩后的 JAR 并核对输出。压缩工具在 Maven 构建期间执行。因此直接从 IDE 运行集成测试往往不可行或者拿到的是上一次构建的过期产物。README 明确提示先执行mvn clean verify再尝试从 IDE 运行集成测试。从模块命名Test: Code shrinking (ProGuard / R8)见 test-shrinker/pom.xml也可以确认它的定位这是一个测试模块父 POM 属性gson.isTestModuletrue并不产出任何可发布的库而是作为 Gson 主库质量的守卫存在。构建流水线一次mvn clean verify里发生了什么test-shrinker/pom.xml 完整定义了构建期的四段式流水线编译 → ProGuard 压缩 → R8 压缩 → Failsafe 集成测试。依赖方面该模块依赖当前仓库的gson主模块版本跟随父 POM 的2.14.1-SNAPSHOT、JUnit 与 Google Truth 断言库同时由于 R8 目前只发布在 Google Maven 仓库POM 中额外声明了maven.google.com作为插件仓库。第一步ProGuard 处理proguard-maven-pluginPOM 使用com.github.wvengen:proguard-maven-plugin版本 2.7.0关键配置如下executions execution phasepackage/phase goalsgoalproguard/goal/goals /execution /executions dependencies !-- 将 ProGuard 升级到插件默认版本之上 -- dependency groupIdcom.guardsquare/groupId artifactIdproguard-base/artifactId version7.9.1/version /dependency dependency groupIdcom.guardsquare/groupId artifactIdproguard-core/artifactId version9.3.2/version /dependency /dependencies configuration obfuscatetrue/obfuscate proguardInclude${project.basedir}/proguard.pro/proguardInclude options option-include/option option${project.basedir}/../gson/src/main/resources/META-INF/proguard/gson.pro/option /options libs lib${java.home}/jmods/java.base.jmod/lib lib${java.home}/jmods/java.sql.jmod/lib !-- Gson 可选 SQL 类型支持 -- lib${java.home}/jmods/java.compiler.jmod/lib !-- Error Prone 注解传递依赖 -- /libs includeDependencyInjartrue/includeDependencyInjar outjarproguard-output.jar/outjar /configuration值得注意的细节显式-include了 Gson 自带的 gson.pro 规则文件。POM 注释直言这是一个hacky solution目前只有 ProGuard 的 Android 插件会自动考虑库内打包的规则文件普通 ProGuard 不会因此这里手工引入而 R8 始终会自动识别该文件。libs指向 JDK 的.jmod文件java.base.jmod、java.sql.jmod、java.compiler.jmod用作 ProGuard 解析 JDK 类的库引用这也解释了为何 JDK 25 且缺失 jmod 文件的环境需要特殊处理见下文 profile。产物输出为target/proguard-output.jar这是集成测试要验证的 ProGuard 压缩 JAR。第二步R8 处理maven-shade-plugin exec-maven-pluginR8 目前没有成熟的 Maven 插件POM 采用两步组合拳maven-shade-plugin3.6.2在package阶段把模块 JAR 与其依赖包括 Gson打成单个带依赖的 fat JAR替换主 JARshadedArtifactAttachedfalse并排除重复的META-INF/MANIFEST.MF与module-info.class。exec-maven-plugin3.6.3在package阶段直接以com.android.tools.r8.R8为主类运行 R8依赖版本 9.4.14声明在插件dependencies中includePluginDependenciestrue使其进入运行类路径参数如下arguments argument--release/argument !-- 生成 Java class 文件而非 Android DEX 文件 -- argument--classfile/argument argument--lib/argumentargument${java.home}/argument argument--pg-conf/argumentargument${project.basedir}/r8.pro/argument !-- 生成 mapping 文件便于调试测试失败 -- argument--pg-map-output/argument argument${project.build.directory}/r8_map.txt/argument argument--output/argument argument${project.build.directory}/r8-output.jar/argument argument${project.build.directory}/${project.build.finalName}.jar/argument /argumentsPOM 注释特别解释了--classfile的意义R8 默认输出 Android DEX 字节码这里指定--classfile让它输出普通 Java class 文件同时由于没有传--pg-compat参数R8 运行在所谓full mode完整模式下优化比 ProGuard 激进得多——这正是下文大量规则差异的根源。产物为target/r8-output.jar。第三步Failsafe 集成测试maven-failsafe-plugin绑定integration-test与verify两个 goal负责执行 ShrinkingIT 这类以IT结尾的测试类验证两个压缩 JAR 的实际行为。第四步JDK 25 的降级 ProfilePOM 末尾定义了JDK25-proguardprofile当运行 JDK 版本 ≥ 25 且${java.home}/jmods/java.base.jmod缺失时JDK 25 不再随 JDK 提供 jmod 文件自动跳过 ProGuard 插件与全部集成测试skipITstrue因为 ProGuard 依赖 jmod 文件才能解析 JDK 类。该 profile 在注释中被标注为临时的等待 Guardsquare 相关 issue 解决后移除。规则文件三件套common.pro / proguard.pro / r8.pro模块根目录下有三份规则文件职责分工明确common.pro 存放 ProGuard 与 R8 公用的规则proguard.pro 与 r8.pro 分别存放各自特有规则。common.pro两者共用的基础规则common.pro 开头的注释划定了边界这里只放该集成测试自身需要的规则对全体 Gson 用户通用的规则不应写在这里而应放在 Gson 的META-INF/proguard下即 gson.pro。其具体内容-allowaccessmodification -dontusemixedcaseclassnames # Windows 上混合大小写类名可能引发问题 -dontnote module-info,jdk.internal.** # 忽略关于重复 JDK 类的提示 # 保留测试入口点 -keep class com.example.Main { public static void runTests(...); } -keep class com.example.NoSerializedNameMain { public static java.lang.String runTestNoArgsConstructor(); public static java.lang.String runTestNoJdkUnsafe(); public static java.lang.String runTestHasArgsConstructor(); } # 保留无注解但应当保留的字段 -keepclassmembers class com.example.ClassWithNamedFields { !transient fields; } -keepclassmembernames class com.example.ClassWithExposeAnnotation { fields; } -keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation { ** f; } -keepclassmembernames class com.example.ClassWithVersionAnnotations { fields; } # 保留类名以便压缩后通过反射检查该类是否仍然存在 -keepnames class com.example.UnusedClass关键点解读-keep与-keepclassmembers的区别-keep连类带成员一起保留-keepclassmembers只保留指定成员允许压缩器删除类本身-keepclassmembernames更进一步只保留成员名字而不阻止压缩/优化适用于类会被保留、成员会被重命名但必须保留字段名的场景。-keepnames class com.example.UnusedClass的目的是在压缩后还能通过类名反射查到该类供 ShrinkingIT 的testUnusedClassRemoved反向验证未被 keep 的未使用类是否被删除。-keepclassmembers class com.example.ClassWithNamedFields { !transient fields; }中的!transient是 ProGuard 通配符语法表示排除 transient 修饰的字段。proguard.proProGuard 特有规则proguard.pro 只有一小段-include common.pro # 与 R8 不同ProGuard 不会执行使类变得抽象的激进优化 # 因此反序列化只需保留字段名即可 -keepclassmembernames class com.example.NoSerializedNameMain$TestClassNoArgsConstructor { fields; } -keepclassmembernames class com.example.NoSerializedNameMain$TestClassNotAbstract { fields; } -keepclassmembernames class com.example.NoSerializedNameMain$TestClassHasArgsConstructor { fields; }其注释点明了 ProGuard 与 R8 在优化策略上的核心差异ProGuard 不会把类优化成抽象类所以在未使用SerializedName的类即不被 Gson 默认规则覆盖的类场景下只需用-keepclassmembernames保留字段名Gson 就能依靠反射按原名读写字段完成反序列化。r8.proR8 full mode 特有规则test-shrinker/r8.pro 则要为 R8 full mode 的激进优化补上额外规则-include common.pro # full mode 下带泛型参数的类需要 keep 规则保留泛型签名 -keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass -keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass # 不混淆类名以便在异常消息中检查 -keep,allowshrinking,allowoptimization class com.example.NoSerializedNameMain$TestClassNoArgsConstructor -keep,allowshrinking,allowoptimization class com.example.NoSerializedNameMain$TestClassHasArgsConstructor # 该规则有副作用R8 仍会移除无参构造器但不会使类抽象化 -keep class com.example.NoSerializedNameMain$TestClassNotAbstract { com.google.gson.annotations.SerializedName fields; } # 保留代码中未显式使用的枚举常量 -keepclassmembers class com.example.EnumClass { ** SECOND; }这些规则背后的成因泛型签名R8 full mode 中-keepattributes只有与-keep匹配的类/字段配合才会生效GenericClass与GenericUsingGenericClass带类型参数T若泛型签名Signature 属性被抹除Gson 通过TypeToken解析具体类型如GenericClassDummyClass就会失败因此必须用-keep,...规则保住它们allowobfuscation等标志表示只要保留类/签名允许继续做缩小、优化、混淆、访问修饰符调整。保留类名用于异常断言TestClassNoArgsConstructor与TestClassHasArgsConstructor被-keep,allowshrinking,allowoptimization保留类名是为了让 R8 压缩后 Gson 抛出的异常消息中仍能出现完整类名从而让集成测试可以按精确消息断言见下文无 SerializedName 的典型失败场景。枚举常量SECOND在代码中从不显式引用反序列化时以 JSON 字符串SECOND触发若不 keepR8 会因未被引用而删除该常量导致反序列化枚举失败。Gson 自带的通用规则gson.pro上文多次提到的 gson.pro 是 Gson 库自身随包分发的规则文件对所有 Gson 用户生效R8 自动识别ProGuard 经本模块 hacky 方式显式 include。其注释明确两点规则是增量式的不包含任何与 Gson 无关的内容如全局禁止混淆且并非完整方案用户仍需为自己的类补充规则。它覆盖四类场景保留元数据-keepattributes Signature类型解析所需泛型签名-keepattributes RuntimeVisibleAnnotations,AnnotationDefaultGson 注解需在运行时可见。TypeToken 相关-keep,allowobfuscation class com.google.gson.reflect.TypeToken以及-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken保证匿名/具名TypeToken子类的泛型签名不被抹除——这也是 R8 full mode 下泛型反序列化能工作的前提。注解驱动的类与字段JsonAdapter标注的类整体 keepExpose、JsonAdapter、Since、Until标注的字段用-keepclassmembers,allowobfuscation保留允许混淆因为这类用户通常会搭配SerializedName固定 JSON 名。适配器类的无参构造器TypeAdapter、TypeAdapterFactory、JsonSerializer、JsonDeserializer的实现类保留init()因为JsonAdapter默认通过无参构造实例化适配器。SerializedName 字段及配套无参构造器通过-if class *条件规则只要类中存在SerializedName字段就保留这些字段并额外保留该类的无参构造器——这能显著缓解 R8 把无参构造器优化掉而导致的实例化失败问题。被压缩的测试代码src/main/java 的设计入口与防优化技巧Main 与 TestExecutorMain.java 是 ProGuard/R8 压缩程序的主入口runTests(BiConsumerString, String)把每个测试用例的输出名称, 内容键值对交给消费者由集成测试统一校验。注释明确不要在压缩代码里做任何重要断言全部输出交给集成测试验证因为压缩器可能影响断言行为。TestExecutor.java 提供了两个关键助手public static void run(BiConsumerString, String outputConsumer, String name, SupplierString resultSupplier) { String result; try { result resultSupplier.get(); } catch (Throwable t) { throw new RuntimeException(Test failed: name, t); } outputConsumer.accept(name, result); } /** 返回 t但以一种希望能阻止压缩器简化它的方式 */ public static T T same(T t) { return Optional.of(t).map(v - Optional.of(v).get()).orElseThrow(() - new AssertionError(unreachable)); }same()是刻意为之的反优化手法它本质上就是return t但包裹了Optional冗余代码。目的如注释所述是让 ProGuard/R8 无法静态推断这里传入了反射要用的Class对象从而阻止压缩器据此把反射调用简化为直接调用或删除相关类。Main中所有toJson/fromJson辅助方法也都经过same()再传给 Gson见 Main.java 的注释hopefully in a way which prevents code shrinkers from understanding that reflection is used。Main.runTests覆盖的用例矩阵对应 ShrinkingIT 的完整预期输出包括TypeToken 写读匿名new TypeTokenListClassWithAdapter(){}与TypeToken.getParameterized(...)手动构造两种方式——按需创建 TypeToken因为泛型签名被抹除时创建会失败命名字段 / SerializedName无注解字段与SerializedName字段的读写构造器四种形态无参构造、有参构造、未被引用代码中从不 new的无参/有参构造——后者用于检验 Gson 的UnsafeAllocator等实例化路径在压缩后的可靠性禁用 JDK Unsafenew GsonBuilder().disableJdkUnsafe().create()场景枚举普通枚举与带SerializedName的枚举注解族Expose配合excludeFieldsWithoutExposeAnnotation、Since/Until配合setVersion(1)、字段级JsonAdapter覆盖 TypeAdapter、TypeAdapterFactory、JsonSerializer、JsonDeserializer 四种形态泛型GenericClassT、UsingGenericClass、GenericUsingGenericClassT的 TypeToken 反序列化接口实现反序列化TypeTokenListInterfaceWithImplementation.Implementation。字段级JsonAdapter测试类 ClassWithJsonAdapterAnnotation.java 特意设计了只注册JsonSerializer或只注册JsonDeserializer的字段用以验证 Gson 的委托回退行为例如 f4 只有反序列化器序列化时回退到反射。无 SerializedName 场景NoSerializedNameMainNoSerializedNameMain.java 覆盖了一类 Gson 用户最容易踩坑的场景类的字段完全没有SerializedName注解因此不被默认 gson.pro 规则匹配。它定义三个静态嵌套测试类TestClassNoArgsConstructor带隐式无参构造器只有一个public String sTestClassNotAbstract同样只有一个字段r8.pro 中专门为它设计规则让 R8 移除无参构造器但保持类非抽象TestClassHasArgsConstructor显式声明有参构造器从而隐式无参构造器不存在。三个入口方法分别对应三种反序列化路径默认new Gson()、禁用 JDK Unsafe 的GsonBuilder、以及有参构造器依赖 Unsafe 绕过构造器场景。集成测试如何验证压缩产物ShrinkingITShrinkingIT.java 是验证端其设计有几大亮点参数化双 JAR 与隔离类加载测试用RunWith(Parameterized.class)对target/proguard-output.jar与target/r8-output.jar两个产物分别执行Parameters返回两个路径。Before阶段先检查 JAR 文件是否存在不存在则直接fail并提示请用mvn clean verify运行。执行时通过new URLClassLoader(new URL[]{jarToTest.toUri().toURL()}, null)以bootstrap class loader 为父加载器加载压缩 JAR确保测试所用的自定义类全部来自被压缩的 JAR 而不是本测试模块的类路径依赖从而真正验证压缩产物而非未压缩代码。精确输出断言test()将Main.runTests的全部输出拼接后与一段极长的期望字符串逐字节比对ShrinkingIT.java包括myField: 2、adapter-1、{tread-1}等细节任何字段名被混淆、字段丢失、适配器失效都会导致断言失败。期望值中有一处值得注意的条件分支Read: Interface implementation, // TODO: 目前只有 ProGuard 可用R8 不行 isTestingProGuard() ? value : ClassCastException,即接口实现反序列化用例目前仅在 ProGuard 下成功R8 会抛ClassCastException——这是 Gson 已知问题源码 TODO 指向 google/gson#2658测试用捕获异常并返回标记字符串的方式将其固化为当前预期行为而非让整条测试红掉。无 SerializedName 的典型失败场景三个testNoSerializedName_*测试针对 ProGuard 与 R8 采用完全不同的断言ProGuard三个入口均返回value反序列化成功R8 full mode由于 R8 更激进的优化如把类变成抽象类、移除无参构造器断言抛出InvocationTargetException且精确匹配 Gson 的异常消息Abstract classes cant be instantiated! Adjust the R8 configuration or register an InstanceCreator or a TypeAdapter for this type. Class name: com.example.NoSerializedNameMain$TestClassNoArgsConstructor并指向 Troubleshooting.md 的 R8 章节禁用 JDK Unsafe 时Unable to create instance of class ...; usage of JDK Unsafe is disabled. ... adjust your R8 configuration to keep the no-args constructor of the class.这说明 Gson 为 R8 用户内置了可诊断的错误提示当你遇到这类异常按提示补充 keep 规则保留无参构造器、保留字段名或注册InstanceCreator/TypeAdapter即可。未使用类删除验证testUnusedClassRemoved通过assumeFalse(isTestingProGuard())只对 R8 生效注释说明 ProGuard 会保留未使用类利用-keepnames保留的类名com.example.UnusedClass反射加载并断言抛出ClassNotFoundException确认压缩器确实删除了未引用类——同时反向验证-keepnames规则没有阻止删除。调试、维护与实操建议从 IDE 运行的正确姿势由于压缩发生在 Maven 构建期README 与verifyJarExists的失败消息都指向同一结论先在模块根目录或仓库根目录执行mvn clean verify让target/proguard-output.jar与target/r8-output.jar就位再运行ShrinkingIT。若改动了规则文件后忘记重新构建IDE 会静默使用过期 JAR造成改规则无效的假象。失败调试利用 mapping 文件README 明确推荐测试失败时查看target目录下 ProGuard 与 R8 生成的 mapping 文件。其中 R8 的 mapping 输出路径由 POM 显式指定为target/r8_map.txt--pg-map-output参数ProGuard 同样会在target下产出 mapping。通过 mapping 可以查出混淆后的类/字段名对照断言中的原始名称快速定位是哪条 keep 规则缺失。脆弱性认知与维护准则README 坦率地指出这套测试尤其是 R8 测试设置可能比较脆弱未来的 ProGuard/R8 版本可能改变行为导致测试结果不同。因此仓库给出了明确的维护指引必要时重写测试甚至在无法适配新版本时移除它们。这也解释了 r8.pro 中若干副作用型规则如TestClassNotAbstract那条-keep会同时阻止 R8 移除无参构造器的连带效应与JDK25-proguardprofile 的存在——它们都是与具体压缩器版本行为博弈的产物。面向你自己的项目的实践要点Gson 打包的 gson.pro 覆盖了通用场景但正如其注释所说并不完整没有SerializedName的类、需要保留的特定字段/无参构造器、自定义泛型类等仍要像本模块的 proguard.pro 与 r8.pro 那样自行补充 keep 规则R8 full mode 与 ProGuard 行为不同泛型类要显式 keep 以保留 Signature 属性被代码引用但不影响 Gson 反射的枚举常量、无参构造器也可能被删除需要按需 keep若反序列化报 Abstract classes cant be instantiated! 或 Unable to create instance优先排查 R8 是否移除了无参构造器或使类抽象化可参考 Troubleshooting.md 中对应的 R8 小节也可像本模块那样为类注册InstanceCreator或TypeAdapter作为替代方案。赞分享后端序列化【免费下载链接】gsonA Java serialization/deserialization library to convert Java Objects into JSON and back项目地址https://gitcode.com/gh_mirrors/gs/gson点击查看免费下载相关推荐Gson 与代码压缩/混淆工具的集成测试test-shrinker 模块深度解析ProGuard 与 R8Gson 与代码压缩/混淆工具的集成测试test shrinker 模块深度解析ProGuard 与 R8 本篇文章围绕 Gson 仓库中的 test s后端Gson Android适配ProGuard和R8混淆配置Gson Android适配ProGuard和R8混淆配置 在Android应用开发中使用GsonJava序列化/反序列化库时代码混淆ProGuar后端序列化Gson 与 ProGuard/R8 混淆配置实战指南官方 android-proguard-example 深度解析Gson 与 ProGuard/R8 混淆配置实战指南官方 android proguard example 深度解析 Gson 依赖 Java 反射访问类的后端上一篇Zsh Codex让AI在命令行为你写代码的终极ZSH插件下一篇Containerd容器镜像签名安全指南与Cosign集成的终极防护方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表