最近困扰我的Java类加载问题
在ArisuBot项目中,需要加载各个功能,这总不可能使用硬编码来使吧……就想到了注解。尝试只用一个注解就可以加载类,写一下一些坑:
0x0 类扫描
0x00 初版
其实是AI给出的主意,大体看起来貌似没什么毛病:
1 | public List<Class> loadClassByLoader(ClassLoader loader) throws URISyntaxException, IOException, ClassNotFoundException { |
实际这个样子不太行,具体问题出在这里:
1 | if (!url.getProtocol().equals("file")) { |
事实上加载出来的是jar包的形式,协议是jar而非file,而jar协议会报错not hierarchal
等会儿手搓一份看起来没问题的。
0x01 来自知乎佬的讲解
原文在这里
递归遍历这个路径获取后缀是.class的文件,然后根据class的名称,路径利用Class.forName加载成Class对象就行了,整个代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 public class Test {
public static void main(String[] args) throws NoSuchMethodException, SecurityException, IOException, ClassNotFoundException {
// 结果 class
List<Class<?>> result = null ;
String scan = "com.hadluo";
// 把 . 转换成 \ 因为下面是文件操作
scan = scan.replaceAll("\\.", "/");
Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources(scan);
while(dirs.hasMoreElements()) {
URL url = dirs.nextElement() ;
if(url.getProtocol().equals("file")) {
List<File> classes = new ArrayList<File>();
// 递归 变量路径下面所有的 class文件
listFiles(new File(url.getFile()),classes);
// 加载我们所有的 class文件 就行了
result= loadeClasses(classes,scan);
}else if(url.getProtocol().equals("jar")) {
// 等下再说
}
}
// 打印结果
for(Class<?> clazz :result) {
System.err.println(clazz.getName());
}
}
private static List<Class<?>> loadeClasses(List<File> classes,String scan) throws ClassNotFoundException {
List<Class<?>> clazzes = new ArrayList<Class<?>>();
for(File file : classes) {
// 因为scan 就是/ , 所有把 file的 / 转成 \ 统一都是: /
String fPath = file.getAbsolutePath().replaceAll("\\\\","/") ;
// 把 包路径 前面的 盘符等 去掉 , 这里必须是lastIndexOf ,防止名称有重复的
String packageName = fPath.substring(fPath.lastIndexOf(scan));
// 去掉后缀.class ,并且把 / 替换成 . 这样就是 com.hadluo.A 格式了 , 就可以用Class.forName加载了
packageName = packageName.replace(".class","").replaceAll("/", ".");
// 根据名称加载类
clazzes.add(Class.forName(packageName));
}
return clazzes ;
}
/** * 查找所有的文件 * * @param dir 路径 * @param fileList 文件集合 */
private static void listFiles(File dir, List<File> fileList) {
if (dir.isDirectory()) {
for (File f : dir.listFiles()) {
listFiles(f, fileList);
}
} else {
if(dir.getName().endsWith(".class")) {
fileList.add(dir);
}
}
}
}
看起来非常靠谱,那么试试看。
这样搞其实有个限制条件,就是所有的包必须放在arisubot.plugins
下,不是很优雅,但是也是目前能想到的比较可行的办法了。
唉,很好奇mirai框架是怎么搞定的。
我有一种感觉,可能我差的这么一点点就在List<Class<?>>
这里,不知道为什么
不太行,还是一下子就跳出来了。
但是他还给了个方法:
如果是jar 我们利用JarURLConnection 类来变量jar里面文件 , 我们接上面的url 来实验下jar的加载:
1
2
3
4
5
6
7
8
9
10 JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = urlConnection.getJarFile().entries();
// 遍历jar
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
//得到该jar文件下面的类实体
System.err.println(entry.getName());
}
欸对哦,我可以直接加载我这个jar包里的文件啊,但是缺点是没有办法加扩展。这不是我关心的问题,第二天直接上试试看。
第二天估计还会更新这篇文章,只要我想得起来。
written 202501020239
我应该可以直接扫描.top
下的所有包,递归那种?但是其他人要想编就不容易了。其他人编要修改扫描范围。不如mirai那种,直接一个目录下解决。
0x02 知乎大佬-改
最后的PluginFinder长这样:
PluginFinder.java
1 | package top.rongxiaoli.backend.Utils; |
0x03 CSDN( )里淘金
原文在这里
被我改成这样了:
1 | package top.rongxiaoli.backend.Utils; |
看起来没啥问题,加载阶段就是有问题,不知道为什么?有可能是加载期间还没有我这个包吧?好怪。
看看单拎出来测试一个包看看行不行。
?也不行。算了,下次换下一种。
唉,只能笨办法了。因为mirai console加载插件的时候,不管是部署环境还是测试环境,都是在根目录下的,所以只要加载的时候使用相对路径./plugins/
找到我的插件就可以了,检查一下版本号就可以了。因为插件版本号格式是确定的,按照x.y.z
加载就可以了。xyz依次是主版本号,次版本号,修订版本号。
2025/01/09 哈哈哈哈哈哈哈哈 破案了 我是傻逼
0x04 错误原因
看这里:
错误:
1 | try { |
输出:
1 | 2025-01-09 16:39:21 D/ArisuBot: Plugin loading. |
正确:
1 | try { |
输出:
1 | 2025-01-09 16:42:39 D/ArisuBot: Plugin loading. |
现在应该就可以一眼看出来了吧。
结案,上传。接下来的主要问题就是类型转换了。
1 | package top.rongxiaoli.backend.Utils; |
顺便测试一下如果是线程的上下文能否检测到这些类。目前的加载代码长这样:
1 | List<Class<?>> reflectClasses = ClassUtil.scan(ArisuBot.class.getPackage(), ArisuBot.GetPluginClassLoader()); |
测试:
1 | List<Class<?>> reflectClasses = ClassUtil.scan(ArisuBot.class.getPackage()); |
我估计是不行的。
确实不太行。但是如果你跟我是同一个包,那么问题就不大,只需要把你的包加入进扫描列表即可。就像这样:
1 | List<Package> checklist = new ArrayList<>(); |
这里是可以扫到Test1的。如果底下还有Test2,也是可以扫得到的。
这就是Java Reflect!!!
完全自动化的插件扫描,适配大型项目的加载方式。插件,爽!
0x1 类转换
0x10 初代
因为我的插件长这样:
flowchart A[PluginBase] --> B["(Static) INSTANCE"] A --> C["[Methods]"] A --> D["Properties"]
这里有个静态的INSTANCE,所以可以很方便地直接将INSTANCE拉入PluginList中,进行管理。
可以,初代就可用,爽。
1 | for (Class<?> clazz : |
搞定,发布。