refactor(app-packages): 重构 ApkMetaParser 为静态工具类并移除冗余字段

- 移除 ApkMetaParser 内部类 ApkMetaResult,提取为独立类
- 移除 Spring @Component 注解,所有方法改为静态方法
- 删除未使用的 maxSdk 字段及相关解析逻辑
- 移除 AppPackageServiceImpl 中注入的 ApkMetaParser 依赖
- 更新测试用例以适配静态方法调用
- 标记 parseByBundleTool 和 fillBundleIcon 方法为 @Deprecated
This commit is contained in:
2026-08-12 18:18:01 +08:00
parent 40fae0bf0d
commit 3db6c17511
5 changed files with 87 additions and 54 deletions

View File

@@ -1,11 +1,9 @@
package com.youlai.boot.app.packages.component;
import com.android.tools.build.bundletool.commands.DumpCommand;
import lombok.Data;
import net.dongliu.apk.parser.ApkFile;
import net.dongliu.apk.parser.bean.ApkMeta;
import net.dongliu.apk.parser.bean.Icon;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -39,31 +37,12 @@ import java.util.zip.ZipInputStream;
* 解包后定位主 APK 复用 APK 解析逻辑,再补充 JSON 描述信息</li>
* </ul>
*/
@Component
public class ApkMetaParser {
@Data
public static class ApkMetaResult {
private String packageName;
private String appLabel;
private String versionName;
private Integer versionCode;
private Integer minSdk;
private Integer targetSdk;
private Integer compileSdk;
private Integer maxSdk;
private List<String> abiList = new ArrayList<>();
private List<String> permissions = new ArrayList<>();
private String iconPath;
private byte[] iconBytes;
/** 容器格式apks/apkm/xapk内解析出的 split apk 列表,便于审计 */
private List<String> splitApkList = new ArrayList<>();
}
/**
* 统一解析入口:根据文件扩展名分发到对应解析策略。
*/
public ApkMetaResult parse(File packageFile) throws IOException {
public static ApkMetaResult parse(File packageFile) throws IOException {
String name = packageFile.getName().toLowerCase(Locale.ROOT);
int dot = name.lastIndexOf('.');
String ext = dot >= 0 ? name.substring(dot + 1) : "";
@@ -82,7 +61,7 @@ public class ApkMetaParser {
/**
* 解析 APK 单包apk-parser 解析二进制 AndroidManifest.xml
*/
public ApkMetaResult parseApk(File apkFile) throws IOException {
public static ApkMetaResult parseApk(File apkFile) throws IOException {
ApkMetaResult result = new ApkMetaResult();
try (ApkFile apkFileParser = new ApkFile(apkFile)) {
apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE);
@@ -103,7 +82,7 @@ public class ApkMetaParser {
* 直接使用 bundletool 依赖(非命令行、非 jar的 {@link DumpCommand} 输出
* base 模块的 AndroidManifest.xml 文本,再提取关键字段。
*/
public ApkMetaResult parseAab(File aabFile) throws IOException {
public static ApkMetaResult parseAab(File aabFile) throws IOException {
ApkMetaResult result = parseByBundleTool(aabFile);
// 从 AAB 的 base 模块资源目录res/mipmap-*、res/drawable-*)提取应用图标
fillBundleIcon(result, aabFile, true);
@@ -118,7 +97,7 @@ public class ApkMetaParser {
* bundletool 依赖可直接解析 .apks 整包并 dump 其 Manifest同时枚举 ZIP 内
* 的 apk 条目记录 split 列表。
*/
public ApkMetaResult parseApks(File apksFile) throws IOException {
public static ApkMetaResult parseApks(File apksFile) throws IOException {
ApkMetaResult result = parseByBundleTool(apksFile);
result.setSplitApkList(collectApkEntries(apksFile));
// 图标位于内部各 split apk 的 res/mipmap-* 或 res/drawable-*,逐包扫描
@@ -139,7 +118,7 @@ public class ApkMetaParser {
* <li>补充解析 JSON 描述文件</li>
* </ol>
*/
public ApkMetaResult parseContainer(File containerFile) throws IOException {
public static ApkMetaResult parseContainer(File containerFile) throws IOException {
List<ContainerEntry> apks = new ArrayList<>();
byte[] manifestJson = null;
byte[] infoJson = null;
@@ -218,7 +197,7 @@ public class ApkMetaParser {
* <li>APKMAPKMirrorinfo.json 描述的 base.apk</li>
* </ul>
*/
private ContainerEntry locateMainApk(List<ContainerEntry> apks) {
private static ContainerEntry locateMainApk(List<ContainerEntry> apks) {
// 优先 base-master.apk / base.apk
for (String candidate : new String[]{"base-master.apk", "base.apk"}) {
for (ContainerEntry e : apks) {
@@ -246,7 +225,8 @@ public class ApkMetaParser {
* AndroidManifest.xml文本形式写入内存再由 {@link #parseManifestXmlText} 提取字段。
* 支持 .aab 与 .apks 两种 Google 标准 bundle。
*/
private ApkMetaResult parseByBundleTool(File bundleFile) throws IOException {
@Deprecated
private static ApkMetaResult parseByBundleTool(File bundleFile) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8)) {
DumpCommand.builder()
@@ -263,7 +243,7 @@ public class ApkMetaParser {
/**
* 枚举 zip 内所有 .apk 条目名(用于 APKS 记录 split 列表)。
*/
private List<String> collectApkEntries(File zipFile) {
private static List<String> collectApkEntries(File zipFile) {
List<String> apkNames = new ArrayList<>();
try (ZipFile zf = new ZipFile(zipFile)) {
Enumeration<? extends ZipEntry> entries = zf.entries();
@@ -288,7 +268,8 @@ public class ApkMetaParser {
*
* @param isAab true 表示 AAB路径前缀 base/res/false 表示 APKS需解内嵌 apk
*/
private void fillBundleIcon(ApkMetaResult result, File bundleFile, boolean isAab) {
@Deprecated
private static void fillBundleIcon(ApkMetaResult result, File bundleFile, boolean isAab) {
if (result.getIconBytes() != null && result.getIconBytes().length > 0) {
return;
}
@@ -324,7 +305,7 @@ public class ApkMetaParser {
/**
* 用 ZipFile 随机访问扫描:先记录最佳条目名,再按名读取字节。
*/
private IconCandidate scanBestIcon(ZipFile zf, Predicate<String> prefixFilter) {
private static IconCandidate scanBestIcon(ZipFile zf, Predicate<String> prefixFilter) {
String bestName = null;
int bestRank = -1;
Enumeration<? extends ZipEntry> entries = zf.entries();
@@ -345,7 +326,7 @@ public class ApkMetaParser {
/**
* 用 ZipInputStream 流式扫描(用于嵌套 apk同时读入候选图标字节。
*/
private IconCandidate scanBestIconFromStream(ZipInputStream zis, Predicate<String> prefixFilter) {
private static IconCandidate scanBestIconFromStream(ZipInputStream zis, Predicate<String> prefixFilter) {
IconCandidate best = null;
try {
ZipEntry entry;
@@ -364,7 +345,7 @@ public class ApkMetaParser {
return best;
}
private void applyIcon(ApkMetaResult result, ZipFile zf, IconCandidate candidate) {
private static void applyIcon(ApkMetaResult result, ZipFile zf, IconCandidate candidate) {
if (candidate == null) {
return;
}
@@ -388,7 +369,7 @@ public class ApkMetaParser {
* <li>跳过 anydpi / nodpi / *_round 等不适合直接作为图标字节的条目</li>
* </ul>
*/
private int iconRank(String path) {
private static int iconRank(String path) {
String lower = path.toLowerCase(Locale.ROOT);
if (!lower.endsWith(".png") && !lower.endsWith(".webp")) {
return -1;
@@ -423,7 +404,7 @@ public class ApkMetaParser {
/**
* 从字节数组解析 APK用于容器内的主 APK
*/
private ApkMetaResult tryParseApkBytes(byte[] apkBytes) {
private static ApkMetaResult tryParseApkBytes(byte[] apkBytes) {
// ApkFile 仅支持 File / String 路径,故将容器内主 APK 字节落盘为临时文件解析
File tmp = null;
try {
@@ -445,7 +426,7 @@ public class ApkMetaParser {
}
}
private void fillFromApkMeta(ApkMetaResult result, ApkMeta meta) {
private static void fillFromApkMeta(ApkMetaResult result, ApkMeta meta) {
result.setPackageName(meta.getPackageName());
result.setAppLabel(meta.getLabel());
result.setVersionName(meta.getVersionName());
@@ -453,14 +434,13 @@ public class ApkMetaParser {
result.setMinSdk(toInt(meta.getMinSdkVersion()));
result.setTargetSdk(toInt(meta.getTargetSdkVersion()));
result.setCompileSdk(toInt(meta.getCompileSdkVersion()));
result.setMaxSdk(toInt(meta.getMaxSdkVersion()));
if (meta.getUsesPermissions() != null) {
result.setPermissions(new ArrayList<>(meta.getUsesPermissions()));
}
}
private void fillIcon(ApkMetaResult result, ApkFile apkFileParser) {
private static void fillIcon(ApkMetaResult result, ApkFile apkFileParser) {
try {
List<Icon> icons = apkFileParser.getIconFiles();
if (icons != null && !icons.isEmpty()) {
@@ -479,7 +459,7 @@ public class ApkMetaParser {
/**
* 从 bundletool 输出的 AndroidManifest.xml 文本中提取关键字段。
*/
private ApkMetaResult parseManifestXmlText(String manifestXml) {
private static ApkMetaResult parseManifestXmlText(String manifestXml) {
ApkMetaResult result = new ApkMetaResult();
// 包名:<manifest ... package="xxx">
result.setPackageName(attr(manifestXml, "package"));
@@ -492,11 +472,10 @@ public class ApkMetaParser {
result.setMinSdk(toInt(attr(manifestXml, "android:minSdkVersion")));
result.setTargetSdk(toInt(attr(manifestXml, "android:targetSdkVersion")));
result.setCompileSdk(toInt(attr(manifestXml, "android:compileSdkVersion")));
result.setMaxSdk(toInt(attr(manifestXml, "android:maxSdkVersion")));
return result;
}
private String attr(String xml, String name) {
private static String attr(String xml, String name) {
// 兼容 package="x" 与 android:label="x" 两种写法
String pattern = name + "=\"";
int idx = xml.indexOf(pattern);
@@ -515,7 +494,7 @@ public class ApkMetaParser {
* 从容器内的 JSON 描述文件补充信息XAPK 的 manifest.json / APKM 的 info.json
* 仅做轻量提取:包名 / 版本名 / 版本号缺失时回填。
*/
private void supplementFromJson(ApkMetaResult result, byte[] jsonBytes) {
private static void supplementFromJson(ApkMetaResult result, byte[] jsonBytes) {
String json = new String(jsonBytes, StandardCharsets.UTF_8);
if (result.getPackageName() == null || result.getPackageName().isBlank()) {
String pkg = jsonStringField(json, "package");
@@ -543,7 +522,7 @@ public class ApkMetaParser {
}
}
private String jsonStringField(String json, String key) {
private static String jsonStringField(String json, String key) {
// 简单匹配 "key" : "value" 或 "key":"value"
String pattern = "\"" + key + "\"\\s*:\\s*\"";
int idx = json.indexOf(pattern);
@@ -558,7 +537,7 @@ public class ApkMetaParser {
return json.substring(start, end);
}
private Integer jsonIntField(String json, String key) {
private static Integer jsonIntField(String json, String key) {
String pattern = "\"" + key + "\"\\s*:\\s*";
int idx = json.indexOf(pattern);
if (idx < 0) {
@@ -582,7 +561,7 @@ public class ApkMetaParser {
/**
* 扫描 apk 内 lib/<abi>/ 目录,提取支持的 CPU 架构列表。
*/
private List<String> extractAbiList(File apkFile) {
private static List<String> extractAbiList(File apkFile) {
Set<String> abis = new LinkedHashSet<>();
try (ZipFile zf = new ZipFile(apkFile)) {
Enumeration<? extends ZipEntry> entries = zf.entries();
@@ -601,7 +580,7 @@ public class ApkMetaParser {
return new ArrayList<>(abis);
}
private Integer toInt(Object value) {
private static Integer toInt(Object value) {
if (value == null) {
return null;
}

View File

@@ -0,0 +1,26 @@
package com.youlai.boot.app.packages.component;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Android 安装包解析结果。
*/
@Data
public class ApkMetaResult {
private String packageName;
private String appLabel;
private String versionName;
private Integer versionCode;
private Integer minSdk;
private Integer targetSdk;
private Integer compileSdk;
private List<String> abiList = new ArrayList<>();
private List<String> permissions = new ArrayList<>();
private String iconPath;
private byte[] iconBytes;
/** 容器格式apks/apkm/xapk内解析出的 split apk 列表,便于审计 */
private List<String> splitApkList = new ArrayList<>();
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.youlai.boot.app.packages.component.ApkMetaParser;
import com.youlai.boot.app.packages.component.ApkMetaResult;
import com.youlai.boot.app.packages.model.document.AppPackageDocument;
import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
import com.youlai.boot.app.packages.model.vo.AppPackageVO;
@@ -34,7 +35,6 @@ import java.util.List;
public class AppPackageServiceImpl implements AppPackageService {
private final AppPackageRepository appPackageRepository;
private final ApkMetaParser apkMetaParser;
private static final List<String> ALLOWED_EXT = List.of("apk", "apks", "aab", "apkm", "xapk");
@@ -84,7 +84,7 @@ public class AppPackageServiceImpl implements AppPackageService {
if ("apk".equalsIgnoreCase(ext)) {
try {
ApkMetaParser.ApkMetaResult meta = apkMetaParser.parse(storedFile);
ApkMetaResult meta = ApkMetaParser.parse(storedFile);
builder.packageName(firstNonBlank(meta.getPackageName(), packageName))
.appLabel(meta.getAppLabel())
.versionName(firstNonBlank(meta.getVersionName(), versionName))
@@ -114,7 +114,7 @@ public class AppPackageServiceImpl implements AppPackageService {
} else {
// aab / xapk / apks / apkm统一交由解析器解包内部 APK
try {
ApkMetaParser.ApkMetaResult meta = apkMetaParser.parse(storedFile);
ApkMetaResult meta = ApkMetaParser.parse(storedFile);
builder.packageName(firstNonBlank(meta.getPackageName(), packageName))
.appLabel(meta.getAppLabel())
.versionName(firstNonBlank(meta.getVersionName(), versionName))