在ArisuBot项目中,需要加载各个功能,这总不可能使用硬编码来使吧……就想到了注解。尝试只用一个注解就可以加载类,写一下一些坑:

0x0 类扫描

0x00 初版

其实是AI给出的主意,大体看起来貌似没什么毛病:

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
public List<Class> loadClassByLoader(ClassLoader loader) throws URISyntaxException, IOException, ClassNotFoundException {
List<Class> out = new ArrayList<>();
Enumeration<URL> urls;
urls = loader.getResources("");
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (!url.getProtocol().equals("file")) {
ArisuBot.INSTANCE.getLogger().verbose(url.getFile());
break;
};
File urlTarget = new File(url.toURI());
out.addAll(loadClassByPath(null, urlTarget.getPath(), out, loader));
}
return out;
}
public List<Class> loadClassByPath(String root, String path, List<Class> list, ClassLoader loader) throws ClassNotFoundException {
List<Class> out = new ArrayList<>(list);
File f = new File(path);
if (root == null) root = f.getPath();
if (f.isFile() && f.getName().matches("^.*\\.class$") && f.getPath().contains("base")) {
String classPath = f.getPath();
String className = classPath.substring(root.length() + 1, classPath.length() - 6).replace("/", ".").replace("\\", ".");
try {
out.add(loader.loadClass(className));
return out;
} catch (ClassNotFoundException e) {
throw e;
}
} else {
File[] fs = f.listFiles(); // Directory.
if (fs == null) return out;
for (File file : fs) loadClassByPath(root, file.getPath(), list, loader);
return out;
}
}

实际这个样子不太行,具体问题出在这里:

1
2
3
4
if (!url.getProtocol().equals("file")) {
break;
}
File urlTarget = new File(url.toURI());

事实上加载出来的是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
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
57
58
59
60
61
62
63
64
65
66
67
68
package top.rongxiaoli.backend.Utils;

import top.rongxiaoli.ArisuBot;
import top.rongxiaoli.backend.interfaces.Plugin;
import top.rongxiaoli.backend.interfaces.PluginBase.PluginBase;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

public class PluginFinder {
public List<PluginBase> getList() {
return PluginList;
}
List<PluginBase> PluginList = new ArrayList<>();
public void scan(String scanPackName) throws IOException, URISyntaxException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
String scan = scanPackName.replaceAll("\\.", "/");
List<Class<?>> result = new ArrayList<>();
Enumeration<URL> dirs = ArisuBot.class.getClassLoader().getResources("");
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
if (url.getProtocol().equals("file")) {
List<File> classes = new ArrayList<>();
listFiles(new File(url.toURI()), classes);
result = loadClasses(classes, scan);
}
}
for (Class<?> clazz : result) {
if (clazz.isAnnotationPresent(Plugin.class)) {
judgeClass(clazz);
}
}
}
private void judgeClass(Class<?> clazz) throws NoSuchFieldException, IllegalAccessException {
for (Class<?> interfaze : clazz.getInterfaces()) {
if (interfaze.getName().contains("PluginBase")) {
Field instanceField = clazz.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
PluginList.add((PluginBase) instanceField.get(null));
}
}
}
private List<Class<?>> loadClasses(List<File> classes, String scan) throws ClassNotFoundException {
List<Class<?>> clazzes = new ArrayList<>();
for (File file : classes) {
String fPath = file.getAbsolutePath().replaceAll("\\\\", "/");
String packageName = fPath.substring(classes.lastIndexOf(scan));
packageName = packageName.replace(".class", "").replaceAll("/", ".");
clazzes.add(Class.forName(packageName));
}
return clazzes;
}

private 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);
}
}
}

0x03 CSDN( )里淘金

原文在这里

被我改成这样了:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package top.rongxiaoli.backend.Utils;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class PluginFinder {
public List<Class<?>> scan(Package pkg) {
String pName = pkg.getName();
return getClasses(pName);

}
private List<Class<?>> getClasses(String packageName) {
return getClasses(packageName, true);
}
private List<Class<?>> getClasses(String packageName, final boolean recursive) {
List<Class<?>> out = new ArrayList<>();
String packDirName = packageName.replace(".", "/");
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources("packDirName");
scanClassMainLoop(dirs, packageName, recursive, out, packDirName);
} catch (IOException e) {
throw new RuntimeException(e);
}
return out;
}
private void scanClassMainLoop(Enumeration<URL> dirs, String packageName, final boolean recursive, List<Class<?>> classes, String packageDirName) throws IOException {
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if (protocol.equals("file")) {
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findClassesByFile(packageName, filePath, recursive, classes);
} else if (protocol.equals("jar")) {
getClassByJarFile(url, packageDirName, packageName, recursive, classes);
}
}
}

private void processClassFile(File singleFile, List<Class<?>> classes, String packageName) {
String className = singleFile.getName().substring(0, singleFile.getName().length() - 6);
try {
classes.add(Class.forName(packageName + "." + className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private void getClassByJarFile(URL url, String packageDirName, String packageName, boolean recursive, List<Class<?>> classes) {
JarFile jar;
try {
jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') name = name.substring(1);
if (name.startsWith(packageDirName)) processJarFile(name, packageName, recursive, entry, classes);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void processJarFile(String name, String packageName, boolean recursive, JarEntry entry, List<Class<?>> classes) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0,idx).replace('/', '.');
}
if ((idx != -1) || recursive) {
judgeIfClass(name, packageName, entry, classes);
}
}
private void judgeIfClass(String name, String packageName, JarEntry entry, List<Class<?>> classes) {
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

private void findClassesByFile(String packageName, String packagePath, final boolean recursive, List<Class<?>> classes) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] files = dir.listFiles(f -> (recursive && f.isDirectory()) || (f.getName().endsWith(".class")));
for (File singleFile :
files) {
if (singleFile.isDirectory()) {
findClassesByFile(packageName + "." + singleFile.getName(), singleFile.getAbsolutePath(), recursive, classes);
} else {
processClassFile(singleFile, classes, packageName);
}
}
}
}

看起来没啥问题,加载阶段就是有问题,不知道为什么?有可能是加载期间还没有我这个包吧?好怪。

看看单拎出来测试一个包看看行不行。

?也不行。算了,下次换下一种。

唉,只能笨办法了。因为mirai console加载插件的时候,不管是部署环境还是测试环境,都是在根目录下的,所以只要加载的时候使用相对路径./plugins/找到我的插件就可以了,检查一下版本号就可以了。因为插件版本号格式是确定的,按照x.y.z加载就可以了。xyz依次是主版本号,次版本号,修订版本号。

2025/01/09 哈哈哈哈哈哈哈哈 破案了 我是傻逼

0x04 错误原因

看这里:

错误:

1
2
3
4
5
6
try {
dirs = loader.getResources("packDirName");
scanClassMainLoop(dirs, packageName, recursive, out, packDirName);
} catch (IOException e) {
throw new RuntimeException(e);
}

输出:

1
2
3
4
5
2025-01-09 16:39:21 D/ArisuBot: Plugin loading.
2025-01-09 16:39:21 V/ArisuBot: Found 0 plugins.
2025-01-09 16:39:21 V/ArisuBot: Plugin load complete.
2025-01-09 16:39:21 V/ArisuBot: Registering listener host.
2025-01-09 16:39:21 D/ArisuBot: Initialization complete.

正确:

1
2
3
4
5
6
try {
dirs = loader.getResources(packDirName);
scanClassMainLoop(dirs, packageName, recursive, out, packDirName);
} catch (IOException e) {
throw new RuntimeException(e);
}

输出:

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
2025-01-09 16:42:39 D/ArisuBot: Plugin loading.
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.ArisuBot
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.Commands.ArisuBotAbstractCompositeCommand
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.Commands.ArisuBotAbstractRawCommand
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.Commands.ArisuBotAbstractSimpleCommand
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.EventListener
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.interfaces.Plugin
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.interfaces.PluginBase.PluginBase
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.interfaces.PluginBase.PluginConfigBase
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.interfaces.PluginBase.PluginDataBase
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.PluginLoader.ConfigLoader
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.PluginLoader.DataLoader
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.PluginLoader.PluginLoader
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.Utils.AnnotationUtil
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.Utils.ClassUtil
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.backend.Utils.UserJudgeUtils
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.Broadcast.Broadcast
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailyFortune.DailyFortune
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailyFortune.UploadFileFilter
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySign
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySignData
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySignString$1
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySignString
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySignTimer$DataSaveTimer
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySignTimer$SignCountTimer
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.DailySign.DailySignTimer
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.EmergencyStop.EmergencyStop
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.Management.Management
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.DelayedDisposer$CoolingUser
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.DelayedDisposer$DelayConsumer
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.DelayedDisposer$ElementAlreadyExistsException
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.DelayedDisposer
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.PictureAPIDataStruct$Data
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.PictureAPIDataStruct$Urls
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.PictureAPIDataStruct
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PicturesPlugin.PicturesPlugin
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.Ping.Ping
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PokeReact.backend.PokeReactTextConfig$1
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PokeReact.backend.PokeReactTextConfig$2
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PokeReact.backend.PokeReactTextConfig
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PokeReact.PokeBack
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PokeReact.PokeReact
2025-01-09 16:42:39 D/ArisuBot.PluginLoader: Found top.rongxiaoli.plugins.PokeReact.SayRandom
2025-01-09 16:42:39 V/ArisuBot: Found 0 plugins.
2025-01-09 16:42:39 V/ArisuBot: Plugin load complete.
2025-01-09 16:42:39 V/ArisuBot: Registering listener host.
2025-01-09 16:42:39 D/ArisuBot: Initialization complete.

现在应该就可以一眼看出来了吧。

结案,上传。接下来的主要问题就是类型转换了。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package top.rongxiaoli.backend.Utils;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class ClassUtil {
public static List<Class<?>> scan(Package pkg) {
String pName = pkg.getName();
return getClasses(pName);
}
public static List<Class<?>> scan(String pkg) {
return getClasses(pkg);
}
public static List<Class<?>> scan(Package pkg, ClassLoader loader) {
String pName = pkg.getName();
return getClasses(pName, loader);
}
private static List<Class<?>> getClasses(String packageName) {
return getClasses(packageName, Thread.currentThread().getContextClassLoader());
}
private static List<Class<?>> getClasses(String packageName, ClassLoader loader) {
return getClasses(packageName, loader, true);
}
private static List<Class<?>> getClasses(String packageName,ClassLoader loader, final boolean recursive) {
List<Class<?>> out = new ArrayList<>();
String packDirName = packageName.replace(".", "/");
Enumeration<URL> dirs;
try {
dirs = loader.getResources(packDirName);
scanClassMainLoop(dirs, packageName, recursive, out, packDirName);
} catch (IOException e) {
throw new RuntimeException(e);
}
return out;
}
private static void scanClassMainLoop(Enumeration<URL> dirs, String packageName, final boolean recursive, List<Class<?>> classes, String packageDirName) throws IOException {
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if (protocol.equals("file")) {
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findClassesByFile(packageName, filePath, recursive, classes);
} else if (protocol.equals("jar")) {
getClassByJarFile(url, packageDirName, packageName, recursive, classes);
}
}
}

private static void processClassFile(File singleFile, List<Class<?>> classes, String packageName) {
String className = singleFile.getName().substring(0, singleFile.getName().length() - 6);
try {
classes.add(Class.forName(packageName + "." + className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private static void getClassByJarFile(URL url, String packageDirName, String packageName, boolean recursive, List<Class<?>> classes) {
JarFile jar;
try {
jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') name = name.substring(1);
if (name.startsWith(packageDirName)) processJarFile(name, packageName, recursive, entry, classes);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void processJarFile(String name, String packageName, boolean recursive, JarEntry entry, List<Class<?>> classes) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0,idx).replace('/', '.');
}
if ((idx != -1) || recursive) {
judgeIfClass(name, packageName, entry, classes);
}
}
private static void judgeIfClass(String name, String packageName, JarEntry entry, List<Class<?>> classes) {
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

private static void findClassesByFile(String packageName, String packagePath, final boolean recursive, List<Class<?>> classes) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] files = dir.listFiles(f -> (recursive && f.isDirectory()) || (f.getName().endsWith(".class")));
for (File singleFile :
files) {
if (singleFile.isDirectory()) {
findClassesByFile(packageName + "." + singleFile.getName(), singleFile.getAbsolutePath(), recursive, classes);
} else {
processClassFile(singleFile, classes, packageName);
}
}
}
}

顺便测试一下如果是线程的上下文能否检测到这些类。目前的加载代码长这样:

1
2
3
4
5
6
List<Class<?>> reflectClasses = ClassUtil.scan(ArisuBot.class.getPackage(), ArisuBot.GetPluginClassLoader());
for (Class<?> clazz :
reflectClasses) {
LOGGER.debug("Found " + clazz.getName());
}
ArisuBot.INSTANCE.getLogger().verbose("Found " + PluginList.size() + " plugins. ");

测试:

1
2
3
4
5
6
List<Class<?>> reflectClasses = ClassUtil.scan(ArisuBot.class.getPackage());
for (Class<?> clazz :
reflectClasses) {
LOGGER.debug("Found " + clazz.getName());
}
ArisuBot.INSTANCE.getLogger().verbose("Found " + PluginList.size() + " plugins. ");

我估计是不行的。

确实不太行。但是如果你跟我是同一个包,那么问题就不大,只需要把你的包加入进扫描列表即可。就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List<Package> checklist = new ArrayList<>();
// Package scan list start.
checklist.add(ArisuBot.class.getPackage());
checklist.add(Test1.class.getPackage());
// Package scan list end.

LOGGER.debug("Loading classes from packages below: ");
List<Class<?>> reflectClasses = new ArrayList<>();
for (Package pack :
checklist) {
LOGGER.debug(pack.toString());
reflectClasses.addAll(ClassUtil.scan(pack, ArisuBot.GetPluginClassLoader()));
}
LOGGER.verbose("Scanning " + reflectClasses.size() + " classes for plugins. ");

for (PluginBase e :
PluginList) {
LOGGER.verbose("Found plugin: " + e.getClass().getName());
}
ArisuBot.INSTANCE.getLogger().verbose("Found " + PluginList.size() + " plugins. ");

这里是可以扫到Test1的。如果底下还有Test2,也是可以扫得到的。

这就是Java Reflect!!!

完全自动化的插件扫描,适配大型项目的加载方式。插件,爽!

0x1 类转换

0x10 初代

因为我的插件长这样:

flowchart

A[PluginBase] --> B["(Static) INSTANCE"]
A --> C["[Methods]"]
A --> D["Properties"]

这里有个静态的INSTANCE,所以可以很方便地直接将INSTANCE拉入PluginList中,进行管理。

可以,初代就可用,爽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (Class<?> clazz :
reflectClasses) {
if (!clazz.isAnnotationPresent(Plugin.class)) {
continue;
}
try {
Field f = clazz.getDeclaredField("INSTANCE");
f.setAccessible(true);
PluginList.add((PluginBase) f.get(null));
INSTANCE.registerCommand((Command) f.get(null), false);
} catch (NoSuchFieldException | IllegalAccessException e) {
LOGGER.error("Cannot load class " + clazz.getName() + " because field \"INSTANCE\" was not found or is inaccessible.");
LOGGER.error("Exception details: ", e);
} catch (ClassCastException e) {
LOGGER.warning("Cannot cast " + clazz.getName() + " to " + Command.class.getName());
}

搞定,发布。

2025-01-02

⬆︎TOP