Skip to content

Commit

Permalink
Add a new property turms.plugin.java.duplicate-class-load-strategy
Browse files Browse the repository at this point in the history
…to control how to handle duplicate classes defined by both the Turms server and the plugin when loading classes by the plugin classloader
  • Loading branch information
JamesChenX committed Nov 20, 2024
1 parent 973fddb commit 98afa7d
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2019 The Turms Project
* https://github.com/turms-im/turms
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.turms.server.common.infra.archive;

import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
* @author James Chen
*/
public final class ZipUtils {

private static final byte[] BYTES_EMPTY = new byte[0];

private ZipUtils() {
}

public static byte[] readEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
long size = entry.getSize();
if (size == 0) {
return BYTES_EMPTY;
}
if (size < 0) {
try (InputStream inputStream = zipFile.getInputStream(entry)) {
return inputStream.readAllBytes();
}
}
if (size > Integer.MAX_VALUE) {
throw new IOException(
"The size of the entry must be less than or equal to "
+ Integer.MAX_VALUE
+ ", but got:"
+ size);
}
int sizeInt = (int) size;
byte[] bytes = new byte[sizeInt];
try (InputStream inputStream = zipFile.getInputStream(entry)) {
int bytesRead = inputStream.readNBytes(bytes, 0, sizeInt);
if (bytesRead != sizeInt) {
throw new IOException(
"The size of the entry must be "
+ sizeInt
+ ", but got: "
+ bytesRead);
}
}
return bytes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import im.turms.server.common.infra.collection.CollectionUtil;
import im.turms.server.common.infra.lang.ClassUtil;
import im.turms.server.common.infra.lang.StringUtil;
import im.turms.server.common.infra.property.constant.DuplicateClassLoadStrategy;

/**
* @author James Chen
Expand All @@ -44,6 +45,7 @@ private JavaPluginFactory() {
public static JavaPlugin create(
JavaPluginDescriptor descriptor,
ZipFile zipFile,
DuplicateClassLoadStrategy duplicateClassLoadStrategy,
NodeType nodeType,
ApplicationContext context) {
Map<NodeType, PluginDescriptor.ServerInfo> compatibleServerTypeToInfo =
Expand All @@ -60,7 +62,7 @@ public static JavaPlugin create(
}
// Note that the loader should NOT be closed here
// because it usually needs to load classes and resources in the JAR file later
PluginClassLoader classLoader = new PluginClassLoader(zipFile);
PluginClassLoader classLoader = new PluginClassLoader(zipFile, duplicateClassLoadStrategy);
try {
Class<? extends TurmsPlugin> pluginClass;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
import java.util.zip.ZipFile;

import im.turms.server.common.BaseTurmsApplication;
import im.turms.server.common.infra.archive.ZipUtils;
import im.turms.server.common.infra.collection.CollectionUtil;
import im.turms.server.common.infra.io.InputOutputException;
import im.turms.server.common.infra.lang.PackageConst;
import im.turms.server.common.infra.property.constant.DuplicateClassLoadStrategy;

/**
* @author James Chen
Expand All @@ -53,10 +55,15 @@ public class PluginClassLoader extends ClassLoader implements AutoCloseable {
* purpose, which just waste unnecessary resources.
*/
private final ZipFile zipFile;
private final boolean throwIfFoundDuplicateClass;

public PluginClassLoader(ZipFile zipFile) {
public PluginClassLoader(
ZipFile zipFile,
DuplicateClassLoadStrategy duplicateClassLoadStrategy) {
super(PARENT_CLASS_LOADER);
this.zipFile = zipFile;
throwIfFoundDuplicateClass =
duplicateClassLoadStrategy == DuplicateClassLoadStrategy.THROW_EXCEPTION;
}

@Override
Expand All @@ -81,23 +88,16 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
if (loadedClass.getClassLoader() == this) {
return loadedClass;
}
// Check if the class loaded by the parent class loader is also defined in the JAR file.
// If so, throw an exception to prevent duplicate classes
// because defining different classes of the same name is an error-prone practice:
// 1. If we use the class loaded by the parent class loader, the plugin developer may be
// confused as their defined class in plugin doesn't work.
// 2. If we use the class defined in the JAR file, JVM will throw an error if an instance of
// the class is passed from the plugin to Turms
// (Same class name but different class loaders).
// As a result, we don't use either of them, but throw.
String binaryName = name.replace('.', '/')
.concat(".class");
if (zipFile.getEntry(binaryName) != null) {
throw new ConflictedClassException(
"The class ("
+ name
+ ") in the JAR file conflicts with the class defined by Turms server. "
+ "The plugin developer needs to rename the class in the JAR file to avoid conflicts");
if (throwIfFoundDuplicateClass) {
String binaryName = name.replace('.', '/')
.concat(".class");
if (zipFile.getEntry(binaryName) != null) {
throw new ConflictedClassException(
"The class ("
+ name
+ ") in the JAR file conflicts with the class defined by Turms server. "
+ "The plugin developer needs to rename the class in the JAR file to avoid conflicts");
}
}
return loadedClass;
}
Expand All @@ -118,17 +118,24 @@ public Class<?> findClass(String name) throws ClassNotFoundException {
"Could not find the class: "
+ name);
}
byte[] bytes = new byte[(int) entry.getSize()];
int size;
try (InputStream inputStream = zipFile.getInputStream(entry)) {
size = inputStream.read(bytes);

byte[] bytes;
try {
bytes = ZipUtils.readEntry(zipFile, entry);
} catch (IOException e) {
throw new InputOutputException(
"Failed to load class: "
"Failed to load the class: "
+ name,
e);
}
try {
return defineClass(name, bytes, 0, bytes.length);
} catch (Throwable e) {
throw new InputOutputException(
"Failed to create the class: "
+ name,
e);
}
return defineClass(name, bytes, 0, size);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@
import im.turms.server.common.infra.plugin.invoker.SequentialExtensionPointInvoker;
import im.turms.server.common.infra.plugin.invoker.SimultaneousExtensionPointInvoker;
import im.turms.server.common.infra.property.TurmsPropertiesManager;
import im.turms.server.common.infra.property.constant.DuplicateClassLoadStrategy;
import im.turms.server.common.infra.property.constant.PluginType;
import im.turms.server.common.infra.property.env.common.plugin.JavaPluginProperties;
import im.turms.server.common.infra.property.env.common.plugin.JsPluginDebugProperties;
import im.turms.server.common.infra.property.env.common.plugin.JsPluginProperties;
import im.turms.server.common.infra.property.env.common.plugin.NetworkPluginProperties;
Expand All @@ -97,6 +99,7 @@ public class PluginManager implements ApplicationListener<ContextRefreshedEvent>
private final Path pluginDir;

private final boolean allowSaveJavaPlugins;
private final DuplicateClassLoadStrategy duplicateClassLoadStrategy;

private final boolean allowSaveJsPlugins;
private final boolean isJsEnabled;
Expand Down Expand Up @@ -130,6 +133,11 @@ public PluginManager(
enabled = pluginProperties.isEnabled();
pluginRepository = new PluginRepository();
pluginDir = ensurePluginDirExists(applicationContext.getHome(), pluginProperties.getDir());

JavaPluginProperties javaPluginProperties = pluginProperties.getJava();
allowSaveJavaPlugins = javaPluginProperties.isAllowSave();
duplicateClassLoadStrategy = javaPluginProperties.getDuplicateClassLoadStrategy();

isJsEnabled = ClassUtil.exists("org.graalvm.polyglot.Engine");
PluginFinder.FindResult findResult = PluginFinder.find(pluginDir, isJsEnabled);
List<ZipFile> zipFiles = findResult.zipFiles();
Expand All @@ -148,8 +156,6 @@ public PluginManager(
}
throw exception;
}
allowSaveJavaPlugins = pluginProperties.getJava()
.isAllowSave();
if (isJsEnabled) {
JsPluginProperties jsPluginProperties = pluginProperties.getJs();
JsPluginDebugProperties debugProperties = jsPluginProperties.getDebug();
Expand Down Expand Up @@ -494,7 +500,8 @@ public void loadJavaPlugins(List<MultipartFile> files, boolean save) {
+ fileName
+ ") because it is not a Java plugin JAR file");
}
Plugin plugin = JavaPluginFactory.create(descriptor, zipFile, nodeType, context);
Plugin plugin = JavaPluginFactory
.create(descriptor, zipFile, duplicateClassLoadStrategy, nodeType, context);
initAndRegisterPlugin(plugin);
} catch (Exception e) {
try {
Expand All @@ -514,7 +521,8 @@ private void loadJavaPlugins(List<ZipFile> zipFiles) {
if (descriptor == null) {
continue;
}
Plugin plugin = JavaPluginFactory.create(descriptor, zipFile, nodeType, context);
Plugin plugin = JavaPluginFactory
.create(descriptor, zipFile, duplicateClassLoadStrategy, nodeType, context);
initAndRegisterPlugin(plugin);
}
}
Expand All @@ -524,7 +532,8 @@ private boolean loadJavaPlugin(ZipFile zipFile) {
if (descriptor == null) {
return false;
}
Plugin plugin = JavaPluginFactory.create(descriptor, zipFile, nodeType, context);
Plugin plugin = JavaPluginFactory
.create(descriptor, zipFile, duplicateClassLoadStrategy, nodeType, context);
initAndRegisterPlugin(plugin);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2019 The Turms Project
* https://github.com/turms-im/turms
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.turms.server.common.infra.property.constant;

/**
* We don't allow child-first visibility as it is more error-prone: For example, if the plugin
* contains the {@link reactor.core.publisher.Mono} class, and pass its instance to the Turms
* server, which will cause the JVM to throw an error as the "same" class are loaded by different
* class loaders: the application classloader and the plugin classloader.
*
* @author James Chen
*/
public enum DuplicateClassLoadStrategy {
/**
* Consider this strategy as the "strict" mode.
* <p>
* Throw an exception if the class is defined by both the Turms server and the plugin.
* <p>
* 1. Pros: There are won't be any duplicate class, and the classes in the plugin should always
* work as expected.
* <p>
* 2. Cons: The plugin developers need to reallocate the classes in the plugin, which is
* troublesome and error-prone as there are usually many transitive dependencies.
*/
THROW_EXCEPTION,
/**
* Consider this strategy as the "lenient" mode.
* <p>
* Parent-first visibility:
* <p>
* 1. Pros: The plugin developers don't need to reallocate the classes in the plugin, which is
* troublesome and error-prone as there are usually many transitive dependencies.
* <p>
* 2. Cons: The plugin developer may be confused as the classes in their plugin may not work as
* expected. Though the plugin developers can always fix the issues by introducing Turms server
* as the provided dependency to use the same classes used by the Turms server.
*/
PARENT_FIRST,
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;

import im.turms.server.common.infra.property.constant.DuplicateClassLoadStrategy;
import im.turms.server.common.infra.property.metadata.Description;

/**
Expand All @@ -36,4 +37,8 @@ public class JavaPluginProperties {
@Description("Whether to allow saving plugins using HTTP API")
protected boolean allowSave;

@Description("The strategy to handle duplicate classes defined by both the Turms server and the plugin when loading classes by the plugin classloader")
protected DuplicateClassLoadStrategy duplicateClassLoadStrategy =
DuplicateClassLoadStrategy.PARENT_FIRST;

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
import im.turms.server.common.infra.plugin.TurmsExtension;
import im.turms.server.common.infra.property.TurmsProperties;
import im.turms.server.common.infra.property.TurmsPropertiesManager;
import im.turms.server.common.infra.property.constant.DuplicateClassLoadStrategy;
import im.turms.server.common.infra.property.env.common.plugin.JavaPluginProperties;
import im.turms.server.common.infra.property.env.common.plugin.PluginProperties;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -168,6 +170,10 @@ private MyExtensionPoint getMyExtensionPoint() {
.plugin(new PluginProperties().toBuilder()
.enabled(true)
.dir(".")
.java(new JavaPluginProperties().toBuilder()
.duplicateClassLoadStrategy(
DuplicateClassLoadStrategy.THROW_EXCEPTION)
.build())
.build())
.build());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2524,6 +2524,19 @@
"sensitive": false,
"type": "boolean",
"value": false
},
"duplicateClassLoadStrategy": {
"deprecated": false,
"description": "The strategy to handle duplicate classes defined by both the Turms server and the plugin when loading classes by the plugin classloader",
"global": false,
"mutable": false,
"options": [
"THROW_EXCEPTION",
"PARENT_FIRST"
],
"sensitive": false,
"type": "enum",
"value": "PARENT_FIRST"
}
},
"js": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2264,6 +2264,18 @@
"mutable": false,
"sensitive": false,
"type": "boolean"
},
"duplicateClassLoadStrategy": {
"deprecated": false,
"description": "The strategy to handle duplicate classes defined by both the Turms server and the plugin when loading classes by the plugin classloader",
"global": false,
"mutable": false,
"options": [
"THROW_EXCEPTION",
"PARENT_FIRST"
],
"sensitive": false,
"type": "enum"
}
},
"js": {
Expand Down

0 comments on commit 98afa7d

Please sign in to comment.