# jar-scaner-kit
一个jar包扫描工具
## 功能说明
1. 扫描jar包中的类文件,并输出包名、类名、方法名
2. 对比两个jar包中的类文件,输出差异信息
3. 扫描指定目录下的所有jar包,检索指定类名、包名、方法名
4. 扫描指定目录下的所有jar包,输出类冲突信息
## 使用方法
```
# 查看帮助
java -jar jar-scaner-kit-1.0.0.jar -h
# 扫描jar包中的类文件,并输出包名、类名、方法名
java -jar jar-scaner-kit-1.0.0.jar -f /data/test/jar-1.0.0.jar
# 对比两个jar包中的类文件,输出差异信息
java -jar jar-scaner-kit-1.0.0.jar -f /data/test/jar-1.0.0.jar -f2 /data/test/jar-1.0.2.jar
# 扫描指定目录下的所有jar包输出类冲突信息
java -jar jar-scaner-kit-1.0.0.jar -d /data/test/
# 检索指定类名、包名、方法名
java -jar jar-scaner-kit-1.0.0.jar -d /data/test/ -c com.test.TestClass
```
1
package com.jnliok.jarscaner;
/**
* 方法信息模型(与 classgraph 生命周期解耦)
*/
public class MethodModel {
/** 方法名 */
public final String name;
/** 格式化后的方法签名,例如:public void hello(java.lang.String): java.lang.String */
public final String signature;
public MethodModel(String name, String signature) {
this.name = name;
this.signature = signature;
}
public String getName() {
return name;
}
public String getSignature() {
return signature;
}
@Override
public String toString() {
return signature;
}
}
package com.jnliok.jarscaner;
import java.io.File;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/**
* jar 相关工具类
*/
public final class JarUtil {
private JarUtil() {
}
/**
* 判断文件是否为 jar 文件(不区分大小写)
*/
public static boolean isJarFile(File file) {
return file.isFile() && file.getName().toLowerCase().endsWith(".jar");
}
/**
* 递归扫描目录,收集所有 jar 文件,按路径排序
*/
public static List<File> listJars(File dir) {
List<File> result = new ArrayList<>();
if (dir == null || !dir.isDirectory()) {
return result;
}
collectJars(dir, result);
result.sort((a, b) -> a.getPath().compareTo(b.getPath()));
return result;
}
private static void collectJars(File file, List<File> result) {
File[] children = file.listFiles();
if (children == null) {
return;
}
for (File child : children) {
if (child.isDirectory()) {
collectJars(child, result);
} else if (isJarFile(child)) {
result.add(child);
}
}
}
/**
* 将 class 文件路径转换为类全限定名,例如 com/foo/Bar.class -> com.foo.Bar
*/
public static String classPathToName(String path) {
return path.substring(0, path.length() - ".class".length()).replace('/', '.');
}
/**
* 计算字节数组的 MD5(十六进制小写)
*/
public static String md5(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(data);
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xff);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("MD5 算法不可用", e);
}
}
}
package com.jnliok.jarscaner;
import java.util.List;
/**
* 类信息模型(与 classgraph 生命周期解耦)
*/
public class ClassModel {
/** 包名 */
public final String packageName;
/** 类全限定名 */
public final String name;
/** 类简单名 */
public final String simpleName;
/** 用户可见的方法列表(已过滤合成/桥方法,已排序) */
public final List<MethodModel> methods;
public ClassModel(String packageName, String name, String simpleName, List<MethodModel> methods) {
this.packageName = packageName;
this.name = name;
this.simpleName = simpleName;
this.methods = methods;
}
}
2
package com.jnliok.jarscaner;
/**
* jar-scaner-kit 入口类,负责命令行参数解析与分发
*/
public class Main {
public static void main(String[] args) {
Options opts = new Options();
String error = opts.parse(args);
if (error != null) {
System.out.println("[错误] " + error);
System.out.println();
printHelp();
return;
}
if (opts.help) {
printHelp();
return;
}
if (opts.jar1 != null && opts.jar2 != null) {
JarComparator.compare(opts.jar1, opts.jar2);
} else if (opts.jar1 != null) {
JarScanner.scanAndPrint(opts.jar1);
} else if (opts.dir != null) {
if (opts.pattern != null) {
ClassSearcher.search(opts.dir, opts.pattern);
} else {
ConflictDetector.detect(opts.dir);
}
} else {
printHelp();
}
}
private static void printHelp() {
System.out.println("========================================");
System.out.println(" jar-scaner-kit - Jar 包扫描工具 v1.0.0");
System.out.println("========================================");
System.out.println("用法:");
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -h");
System.out.println(" 查看帮助");
System.out.println();
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -f <jar路径>");
System.out.println(" 扫描 jar 包中的类文件,输出包名、类名、方法名");
System.out.println();
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -f <jar路径1> -f2 <jar路径2>");
System.out.println(" 对比两个 jar 包,输出类差异信息");
System.out.println();
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -d <目录>");
System.out.println(" 扫描目录下所有 jar 包,输出类冲突信息");
System.out.println();
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -d <目录> -c <类名/包名/方法名>");
System.out.println(" 扫描目录下所有 jar 包,检索指定的类名、包名或方法名");
System.out.println();
System.out.println("参数说明:");
System.out.println(" -h 显示帮助信息");
System.out.println(" -f <path> 指定 jar 包文件路径");
System.out.println(" -f2 <path> 指定第二个 jar 包文件路径(与 -f 一起使用进行对比)");
System.out.println(" -d <path> 指定扫描目录");
System.out.println(" -c <pattern> 检索模式:类名(全限定名/简单名)、包名或方法名");
System.out.println();
System.out.println("示例:");
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -f /data/test/jar-1.0.0.jar");
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -f /data/test/jar-1.0.0.jar -f2 /data/test/jar-1.0.2.jar");
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -d /data/test/");
System.out.println(" java -jar jar-scaner-kit-1.0.0.jar -d /data/test/ -c com.test.TestClass");
}
/**
* 命令行参数
*/
private static class Options {
boolean help;
String jar1;
String jar2;
String dir;
String pattern;
/**
* 解析参数,返回错误信息(null 表示解析成功)
*/
String parse(String[] args) {
for (int i = 0; i < args.length; i++) {
String arg = args[i];
switch (arg) {
case "-h":
case "--help":
help = true;
break;
case "-f":
if (i + 1 >= args.length) {
return "参数 -f 缺少值";
}
jar1 = args[++i];
break;
case "-f2":
if (i + 1 >= args.length) {
return "参数 -f2 缺少值";
}
jar2 = args[++i];
break;
case "-d":
if (i + 1 >= args.length) {
return "参数 -d 缺少值";
}
dir = args[++i];
break;
case "-c":
if (i + 1 >= args.length) {
return "参数 -c 缺少值";
}
pattern = args[++i];
break;
default:
return "未知参数: " + arg;
}
}
return null;
}
}
}
3
package com.jnliok.jarscaner;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.MethodInfo;
import io.github.classgraph.MethodParameterInfo;
import io.github.classgraph.MethodTypeSignature;
import io.github.classgraph.ScanResult;
import io.github.classgraph.TypeSignature;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* 功能1:扫描 jar 包中的类文件,输出包名、类名、方法名
*/
public final class JarScanner {
private JarScanner() {
}
/**
* 扫描单个 jar 并输出类信息(包名、类名、方法名)
*/
public static void scanAndPrint(String jarPath) {
File jarFile = new File(jarPath);
if (!jarFile.isFile()) {
System.out.println("[错误] 文件不存在或不是有效文件: " + jarPath);
return;
}
List<ClassModel> classes = scanJar(jarPath);
System.out.println("Jar 文件: " + jarFile.getAbsolutePath());
System.out.println("扫描到 " + classes.size() + " 个类");
System.out.println("========================================");
Set<String> packageNames = new TreeSet<>();
Set<String> classNames = new TreeSet<>();
for (ClassModel cm : classes) {
packageNames.add(cm.packageName);
classNames.add(cm.name);
printClass(cm);
}
System.out.println("----------------------------------------");
System.out.println("## 共 " + packageNames.size() + " 个包名");
packageNames.forEach(System.out::println);
System.out.println("----------------------------------------");
System.out.println("## 共 " + classNames.size() + " 个类名");
classNames.forEach(System.out::println);
}
/**
* 扫描单个 jar 文件,返回按类名排序的类模型列表。
* 所有 classgraph 对象的访问都必须在 ScanResult 打开期间完成。
*/
public static List<ClassModel> scanJar(String jarPath) {
List<ClassModel> models = new ArrayList<>();
try (ScanResult scanResult = new ClassGraph()
.enableAllInfo()
.overrideClasspath(jarPath)
.scan()) {
List<ClassInfo> classes = new ArrayList<>(scanResult.getAllClasses());
classes.sort(Comparator.comparing(ClassInfo::getName));
for (ClassInfo ci : classes) {
models.add(toModel(ci));
}
}
return models;
}
/**
* 将 ClassInfo 转换为与 ScanResult 生命周期解耦的 ClassModel
*/
private static ClassModel toModel(ClassInfo ci) {
List<MethodModel> methods = new ArrayList<>();
for (MethodInfo m : ci.getMethodInfo()) {
if (m.isSynthetic() || m.isBridge()) {
continue;
}
methods.add(new MethodModel(m.getName(), formatMethod(m)));
}
// 先按方法名、再按签名排序;使用显式方法引用让泛型推断更明确
methods.sort(Comparator
.comparing(MethodModel::getName)
.thenComparing(mm -> mm.signature));
return new ClassModel(ci.getPackageName(), ci.getName(), ci.getSimpleName(), methods);
}
/**
* 打印单个类:包名、类名、方法名
*/
private static void printClass(ClassModel cm) {
System.out.println("包名: " + cm.packageName);
System.out.println("类名: " + cm.name);
if (cm.methods.isEmpty()) {
System.out.println("方法: (无)");
} else {
for (MethodModel mm : cm.methods) {
System.out.println("方法: " + mm.signature);
}
}
System.out.println("----------------------------------------");
}
/**
* 格式化方法签名,例如:public void method(int, java.lang.String): int
*/
private static String formatMethod(MethodInfo m) {
StringBuilder sb = new StringBuilder();
String mods = m.getModifiersStr();
if (mods != null && !mods.isEmpty()) {
sb.append(mods).append(' ');
}
String name = m.isConstructor() ? m.getClassInfo().getSimpleName() : m.getName();
sb.append(name).append('(');
MethodParameterInfo[] params = m.getParameterInfo();
for (int i = 0; i < params.length; i++) {
if (i > 0) {
sb.append(", ");
}
TypeSignature ts = params[i].getTypeDescriptor();
sb.append(ts != null ? ts.toString() : "?");
}
sb.append(')');
MethodTypeSignature sig = m.getTypeDescriptor();
if (sig != null && sig.getResultType() != null) {
sb.append(": ").append(sig.getResultType().toString());
}
return sb.toString();
}
}
4
package com.jnliok.jarscaner;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
/**
* 功能2:对比两个 jar 包中的类文件,输出差异信息
*/
public final class JarComparator {
private JarComparator() {
}
public static void compare(String jarPath1, String jarPath2) {
File f1 = new File(jarPath1);
File f2 = new File(jarPath2);
if (!f1.isFile()) {
System.out.println("[错误] 文件不存在或不是有效文件: " + jarPath1);
return;
}
if (!f2.isFile()) {
System.out.println("[错误] 文件不存在或不是有效文件: " + jarPath2);
return;
}
Map<String, ClassModel> map1 = toMap(JarScanner.scanJar(jarPath1));
Map<String, ClassModel> map2 = toMap(JarScanner.scanJar(jarPath2));
TreeSet<String> onlyIn1 = new TreeSet<>(map1.keySet());
onlyIn1.removeAll(map2.keySet());
TreeSet<String> onlyIn2 = new TreeSet<>(map2.keySet());
onlyIn2.removeAll(map1.keySet());
TreeSet<String> common = new TreeSet<>(map1.keySet());
common.retainAll(map2.keySet());
System.out.println("===== Jar 对比结果 =====");
System.out.println("Jar1: " + f1.getAbsolutePath() + " (" + map1.size() + " 个类)");
System.out.println("Jar2: " + f2.getAbsolutePath() + " (" + map2.size() + " 个类)");
System.out.println();
System.out.println("[仅存在于 Jar1 的类] (" + onlyIn1.size() + "):");
if (onlyIn1.isEmpty()) {
System.out.println(" (无)");
}
for (String name : onlyIn1) {
System.out.println(" " + name);
}
System.out.println();
System.out.println("[仅存在于 Jar2 的类] (" + onlyIn2.size() + "):");
if (onlyIn2.isEmpty()) {
System.out.println(" (无)");
}
for (String name : onlyIn2) {
System.out.println(" " + name);
}
System.out.println();
System.out.println("[两包共有类的方法差异]:");
int methodDiffClassCount = 0;
for (String name : common) {
TreeSet<String> m1 = methodSet(map1.get(name));
TreeSet<String> m2 = methodSet(map2.get(name));
TreeSet<String> diff1 = new TreeSet<>(m1);
diff1.removeAll(m2);
TreeSet<String> diff2 = new TreeSet<>(m2);
diff2.removeAll(m1);
if (diff1.isEmpty() && diff2.isEmpty()) {
continue;
}
methodDiffClassCount++;
System.out.println(" 类: " + name);
for (String s : diff1) {
System.out.println(" 仅 Jar1: " + s);
}
for (String s : diff2) {
System.out.println(" 仅 Jar2: " + s);
}
}
if (methodDiffClassCount == 0) {
System.out.println(" (无)");
}
System.out.println();
System.out.println("========== 汇总 ==========");
System.out.println("新增类: " + onlyIn2.size() + " 个");
System.out.println("删除类: " + onlyIn1.size() + " 个");
System.out.println("方法有变化的类: " + methodDiffClassCount + " 个");
}
private static Map<String, ClassModel> toMap(List<ClassModel> classes) {
Map<String, ClassModel> map = new HashMap<>();
for (ClassModel cm : classes) {
map.put(cm.name, cm);
}
return map;
}
private static TreeSet<String> methodSet(ClassModel cm) {
TreeSet<String> set = new TreeSet<>();
for (MethodModel mm : cm.methods) {
set.add(mm.signature);
}
return set;
}
}
6
package com.jnliok.jarscaner;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 功能4:扫描指定目录下的所有 jar 包,输出类冲突信息
*/
public final class ConflictDetector {
private ConflictDetector() {
}
public static void detect(String dirPath) {
File dir = new File(dirPath);
if (!dir.isDirectory()) {
System.out.println("[错误] 目录不存在或不是有效目录: " + dirPath);
return;
}
List<File> jars = JarUtil.listJars(dir);
if (jars.isEmpty()) {
System.out.println("目录下未找到 jar 文件: " + dir.getAbsolutePath());
return;
}
System.out.println("扫描目录: " + dir.getAbsolutePath());
System.out.println("jar 文件数: " + jars.size());
System.out.println("========================================");
// className -> (jarName -> class 文件 md5)
Map<String, Map<String, String>> classOwners = new TreeMap<>();
for (File jar : jars) {
scanJarClasses(jar, classOwners);
}
int conflictCount = 0;
int duplicateCount = 0;
for (Map.Entry<String, Map<String, String>> entry : classOwners.entrySet()) {
String className = entry.getKey();
Map<String, String> owners = entry.getValue();
if (owners.size() < 2) {
continue;
}
boolean contentSame = new TreeSet<>(owners.values()).size() == 1;
if (contentSame) {
duplicateCount++;
System.out.println("[重复] " + className + " 出现于: " + owners.keySet());
} else {
conflictCount++;
System.out.println("[冲突] " + className + " 出现于:");
for (Map.Entry<String, String> o : owners.entrySet()) {
System.out.println(" " + o.getKey() + " (md5: " + o.getValue() + ")");
}
}
}
System.out.println("========================================");
System.out.println("检测完成: 冲突 " + conflictCount + " 个类, 重复(内容一致) " + duplicateCount + " 个类");
}
private static void scanJarClasses(File jar, Map<String, Map<String, String>> classOwners) {
try (JarFile jarFile = new JarFile(jar)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
if (!name.endsWith(".class") || name.startsWith("META-INF/")) {
continue;
}
String className = JarUtil.classPathToName(name);
if (className.equals("module-info") || className.equals("package-info")
|| className.endsWith(".module-info") || className.endsWith(".package-info")) {
continue;
}
byte[] data = readEntry(jarFile, entry);
String md5 = JarUtil.md5(data);
classOwners.computeIfAbsent(className, k -> new TreeMap<>())
.put(jar.getName(), md5);
}
} catch (IOException e) {
System.out.println("[警告] 无法读取 jar 文件: " + jar.getAbsolutePath() + " (" + e.getMessage() + ")");
}
}
private static byte[] readEntry(JarFile jarFile, JarEntry entry) throws IOException {
try (InputStream in = jarFile.getInputStream(entry);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
}
}
7
package com.jnliok.jarscaner;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* 功能3:扫描指定目录下的所有 jar 包,检索指定的类名、包名或方法名
*/
public final class ClassSearcher {
private ClassSearcher() {
}
public static void search(String dirPath, String pattern) {
File dir = new File(dirPath);
if (!dir.isDirectory()) {
System.out.println("[错误] 目录不存在或不是有效目录: " + dirPath);
return;
}
List<File> jars = JarUtil.listJars(dir);
if (jars.isEmpty()) {
System.out.println("目录下未找到 jar 文件: " + dir.getAbsolutePath());
return;
}
System.out.println("检索模式: " + pattern);
System.out.println("扫描目录: " + dir.getAbsolutePath());
System.out.println("jar 文件数: " + jars.size());
System.out.println("========================================");
int hitJarCount = 0;
int hitClassCount = 0;
for (File jar : jars) {
List<ClassModel> classes = JarScanner.scanJar(jar.getAbsolutePath());
boolean jarHit = false;
StringBuilder sb = new StringBuilder();
for (ClassModel cm : classes) {
List<MethodModel> matchedMethods = match(cm, pattern);
if (matchedMethods == null) {
continue;
}
jarHit = true;
hitClassCount++;
sb.append(" 类: ").append(cm.name).append('\n');
for (MethodModel mm : matchedMethods) {
sb.append(" 方法: ").append(mm.signature).append('\n');
}
}
if (jarHit) {
hitJarCount++;
System.out.println("[命中] " + jar.getAbsolutePath());
System.out.print(sb);
System.out.println("----------------------------------------");
}
}
System.out.println("========================================");
System.out.println("命中 " + hitJarCount + " 个 jar 包, " + hitClassCount + " 个类");
}
/**
* 匹配规则:
* 1. 类全限定名完全匹配
* 2. 类简单名完全匹配
* 3. 包名匹配(等于或以 pattern. 开头)
* 4. 方法名匹配(等于或包含)
* 5. 类简单名包含匹配(模糊检索)
*
* @return 命中的方法列表(类/包名命中时为空列表);未命中返回 null
*/
private static List<MethodModel> match(ClassModel cm, String pattern) {
if (cm.name.equals(pattern) || cm.simpleName.equals(pattern)) {
return new ArrayList<>();
}
if (cm.packageName.equals(pattern) || cm.packageName.startsWith(pattern + ".")) {
return new ArrayList<>();
}
List<MethodModel> matched = new ArrayList<>();
for (MethodModel mm : cm.methods) {
if (mm.name.equals(pattern) || mm.name.contains(pattern)) {
matched.add(mm);
}
}
if (!matched.isEmpty()) {
return matched;
}
if (cm.simpleName.contains(pattern)) {
return new ArrayList<>();
}
return null;
}
}
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jnliok</groupId>
<artifactId>jar-scaner-kit</artifactId>
<version>1.0.0</version>
<name>jar-scaner-kit</name>
<description>一个 jar 包扫描工具</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.181</version>
</dependency>
</dependencies>
<build>
<finalName>jar-scaner-kit-1.0.0</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.jnliok.jarscaner.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
评论区