refactor(app-packages): 重构 ApkMetaParser 为静态工具类并移除冗余字段
- 移除 ApkMetaParser 内部类 ApkMetaResult,提取为独立类 - 移除 Spring @Component 注解,所有方法改为静态方法 - 删除未使用的 maxSdk 字段及相关解析逻辑 - 移除 AppPackageServiceImpl 中注入的 ApkMetaParser 依赖 - 更新测试用例以适配静态方法调用 - 标记 parseByBundleTool 和 fillBundleIcon 方法为 @Deprecated
This commit is contained in:
@@ -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>APKM(APKMirror):info.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;
|
||||
}
|
||||
|
||||
@@ -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<>();
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user