在开发 Bukkit 插件库的过程中,有时会需要用到插件主类实例的情况。举一个栗子:

1
2
3
4
5
registerEventListeners(this) {
event<PlayerJoinEvent> {
player.sendMessage("Hello!")
}
}

因为事件监听器的注册需要绑定在插件上,对于库/前置插件开发者来说,需要每次用到插件实例时让调用者提供。看起来没什么不对的,但这里有一个更好的方法。我们先来分析下插件是如何加载的。

插件注册分析

服务器插件的加载是由 CraftServerloadPlugins 开始的。

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
public final class CraftServer implements Server {
public void loadPlugins() {
this.pluginManager.registerInterface(JavaPluginLoader.class);
File pluginFolder = (File)this.console.options.valueOf("plugins");
if (pluginFolder.exists()) {
Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder);
Plugin[] var6 = plugins;
int var5 = plugins.length;

for(int var4 = 0; var4 < var5; ++var4) {
Plugin plugin = var6[var4];

try {
String message = String.format("Loading %s", plugin.getDescription().getFullName());
plugin.getLogger().info(message);
plugin.onLoad();
} catch (Throwable var8) {
Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, var8.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", var8);
}
}
} else {
pluginFolder.mkdir();
}

}
}

首先,CraftServer 调用了其内部持有的 SimplePluginManagerregisterInterface(JavaPluginLoader.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
public final class SimplePluginManager implements PluginManager {
public void registerInterface(Class<? extends PluginLoader> loader) throws IllegalArgumentException {
PluginLoader instance;

if (PluginLoader.class.isAssignableFrom(loader)) {
Constructor<? extends PluginLoader> constructor;

try {
constructor = loader.getConstructor(Server.class);
instance = constructor.newInstance(server);
} catch (NoSuchMethodException ex) {
String className = loader.getName();

throw new IllegalArgumentException(String.format("Class %s does not have a public %s(Server) constructor", className, className), ex);
} catch (Exception ex) {
throw new IllegalArgumentException(String.format("Unexpected exception %s while attempting to construct a new instance of %s", ex.getClass().getName(), loader.getName()), ex);
}
} else {
throw new IllegalArgumentException(String.format("Class %s does not implement interface PluginLoader", loader.getName()));
}

Pattern[] patterns = instance.getPluginFileFilters();

synchronized (this) {
for (Pattern pattern : patterns) {
fileAssociations.put(pattern, instance);
}
}
}
}

这一步SimplePluginManager 负责 new 出 JavaPluginLoader 的实例,并且将插件名字过滤正则与 JavaPluginLoader 实例对应。(默认情况下 PluginLoader 接口仅有一个实现类 JavaPluginLoader

1
2
3
public final class JavaPluginLoader implements PluginLoader {
private final Pattern[] fileFilters = [Pattern.compile("\\.jar$")];
}

JavaPluginLoader 只含有一个正则表达式,用于过滤插件文件夹中的文件。
接着,CraftServer 会调用 SimplePluginManagerloadPlugins(File) 方法。

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
public Plugin[] loadPlugins(File directory) {
Validate.notNull(directory, "Directory cannot be null");
Validate.isTrue(directory.isDirectory(), "Directory must be a directory");

List<Plugin> result = new ArrayList<Plugin>();
Set<Pattern> filters = fileAssociations.keySet();

if (!(server.getUpdateFolder().equals(""))) {
updateDirectory = new File(directory, server.getUpdateFolder());
}

Map<String, File> plugins = new HashMap<String, File>();
Set<String> loadedPlugins = new HashSet<String>();
Map<String, Collection<String>> dependencies = new HashMap<String, Collection<String>>();
Map<String, Collection<String>> softDependencies = new HashMap<String, Collection<String>>();

// This is where it figures out all possible plugins
for (File file : directory.listFiles()) {
PluginLoader loader = null;
for (Pattern filter : filters) {
Matcher match = filter.matcher(file.getName());
if (match.find()) {
loader = fileAssociations.get(filter);
}
}

if (loader == null) continue;

PluginDescriptionFile description = null;
try {
description = loader.getPluginDescription(file);
String name = description.getName();
if (name.equalsIgnoreCase("bukkit") || name.equalsIgnoreCase("minecraft") || name.equalsIgnoreCase("mojang")) {
server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "': Restricted Name");
continue;
} else if (description.rawName.indexOf(' ') != -1) {
server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "': uses the space-character (0x20) in its name");
continue;
}
} catch (InvalidDescriptionException ex) {
server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "'", ex);
continue;
}

File replacedFile = plugins.put(description.getName(), file);
if (replacedFile != null) {
server.getLogger().severe(String.format(
"Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'",
description.getName(),
file.getPath(),
replacedFile.getPath(),
directory.getPath()
));
}
//省去通过依赖分析来决定加载顺序。
return result.toArray(new Plugin[result.size()]);
}

该方法会找到目录下所有 jar 文件,并调用 JavaPluginLoadergetPluginDescription(File) 方法,找到目录下所有插件的描述文件。下一步会根据 softDependencydependency 处理各个插件的加载顺序,并进行一系列的合法性校验,并对每个合法文件调用 loadPlugin(File) 方法。

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
public final class SimplePluginManager implements PluginManager {
public synchronized Plugin loadPlugin(File file) throws InvalidPluginException, UnknownDependencyException {
Validate.notNull(file, "File cannot be null");

checkUpdate(file);

Set<Pattern> filters = fileAssociations.keySet();
Plugin result = null;

for (Pattern filter : filters) {
String name = file.getName();
Matcher match = filter.matcher(name);

if (match.find()) {
PluginLoader loader = fileAssociations.get(filter);

result = loader.loadPlugin(file);
}
}

if (result != null) {
plugins.add(result);
lookupNames.put(result.getDescription().getName(), result);
}

return result;
}

当正则匹配时,方法调用了 fileAssociations 中存的 loader,也就是 JavaPluginLoaderloadPlugin(File) 方法。

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
public final class JavaPluginLoader implements PluginLoader {
public Plugin loadPlugin(final File file) throws InvalidPluginException {
Validate.notNull(file, "File cannot be null");

if (!file.exists()) {
throw new InvalidPluginException(new FileNotFoundException(file.getPath() + " does not exist"));
}

final PluginDescriptionFile description;
try {
description = getPluginDescription(file);
} catch (InvalidDescriptionException ex) {
throw new InvalidPluginException(ex);
}

final File parentFile = file.getParentFile();
final File dataFolder = new File(parentFile, description.getName());
@SuppressWarnings("deprecation")
final File oldDataFolder = new File(parentFile, description.getRawName());

// Found old data folder
if (dataFolder.equals(oldDataFolder)) {
// They are equal -- nothing needs to be done!
} else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) {
server.getLogger().warning(String.format(
"While loading %s (%s) found old-data folder: `%s' next to the new one `%s'",
description.getFullName(),
file,
oldDataFolder,
dataFolder
));
} else if (oldDataFolder.isDirectory() && !dataFolder.exists()) {
if (!oldDataFolder.renameTo(dataFolder)) {
throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'");
}
server.getLogger().log(Level.INFO, String.format(
"While loading %s (%s) renamed data folder: `%s' to `%s'",
description.getFullName(),
file,
oldDataFolder,
dataFolder
));
}

if (dataFolder.exists() && !dataFolder.isDirectory()) {
throw new InvalidPluginException(String.format(
"Projected datafolder: `%s' for %s (%s) exists and is not a directory",
dataFolder,
description.getFullName(),
file
));
}

for (final String pluginName : description.getDepend()) {
Plugin current = server.getPluginManager().getPlugin(pluginName);

if (current == null) {
throw new UnknownDependencyException(pluginName);
}
}

final PluginClassLoader loader;
try {
loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file);
} catch (InvalidPluginException ex) {
throw ex;
} catch (Throwable ex) {
throw new InvalidPluginException(ex);
}

loaders.add(loader);

return loader.plugin;
}
}

这里对插件数据文件夹进行诸如创建之类的操作,并且检测当前插件的依赖是否全部已经被加载。接下来最重要的一步为 new 出 PluginClassLoader

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
final class PluginClassLoader extends URLClassLoader {
PluginClassLoader(final JavaPluginLoader loader, final ClassLoader parent, final PluginDescriptionFile description, final File dataFolder, final File file) throws IOException, InvalidPluginException, MalformedURLException {
super(new URL[] {file.toURI().toURL()}, parent);
Validate.notNull(loader, "Loader cannot be null");

this.loader = loader;
this.description = description;
this.dataFolder = dataFolder;
this.file = file;
this.jar = new JarFile(file);
this.manifest = jar.getManifest();
this.url = file.toURI().toURL();

try {
Class<?> jarClass;
try {
jarClass = Class.forName(description.getMain(), true, this);
} catch (ClassNotFoundException ex) {
throw new InvalidPluginException("Cannot find main class `" + description.getMain() + "'", ex);
}

Class<? extends JavaPlugin> pluginClass;
try {
pluginClass = jarClass.asSubclass(JavaPlugin.class);
} catch (ClassCastException ex) {
throw new InvalidPluginException("main class `" + description.getMain() + "' does not extend JavaPlugin", ex);
}

plugin = pluginClass.newInstance();
} catch (IllegalAccessException ex) {
throw new InvalidPluginException("No public constructor", ex);
} catch (InstantiationException ex) {
throw new InvalidPluginException("Abnormal plugin type", ex);
}
}
}

PluginClassLoader 构造器中校验插件主类,并使用反射在本类加载器下创建实例。

1
2
3
4
5
6
7
8
9
public abstract class JavaPlugin extends PluginBase {
public JavaPlugin() {
final ClassLoader classLoader = this.getClass().getClassLoader();
if (!(classLoader instanceof PluginClassLoader)) {
throw new IllegalStateException("JavaPlugin requires " + PluginClassLoader.class.getName());
}
((PluginClassLoader) classLoader).initialize(this);
}
}

JavaPlugin 的构造器中,其调用了 PluginClassLoaderinitialize(JavaPlugin) 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final class PluginClassLoader extends URLClassLoader {
synchronized void initialize(JavaPlugin javaPlugin) {
Validate.notNull(javaPlugin, "Initializing plugin cannot be null");
Validate.isTrue(javaPlugin.getClass().getClassLoader() == this, "Cannot initialize plugin outside of this class loader");
if (this.plugin != null || this.pluginInit != null) {
throw new IllegalArgumentException("Plugin already initialized!", pluginState);
}

pluginState = new IllegalStateException("Initial initialization");
this.pluginInit = javaPlugin;

javaPlugin.init(loader, loader.server, description, dataFolder, file, this);
}
}

pluginState 居然是 IllegalArgumentException,我觉得布星。
这里 initialize(JavaPlugin) 又调用了 JavaPlugininit(PluginLoader, Server, PluginDescriptionFile, File, File, ClassLoader)

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class JavaPlugin extends PluginBase {
final void init(PluginLoader loader, Server server, PluginDescriptionFile description, File dataFolder, File file, ClassLoader classLoader) {
this.loader = loader;
this.server = server;
this.file = file;
this.description = description;
this.dataFolder = dataFolder;
this.classLoader = classLoader;
this.configFile = new File(dataFolder, "config.yml");
this.logger = new PluginLogger(this);
}
}

该方法传入了一些参数,供插件编写者使用。至此,一个插件就加载好了。

大雾

了解一个插件是如何被加载后,我们可以从属于该插件代码内的任何一个实例获取插件主类的实例。直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Jaba {
public static JavaPlugin jaba(Object object) {
ClassLoader classLoader = object.getClass().getClassLoader();
try {
Class<?> loaderClass = Class.forName("org.bukkit.plugin.java.PluginClassLoader");
if(!loaderClass.isInstance(classLoader))
throw new IllegalPluginAccessException();
Field pluginField = loaderClass.getField("plugin");
pluginField.setAccessible(true);
return (JavaPlugin) pluginField.get(classLoader);
} catch(ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
throw new IllegalPluginAccessException(e.getMessage());
}
}
}

万恶的 PluginClassLoader 是 package-private 的,所以需要通过反射访问。只是这段代码是 100% 不可能用到的。