feat(plugin): support annotation plugin

This commit is contained in:
qianmoQ 2024-11-22 15:08:00 +08:00
parent a9ce7e989c
commit 2ef0f46876
14 changed files with 407 additions and 86 deletions

View File

@ -177,7 +177,7 @@ public abstract class Plugin
// 同时使用SPI和注解两种方式加载服务
// Load services using both SPI and annotation methods
ServiceBindings bindings = null;
ServiceBindings bindings;
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (pluginClassLoader != null) {
bindings = ServiceSpiLoader.loadServices(serviceType, basePackage, pluginClassLoader);

View File

@ -7,24 +7,22 @@ import io.edurt.datacap.common.utils.DateUtils;
import io.edurt.datacap.plugin.loader.PluginClassLoader;
import io.edurt.datacap.plugin.loader.PluginLoaderFactory;
import io.edurt.datacap.plugin.utils.PluginClassLoaderUtils;
import io.edurt.datacap.plugin.utils.VersionUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Manifest;
import java.util.stream.Stream;
@Slf4j
@ -54,6 +52,10 @@ public class PluginManager
// Temporary directory prefix
private static final String TEMP_DIR_PREFIX = "plugin_install_";
// 默认版本
// Default version
private static final String DEFAULT_VERSION = "1.0.0";
public PluginManager(PluginConfigure config)
{
this.config = config;
@ -461,7 +463,7 @@ public class PluginManager
// 获取插件版本(可以从配置文件或清单文件中读取)
// Get plugin version (can be read from config or manifest file)
String pluginVersion = getPluginVersion(pluginDir);
String pluginVersion = VersionUtils.determinePluginVersion(pluginDir);
log.debug("Found plugin version: {}", pluginVersion);
// 创建插件专用类加载器
@ -488,10 +490,10 @@ public class PluginManager
PluginMetadata pluginMetadata = PluginMetadata.builder()
.name(pluginName)
.version(module.getVersion())
.version(Objects.equals(module.getVersion(), DEFAULT_VERSION) ? module.getPluginClassLoader().getPluginVersion() : module.getVersion())
.location(pluginDir)
.state(PluginState.CREATED)
.classLoader(loader)
.classLoader(module.getPluginClassLoader() == null ? loader : module.getPluginClassLoader())
.loaderName(module.getClassLoader())
.instance(module)
.type(module.getType())
@ -520,79 +522,6 @@ public class PluginManager
}
}
// 获取插件版本
// Get plugin version
private String getPluginVersion(Path pluginDir)
{
// 这里可以实现从配置文件或清单文件中读取版本号的逻辑
// Here you can implement logic to read version number from config or manifest file
try {
// 优先从 plugin.properties 读取
// Read from plugin.properties first
Path propertiesFile = pluginDir.resolve("plugin.properties");
if (Files.exists(propertiesFile)) {
// 读取属性文件实现
// Implement properties file reading
return readVersionFromProperties(propertiesFile);
}
// 其次从 MANIFEST.MF 读取
// Then read from MANIFEST.MF
Path manifestFile = pluginDir.resolve("META-INF/MANIFEST.MF");
if (Files.exists(manifestFile)) {
return readVersionFromManifest(manifestFile);
}
// 最后从 pom.xml 读取
// Finally read from pom.xml
Path pomFile = pluginDir.resolve("pom.xml");
if (Files.exists(pomFile)) {
return readVersionFromPom(pomFile);
}
}
catch (Exception e) {
log.warn("Failed to read plugin version from: {}", pluginDir, e);
}
// 如果都读取失败返回默认版本
// Return default version if all reads fail
return "1.0.0";
}
private String readVersionFromProperties(Path propertiesFile)
{
// TODO: 实现从 properties 文件读取版本号
// Implement reading version from properties file
return "1.0.0";
}
private String readVersionFromManifest(Path manifestFile)
throws IOException
{
// TODO: 实现从 MANIFEST.MF 读取版本号
// Implement reading version from MANIFEST.MF
ClassLoader loader = getClass().getClassLoader();
Enumeration<URL> resources = loader.getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
try (InputStream is = resources.nextElement().openStream()) {
Manifest manifest = new Manifest(is);
String version = manifest.getMainAttributes().getValue("Implementation-Version");
if (version != null) {
return version;
}
}
}
return "1.0.0";
}
private String readVersionFromPom(Path pomFile)
{
// TODO: 实现从 pom.xml 读取版本号
// Implement reading version from pom.xml
return "1.0.0";
}
// 关闭插件类加载器
// Close plugin class loader
private void closePluginClassLoader(PluginMetadata pluginMetadata)

View File

@ -6,7 +6,8 @@ public enum SpiType
DIRECTORY("Directory"),
POM("Pom"),
PROPERTIES("Properties"),
SPI("Spi");
SPI("Spi"),
INJECT("Inject");
private String name;

View File

@ -0,0 +1,27 @@
package io.edurt.datacap.plugin.annotation;
import io.edurt.datacap.plugin.PluginType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectPlugin
{
/**
* 插件名称
* Plugin name
*/
String name() default "";
/**
* 插件版本
* Plugin version
*/
String version() default "1.0.0";
PluginType type() default PluginType.CONNECTOR;
}

View File

@ -1,4 +1,4 @@
package io.edurt.datacap.plugin.service;
package io.edurt.datacap.plugin.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;

View File

@ -0,0 +1,94 @@
package io.edurt.datacap.plugin.loader;
import io.edurt.datacap.plugin.Plugin;
import io.edurt.datacap.plugin.PluginContextManager;
import io.edurt.datacap.plugin.SpiType;
import io.edurt.datacap.plugin.scanner.PluginAnnotationScanner;
import io.edurt.datacap.plugin.utils.PluginClassLoaderUtils;
import io.edurt.datacap.plugin.utils.VersionUtils;
import lombok.extern.slf4j.Slf4j;
import java.nio.file.Path;
import java.util.List;
@Slf4j
public class InjectPluginLoader
implements PluginLoader
{
private final PluginAnnotationScanner scanner;
public InjectPluginLoader()
{
this.scanner = new PluginAnnotationScanner();
}
@Override
public SpiType getType()
{
return SpiType.INJECT;
}
private Path getEffectivePluginPath(Path originalPath)
{
if (originalPath.endsWith("classes")) {
return originalPath.getParent();
}
return originalPath;
}
private String getEffectivePluginName(Path path)
{
if (path.getFileName().toString().equals("target")) {
return path.getParent().getFileName().toString();
}
if (path.getFileName().toString().equals("classes")) {
return path.getParent().getParent().getFileName().toString();
}
return path.getFileName().toString();
}
@Override
public List<Plugin> load(Path path)
{
try {
if (isExcludedPath(path)) {
log.debug("Skipping excluded directory: {}", path);
return List.of();
}
// 获取实际的插件路径用于版本检测
// Get actual plugin path (used for version detection)
Path effectivePath = getEffectivePluginPath(path);
// 获取正确的插件名称
// Get correct plugin name
String pluginName = getEffectivePluginName(effectivePath);
String version = VersionUtils.determinePluginVersion(effectivePath);
if (log.isDebugEnabled()) {
log.debug("Loading plugin - Path: {}, Effective Path: {}, Name: {}, Version: {}", path, effectivePath, pluginName, version);
}
// 创建插件专用类加载器
// Create plugin-specific class loader
PluginClassLoader classLoader = PluginClassLoaderUtils.createClassLoader(
path,
pluginName,
version
);
return PluginContextManager.runWithClassLoader(classLoader, () -> {
List<Plugin> plugins = scanner.scanPlugins(path, classLoader);
// 设置插件的类加载器
// Set class loader for plugins
plugins.forEach(plugin -> plugin.setPluginClassLoader(classLoader));
return plugins;
});
}
catch (Exception e) {
log.error("Failed to load inject plugins from path: {}", path, e);
return List.of();
}
}
}

View File

@ -3,11 +3,80 @@ package io.edurt.datacap.plugin.loader;
import io.edurt.datacap.plugin.Plugin;
import io.edurt.datacap.plugin.SpiType;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public interface PluginLoader
{
Set<String> EXCLUDED_DIRS = new HashSet<>()
{{
// Maven 相关目录
// Maven related directories
add("target/test-classes");
add("target/generated-sources");
add("target/generated-test-sources");
add("target/maven-status");
add("target/maven-archiver");
add("target/surefire-reports");
add("target/reports");
add("target/javadoc-bundle-options");
// 版本控制和 IDE 目录
// Version control and IDE directories
add(".git");
add(".idea");
add(".vscode");
add(".settings");
add(".project");
add(".classpath");
// 依赖目录
// Dependency directories
add("node_modules");
// SPI 特有的排除目录
// SPI-specific exclusion directories
add("META-INF/services/test");
add("META-INF/maven");
add("META-INF/versions");
// 源码目录
// Source code directory
add("src/main/java");
add("src/main/kotlin");
add("src/main/resources");
add("src/test");
add("src/test/java");
add("src/test/kotlin");
add("src/test/resources");
}};
/**
* 检查路径是否应该被排除
* Check if the path should be excluded
*/
default boolean isExcludedPath(Path path)
{
String pathStr = path.toString();
return EXCLUDED_DIRS.stream()
.anyMatch(dir -> pathStr.contains(dir.replace('/', File.separatorChar)));
}
/**
* 检查目录是否有效且不在排除列表中
* Check if the directory is valid and not in the exclusion list
*/
default boolean isValidDirectory(Path path)
{
return Files.exists(path) &&
Files.isDirectory(path) &&
!isExcludedPath(path);
}
// 获取加载器类型
// Get loader type
SpiType getType();

View File

@ -7,6 +7,7 @@ import io.edurt.datacap.plugin.utils.PluginClassLoaderUtils;
import io.edurt.datacap.plugin.utils.VersionUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.ServiceLoader;
@ -27,6 +28,13 @@ public class SpiPluginLoader
public List<Plugin> load(Path path)
{
try {
// 检查路径是否有效
// Check if the path is valid
if (!isValidDirectory(path)) {
log.debug("Skipping excluded or invalid directory: {}", path);
return List.of();
}
// 获取目录名作为插件名
// Get directory name as plugin name
String pluginName = path.getFileName().toString();
@ -43,11 +51,20 @@ public class SpiPluginLoader
return PluginContextManager.runWithClassLoader(classLoader, () -> {
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, classLoader);
List<Plugin> plugins = StreamSupport.stream(serviceLoader.spliterator(), false)
.filter(plugin -> {
// 检查插件类是否来自排除目录
// Check if the plugin class is from an excluded directory
String className = plugin.getClass().getName();
Path classPath = Path.of(className.replace('.', File.separatorChar) + ".class");
return !isExcludedPath(classPath);
})
.collect(Collectors.toList());
// 设置插件的类加载器
// Set class loader for plugins
plugins.forEach(plugin -> plugin.setPluginClassLoader(classLoader));
plugins.stream()
.peek(plugin -> log.debug("Loaded SPI plugin: {} (version: {})", plugin.getClass().getName(), version))
.forEach(plugin -> plugin.setPluginClassLoader(classLoader));
return plugins;
});

View File

@ -0,0 +1,151 @@
package io.edurt.datacap.plugin.scanner;
import io.edurt.datacap.plugin.Plugin;
import io.edurt.datacap.plugin.annotation.InjectPlugin;
import io.edurt.datacap.plugin.loader.PluginClassLoader;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@Slf4j
public class PluginAnnotationScanner
{
// 定义要排除的目录
// Define directories to exclude
private static final Set<String> EXCLUDED_DIRS = new HashSet<>()
{{
add("target/test-classes");
add("target/generated-sources");
add("target/generated-test-sources");
add("target/maven-status");
add("target/maven-archiver");
add("target/surefire-reports");
add(".git");
add(".idea");
add("node_modules");
}};
/**
* 扫描目录中的插件注解
* Scan plugins in directory
*/
public List<Plugin> scanPlugins(Path directory, PluginClassLoader classLoader)
{
List<Plugin> plugins = new ArrayList<>();
try {
// 获取要扫描的目录
// Get directories to scan
List<Path> scanDirs = new ArrayList<>();
// 检查主类目录
// Check main classes directory
Path mainClassesDir = directory;
if (Files.exists(directory.resolve("target/classes"))) {
mainClassesDir = directory.resolve("target/classes");
}
scanDirs.add(mainClassesDir);
// 扫描每个目录
// Scan each directory
for (Path scanDir : scanDirs) {
try (Stream<Path> paths = Files.walk(scanDir)) {
paths.filter(path -> !isExcludedPath(path))
.filter(path -> path.toString().endsWith(".class"))
.filter(path -> !path.toString().contains("$"))
.forEach(path -> {
try {
String className = getClassName(scanDir, path);
Class<?> cls = classLoader.loadClass(className);
// 检查是否有 @InjectPlugin 注解
// Check if it has @InjectPlugin annotation
InjectPlugin annotation = cls.getAnnotation(InjectPlugin.class);
if (annotation != null) {
processPluginClass(cls, annotation, plugins, classLoader);
}
}
catch (Exception e) {
log.debug("Failed to load class: {}", path);
}
});
}
}
}
catch (Exception e) {
log.error("Failed to scan plugins in directory: {}", directory, e);
}
return plugins;
}
/**
* 检查路径是否应该被排除
* Check if path should be excluded
*/
private boolean isExcludedPath(Path path)
{
String pathStr = path.toString();
return EXCLUDED_DIRS.stream()
.anyMatch(dir -> pathStr.contains(dir.replace('/', File.separatorChar)));
}
/**
* 处理带有 @InjectPlugin 注解的类
* Handle class with @InjectPlugin annotation
*/
private void processPluginClass(Class<?> cls, InjectPlugin annotation, List<Plugin> plugins,
PluginClassLoader classLoader)
{
try {
// 验证类是否实现了Plugin接口
// Validate class implements Plugin interface
if (!Plugin.class.isAssignableFrom(cls)) {
log.warn("Class {} has @InjectPlugin annotation but doesn't implement Plugin interface", cls.getName());
return;
}
// 创建插件实例
// Create plugin instance
Plugin plugin = (Plugin) cls.getDeclaredConstructor().newInstance();
// 设置插件元数据
// Set plugin metadata
String name = annotation.name().isEmpty() ? cls.getSimpleName() : annotation.name();
plugin.setPluginClassLoader(classLoader);
plugins.add(plugin);
log.info("Found inject plugin: {} (version: {})", name, annotation.version());
}
catch (Exception e) {
log.error("Failed to process plugin class: {}", cls.getName(), e);
}
}
/**
* 获取类名
* Get class name
*/
private String getClassName(Path baseDir, Path classFile)
{
// 获取相对路径
// Get relative path
String relativePath = baseDir.relativize(classFile).toString();
// 处理路径分隔符和移除.class扩展名
// Handle path separators and remove .class extension
String className = relativePath.replace(File.separatorChar, '.');
if (className.startsWith("test-classes.")) {
className = className.substring("test-classes.".length());
}
return className.replace(".class", "");
}
}

View File

@ -1,8 +1,9 @@
package io.edurt.datacap.plugin.service;
package io.edurt.datacap.plugin.scanner;
import com.google.common.collect.Sets;
import com.google.common.reflect.ClassPath;
import io.edurt.datacap.plugin.Service;
import io.edurt.datacap.plugin.annotation.InjectService;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;

View File

@ -2,6 +2,8 @@ package io.edurt.datacap.plugin.service;
import com.google.common.collect.Sets;
import io.edurt.datacap.plugin.Service;
import io.edurt.datacap.plugin.annotation.InjectService;
import io.edurt.datacap.plugin.scanner.ServiceAnnotationScanner;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;

View File

@ -0,0 +1,16 @@
package io.edurt.datacap.test;
import io.edurt.datacap.plugin.Plugin;
import io.edurt.datacap.plugin.PluginType;
import io.edurt.datacap.plugin.annotation.InjectPlugin;
@InjectPlugin
public class AnnotationPlugin
extends Plugin
{
@Override
public PluginType getType()
{
return PluginType.CONNECTOR;
}
}

View File

@ -1,6 +1,6 @@
package io.edurt.datacap.test;
import io.edurt.datacap.plugin.service.InjectService;
import io.edurt.datacap.plugin.annotation.InjectService;
import lombok.extern.slf4j.Slf4j;
@Slf4j

View File

@ -2,6 +2,8 @@ package io.edurt.datacap.test;
import io.edurt.datacap.plugin.PluginConfigure;
import io.edurt.datacap.plugin.PluginManager;
import io.edurt.datacap.plugin.loader.InjectPluginLoader;
import io.edurt.datacap.plugin.loader.PluginLoaderFactory;
import io.edurt.datacap.plugin.utils.PluginPathUtils;
import org.junit.Assert;
import org.junit.Before;
@ -19,8 +21,10 @@ public class LocalPluginTest
Path projectRoot = PluginPathUtils.findProjectRoot();
PluginConfigure config = PluginConfigure.builder()
.pluginsDir(projectRoot.resolve("test/datacap-test-plugin"))
.scanDepth(2)
.build();
PluginLoaderFactory.registerLoader(new InjectPluginLoader());
pluginManager = new PluginManager(config);
pluginManager.start();
}
@ -30,4 +34,14 @@ public class LocalPluginTest
{
Assert.assertFalse(pluginManager.getPlugin("Local").isEmpty());
}
@Test
public void testAnnotationPlugin()
{
pluginManager.getPlugin("Annotation")
.ifPresent(value -> {
LogService logService = value.getService(LogService.class);
logService.print();
});
}
}