提交后续开发的内容
This commit is contained in:
parent
0f8f6f837d
commit
6ac4a14ded
3
.idea/inspectionProfiles/Project_Default.xml
generated
3
.idea/inspectionProfiles/Project_Default.xml
generated
@ -63,6 +63,9 @@
|
|||||||
<inspection_tool class="AlibabaUnsupportedExceptionWithModifyAsList" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="AlibabaUnsupportedExceptionWithModifyAsList" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="AlibabaUseQuietReferenceNotation" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="AlibabaUseQuietReferenceNotation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="AlibabaUseRightCaseForDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="AlibabaUseRightCaseForDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JavadocDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ADDITIONAL_TAGS" value="Created,Date" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
10
.idea/jarRepositories.xml
generated
10
.idea/jarRepositories.xml
generated
@ -1,16 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="RemoteRepositoriesConfiguration">
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="huaweicloud" />
|
|
||||||
<option name="name" value="huawei" />
|
|
||||||
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
<remote-repository>
|
||||||
<option name="id" value="central" />
|
<option name="id" value="central" />
|
||||||
<option name="name" value="Central Repository" />
|
<option name="name" value="Central Repository" />
|
||||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="huaweicloud" />
|
||||||
|
<option name="name" value="huawei" />
|
||||||
|
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
||||||
|
</remote-repository>
|
||||||
<remote-repository>
|
<remote-repository>
|
||||||
<option name="id" value="aliyunmaven" />
|
<option name="id" value="aliyunmaven" />
|
||||||
<option name="name" value="aliyun" />
|
<option name="name" value="aliyun" />
|
||||||
|
15
dependencies/.flattened-pom.xml
vendored
15
dependencies/.flattened-pom.xml
vendored
@ -352,6 +352,21 @@
|
|||||||
<artifactId>spring-boot-admin-starter-client</artifactId>
|
<artifactId>spring-boot-admin-starter-client</artifactId>
|
||||||
<version>${spring-boot-admin.version}</version>
|
<version>${spring-boot-admin.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||||
|
<version>6.3.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-client</artifactId>
|
||||||
|
<version>6.3.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-resource-server</artifactId>
|
||||||
|
<version>6.3.4</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-inline</artifactId>
|
<artifactId>mockito-inline</artifactId>
|
||||||
|
17
dependencies/pom.xml
vendored
17
dependencies/pom.xml
vendored
@ -228,7 +228,6 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!--ci-pipeline-dependency-->
|
<!--ci-pipeline-dependency-->
|
||||||
|
|
||||||
<!---->
|
<!---->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.mouzt</groupId>
|
<groupId>io.github.mouzt</groupId>
|
||||||
@ -401,6 +400,22 @@
|
|||||||
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 客户端 -->
|
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 客户端 -->
|
||||||
<version>${spring-boot-admin.version}</version>
|
<version>${spring-boot-admin.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--鉴权相关-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||||
|
<version>6.3.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-client</artifactId>
|
||||||
|
<version>6.3.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-resource-server</artifactId>
|
||||||
|
<version>6.3.4</version>
|
||||||
|
</dependency>
|
||||||
<!-- Test 测试 -->
|
<!-- Test 测试 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
|
@ -7,15 +7,36 @@ import cn.hutool.core.util.IdUtil;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.crypto.digest.DigestUtil;
|
import cn.hutool.crypto.digest.DigestUtil;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||||
|
import static org.springframework.util.FileSystemUtils.deleteRecursively;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件工具类
|
* 文件工具类 , 为了匹配插件类,这里补充了yudao里面的工具类
|
||||||
*
|
*
|
||||||
* @author mianbin
|
* @author mianbin
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class FileUtils {
|
public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,4 +102,280 @@ public class FileUtils {
|
|||||||
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
|
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 保持原有的 unzip(ZipInputStream zis, Path targetPath) 方法不变
|
||||||
|
public static void unzip(@NonNull ZipInputStream zis, @NonNull Path targetPath)
|
||||||
|
throws IOException {
|
||||||
|
// 1. unzip file to folder
|
||||||
|
// 2. return the folder path
|
||||||
|
Assert.notNull(zis, "Zip input stream must not be null");
|
||||||
|
Assert.notNull(targetPath, "Target path must not be null");
|
||||||
|
|
||||||
|
// Create path if absent
|
||||||
|
createIfAbsent(targetPath);
|
||||||
|
|
||||||
|
// Folder must be empty
|
||||||
|
ensureEmpty(targetPath);
|
||||||
|
|
||||||
|
ZipEntry zipEntry = zis.getNextEntry();
|
||||||
|
|
||||||
|
while (zipEntry != null) {
|
||||||
|
// Resolve the entry path
|
||||||
|
Path entryPath = targetPath.resolve(zipEntry.getName());
|
||||||
|
|
||||||
|
checkDirectoryTraversal(targetPath, entryPath);
|
||||||
|
|
||||||
|
if (Files.notExists(entryPath.getParent())) {
|
||||||
|
Files.createDirectories(entryPath.getParent());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipEntry.isDirectory()) {
|
||||||
|
// Create directory
|
||||||
|
Files.createDirectory(entryPath);
|
||||||
|
} else {
|
||||||
|
// Copy file
|
||||||
|
Files.copy(zis, entryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
zipEntry = zis.getNextEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void jar(Path sourcePath, Path targetPath) throws IOException {
|
||||||
|
try (var jos = new JarOutputStream(Files.newOutputStream(targetPath))) {
|
||||||
|
Files.walkFileTree(sourcePath, new SimpleFileVisitor<>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||||
|
throws IOException {
|
||||||
|
checkDirectoryTraversal(sourcePath, file);
|
||||||
|
var relativePath = sourcePath.relativize(file);
|
||||||
|
var entry = new JarEntry(relativePath.toString());
|
||||||
|
jos.putNextEntry(entry);
|
||||||
|
Files.copy(file, jos);
|
||||||
|
jos.closeEntry();
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates directories if absent.
|
||||||
|
*
|
||||||
|
* @param path path must not be null
|
||||||
|
* @throws IOException io exception
|
||||||
|
*/
|
||||||
|
public static void createIfAbsent(@NonNull Path path) throws IOException {
|
||||||
|
Assert.notNull(path, "Path must not be null");
|
||||||
|
|
||||||
|
if (Files.notExists(path)) {
|
||||||
|
// Create directories
|
||||||
|
Files.createDirectories(path);
|
||||||
|
|
||||||
|
log.debug("Created directory: [{}]", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given path must be empty.
|
||||||
|
*
|
||||||
|
* @param path path must not be null
|
||||||
|
* @throws IOException io exception
|
||||||
|
*/
|
||||||
|
public static void ensureEmpty(@NonNull Path path) throws IOException {
|
||||||
|
if (!isEmpty(path)) {
|
||||||
|
throw new DirectoryNotEmptyException("Target directory: " + path + " was not empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given path is empty.
|
||||||
|
*
|
||||||
|
* @param path path must not be null
|
||||||
|
* @return true if the given path is empty; false otherwise
|
||||||
|
* @throws IOException io exception
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(@NonNull Path path) throws IOException {
|
||||||
|
Assert.notNull(path, "Path must not be null");
|
||||||
|
|
||||||
|
if (!Files.isDirectory(path) || Files.notExists(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> pathStream = Files.list(path)) {
|
||||||
|
return pathStream.findAny().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeQuietly(final Closeable closeable) {
|
||||||
|
closeQuietly(closeable, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the given {@link Closeable} as a null-safe operation while consuming IOException by
|
||||||
|
* the given {@code consumer}.
|
||||||
|
*
|
||||||
|
* @param closeable The resource to close, may be null.
|
||||||
|
* @param consumer Consumes the IOException thrown by {@link Closeable#close()}.
|
||||||
|
*/
|
||||||
|
public static void closeQuietly(final Closeable closeable,
|
||||||
|
final Consumer<IOException> consumer) {
|
||||||
|
if (closeable != null) {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (consumer != null) {
|
||||||
|
consumer.accept(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks directory traversal vulnerability.
|
||||||
|
*
|
||||||
|
* @param parentPath parent path must not be null.
|
||||||
|
* @param pathToCheck path to check must not be null
|
||||||
|
*/
|
||||||
|
public static void checkDirectoryTraversal(@NonNull Path parentPath,
|
||||||
|
@NonNull Path pathToCheck) {
|
||||||
|
Assert.notNull(parentPath, "Parent path must not be null");
|
||||||
|
Assert.notNull(pathToCheck, "Path to check must not be null");
|
||||||
|
|
||||||
|
if (pathToCheck.normalize().startsWith(parentPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Directory traversal detected: " + pathToCheck.toString() +
|
||||||
|
"problemDetail.directoryTraversal" + parentPath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks directory traversal vulnerability.
|
||||||
|
*
|
||||||
|
* @param parentPath parent path must not be null.
|
||||||
|
* @param pathToCheck path to check must not be null
|
||||||
|
*/
|
||||||
|
public static void checkDirectoryTraversal(@NonNull String parentPath,
|
||||||
|
@NonNull String pathToCheck) {
|
||||||
|
checkDirectoryTraversal(Paths.get(parentPath), Paths.get(pathToCheck));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks directory traversal vulnerability.
|
||||||
|
*
|
||||||
|
* @param parentPath parent path must not be null.
|
||||||
|
* @param pathToCheck path to check must not be null
|
||||||
|
*/
|
||||||
|
public static void checkDirectoryTraversal(@NonNull Path parentPath,
|
||||||
|
@NonNull String pathToCheck) {
|
||||||
|
checkDirectoryTraversal(parentPath, Paths.get(pathToCheck));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete folder recursively without exception throwing.
|
||||||
|
*
|
||||||
|
* @param root the root File to delete
|
||||||
|
*/
|
||||||
|
public static void deleteRecursivelyAndSilently(Path root) {
|
||||||
|
try {
|
||||||
|
var deleted = deleteRecursively(root);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Delete {} result: {}", root, deleted);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// Ignore this error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void forceDelete(File file) {
|
||||||
|
if (!FileUtil.exist(file)) {
|
||||||
|
log.debug("文件或目录不存在,无需删除: {}", file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Path path = file.toPath();
|
||||||
|
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
Files.delete(file);
|
||||||
|
log.debug("已删除文件: {}", file);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
Files.delete(dir);
|
||||||
|
log.debug("已删除目录: {}", dir);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.info("成功删除文件或目录: {}", file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("删除文件或目录失败: {}", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Boolean deleteFileSilently(Path file) {
|
||||||
|
if (file == null || !Files.isRegularFile(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Files.deleteIfExists(file);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyResource(Resource resource, Path path) {
|
||||||
|
try (var inputStream = resource.getInputStream()) {
|
||||||
|
Files.copy(inputStream, path, REPLACE_EXISTING);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copy(Path source, Path dest, CopyOption... options) {
|
||||||
|
try {
|
||||||
|
Files.copy(source, dest, options);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyRecursively(Path src, Path target, Set<String> excludes)
|
||||||
|
throws IOException {
|
||||||
|
var pathMatcher = new AntPathMatcher();
|
||||||
|
Predicate<Path> shouldExclude = path -> excludes.stream()
|
||||||
|
.anyMatch(pattern -> pathMatcher.match(pattern, path.toString()));
|
||||||
|
Files.walkFileTree(src, new SimpleFileVisitor<>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||||
|
throws IOException {
|
||||||
|
if (!shouldExclude.test(src.relativize(file))) {
|
||||||
|
Files.copy(file, target.resolve(src.relativize(file)), REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
|
||||||
|
throws IOException {
|
||||||
|
if (shouldExclude.test(src.relativize(dir))) {
|
||||||
|
return FileVisitResult.SKIP_SUBTREE;
|
||||||
|
}
|
||||||
|
Files.createDirectories(target.resolve(src.relativize(dir)));
|
||||||
|
return super.preVisitDirectory(dir, attrs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path createTempDir(String prefix) {
|
||||||
|
try {
|
||||||
|
return Files.createTempDirectory(prefix);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON 工具类 ,经量用hutool的
|
* JSON 工具类 ,经量用hutool的
|
||||||
@ -202,6 +204,44 @@ public class JsonUtils extends JSONUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将对象转换为 Map。
|
||||||
|
*
|
||||||
|
* @param obj 要转换的对象
|
||||||
|
* @return 转换后的 Map,如果对象为 null 或转换失败则返回空 Map
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> objectToMap(Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.convertValue(obj, new TypeReference<Map<String, Object>>() {});
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("对象转换为 Map 失败", e);
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Map 转换为指定类型的 Java Bean。
|
||||||
|
*
|
||||||
|
* @param map 要转换的 Map
|
||||||
|
* @param clazz 目标 Java Bean 的 Class 对象
|
||||||
|
* @param <T> 目标 Java Bean 的类型
|
||||||
|
* @return 转换后的 Java Bean,如果 Map 为 null 或转换失败则返回 null
|
||||||
|
*/
|
||||||
|
public static <T> T mapToBean(Map<String, Object> map, Class<T> clazz) {
|
||||||
|
if (map == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.convertValue(map, clazz);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("Map 转换为 Java Bean 失败,目标类型: {}", clazz.getName(), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isJson(String text) {
|
public static boolean isJson(String text) {
|
||||||
return JSONUtil.isTypeJSON(text);
|
return JSONUtil.isTypeJSON(text);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,16 @@
|
|||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cd.casic.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cd.casic.boot</groupId>
|
<groupId>cd.casic.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
@ -65,6 +75,7 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import cd.casic.plugin.function.PluginGetter;
|
||||||
|
import org.pf4j.Plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/20 15:11
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class DefaultPluginGetter implements PluginGetter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Plugin getPlugin(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.pf4j.DefaultPluginDescriptor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/19 17:32
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OpsPluginDescriptor extends DefaultPluginDescriptor {
|
||||||
|
|
||||||
|
private final String configFileName;
|
||||||
|
private final List<String> configFileActive;
|
||||||
|
|
||||||
|
public OpsPluginDescriptor(String pluginId, String pluginDescription, String pluginClass,
|
||||||
|
String version, String requires, String provider, String license,
|
||||||
|
String configFileName, List<String> configFileActive) {
|
||||||
|
super(pluginId, pluginDescription, pluginClass, version, requires, provider, license);
|
||||||
|
this.configFileActive = configFileActive;
|
||||||
|
this.configFileName = configFileName;
|
||||||
|
}
|
||||||
|
}
|
@ -1,46 +0,0 @@
|
|||||||
package cd.casic.plugin;
|
|
||||||
|
|
||||||
import cd.casic.plugin.extension.Plugin;
|
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname PluginService
|
|
||||||
* @Description 插件服务类
|
|
||||||
* @Date 2025/5/8 19:58
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public interface PluginService {
|
|
||||||
|
|
||||||
boolean installPresetPlugins();
|
|
||||||
|
|
||||||
Plugin install(Path path);
|
|
||||||
|
|
||||||
Plugin upgrade(String name, Path path);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新加载插件,插件spec.enabled 设置为 true
|
|
||||||
*/
|
|
||||||
Mono<Plugin> reload(String name);
|
|
||||||
|
|
||||||
DataBuffer uglifyJsBundle();
|
|
||||||
|
|
||||||
DataBuffer uglifyCssBundle();
|
|
||||||
|
|
||||||
String generateBundleVersion();
|
|
||||||
|
|
||||||
Resource getJsBundle(String version);
|
|
||||||
|
|
||||||
Resource getCssBundle(String version);
|
|
||||||
|
|
||||||
Plugin changeState(String pluginName, boolean requestToEnable, boolean wait);
|
|
||||||
|
|
||||||
List<String> getRequiredDependencies(Plugin plugin,
|
|
||||||
Predicate<PluginWrapper> predicate);
|
|
||||||
}
|
|
@ -0,0 +1,180 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import cd.casic.framework.commons.util.io.FileUtils;
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import cd.casic.plugin.dataobject.dao.PluginFacadeMemoryCache;
|
||||||
|
import cd.casic.plugin.dataobject.dto.PluginInformation;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.exception.PluginInstallationException;
|
||||||
|
import cd.casic.plugin.function.IPluginManager;
|
||||||
|
import cd.casic.plugin.function.PluginLifecycle;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.pf4j.DefaultPluginManager;
|
||||||
|
import org.pf4j.PluginState;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import org.pf4j.RuntimeMode;
|
||||||
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class PluginServiceImpl extends DefaultPluginManager implements
|
||||||
|
IPluginManager,
|
||||||
|
PluginLifecycle,
|
||||||
|
InitializingBean,
|
||||||
|
DisposableBean {
|
||||||
|
|
||||||
|
public PluginServiceImpl(PluginProperties pluginProperties) {
|
||||||
|
this.pluginsRoots.add(Paths.get(pluginProperties.getPluginRoot()));
|
||||||
|
this.setRuntimeMode(pluginProperties.getRuntimeMode());
|
||||||
|
super.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
// 这里不需要任何操作,只是覆盖父类的initialize方法
|
||||||
|
// 如果不重写的话,在调用构造函数的时候会调用父类的无参构造函数,导致父类initialize方法被调用。
|
||||||
|
// 父类initialize方法中会调用createPluginStatusProvider方法,该方法需要使用到PluginProperties,而此时还未被注入
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginInfo install(Path path) throws Exception {
|
||||||
|
/* 插件的目录
|
||||||
|
工作目录
|
||||||
|
│
|
||||||
|
├─plugins
|
||||||
|
│ ├─example-mybatis-plugin@0.1 插件目录,id@version,存放jar、database、resource
|
||||||
|
│ │ ├─ops-module-plugin-example-mybatis-plugin.jar 插件jar包,jar包命名不影响
|
||||||
|
│ │ ├─database 存放sqlite数据库文件
|
||||||
|
│ │ └─resource 存放静态资源
|
||||||
|
│ └─example-redis-plugin@1.1.4
|
||||||
|
│ ├─ops-module-plugin-example-redis-plugin.jar
|
||||||
|
│ ├─database
|
||||||
|
│ └─resource
|
||||||
|
*/
|
||||||
|
String pluginId = StringUtils.EMPTY;
|
||||||
|
try {
|
||||||
|
beforeWork();
|
||||||
|
pluginId = loadPlugin(path);
|
||||||
|
PluginState pluginState = startPlugin(pluginId);
|
||||||
|
log.info("install plugin [{}] success , plugin state {}", pluginId, pluginState.name());
|
||||||
|
return PluginFacadeMemoryCache.getPluginInfo(pluginId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("安装插件出现异常:{}", e.getMessage());
|
||||||
|
Optional.of(pluginId).ifPresent(PluginFacadeMemoryCache::removePlugin);
|
||||||
|
throw new PluginInstallationException(e.getMessage(), e.getLocalizedMessage(), e.getStackTrace());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void installAfter(String pluginId) {
|
||||||
|
try {
|
||||||
|
super.stopPlugin(pluginId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("install plugin after error:{}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unInstall(String pluginId, boolean isUpdate) throws Exception {
|
||||||
|
// TODO unInstall的时候除掉插件资源
|
||||||
|
log.info("unInstallPlugin:" + pluginId);
|
||||||
|
PluginState pluginState = stopPlugin(pluginId);
|
||||||
|
if (pluginState != PluginState.STOPPED) {
|
||||||
|
throw new UnexpectedPluginStateException();
|
||||||
|
}
|
||||||
|
PluginInfo plugin = PluginFacadeMemoryCache.getPluginInfo(pluginId);
|
||||||
|
plugin.clearApplicationContext();
|
||||||
|
PluginFacadeMemoryCache.removePlugin(pluginId);
|
||||||
|
//删除插件
|
||||||
|
FileUtils.forceDelete(plugin.getPluginWrapper().getPluginPath().toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initPlugins(List<PluginInformation> plugins) throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginWrapper> getInstallPlugins() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginInfo upgrade(String name, Path path) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginInfo reload(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataBuffer uglifyJsBundle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataBuffer uglifyCssBundle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateBundleVersion() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resource getJsBundle(String version) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resource getCssBundle(String version) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginInfo changeState(String pluginName, boolean requestToEnable, boolean wait) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeWork() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void AfterWork() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class UnexpectedPluginStateException extends RuntimeException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRuntimeMode(RuntimeMode runtimeMode) {
|
||||||
|
this.runtimeMode = runtimeMode;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import cd.casic.plugin.function.PluginsRootGetter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/13 10:53
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PluginsRootGetterImpl implements PluginsRootGetter {
|
||||||
|
|
||||||
|
private final PluginProperties properties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public String get() {
|
||||||
|
return properties.getPluginRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
|||||||
package cd.casic.plugin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname SpringPlugin
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 14:48
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public class SpringPlugin {
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.PluginContext;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
|
import cd.casic.plugin.function.PluginGetter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.pf4j.Plugin;
|
||||||
|
import org.pf4j.PluginFactory;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 15:16
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SpringPluginFactory implements PluginFactory {
|
||||||
|
|
||||||
|
private final PluginApplicationContextFactory contextFactory;
|
||||||
|
private final PluginGetter pluginGetter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Plugin create(PluginWrapper pluginWrapper) {
|
||||||
|
var plugin = pluginGetter.getPlugin(pluginWrapper.getPluginId());
|
||||||
|
var pluginContext = PluginContext.builder()
|
||||||
|
.name(pluginWrapper.getPluginId())
|
||||||
|
.configMapName(plugin.getClass().getName())
|
||||||
|
.version(pluginWrapper.getDescriptor().getVersion())
|
||||||
|
.runtimeMode(pluginWrapper.getRuntimeMode())
|
||||||
|
.build();
|
||||||
|
return new SpringPlugin(contextFactory, pluginContext);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.dao.PluginFacadeMemoryCache;
|
||||||
|
import cd.casic.plugin.dataobject.dto.PluginSpecStorage;
|
||||||
|
import cd.casic.plugin.utils.PluginDescriptorUtils;
|
||||||
|
import org.pf4j.PluginDescriptor;
|
||||||
|
import org.pf4j.PluginDescriptorFinder;
|
||||||
|
import org.pf4j.util.FileUtils;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 优化代码
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 19:19
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class YamlPluginDescriptorFinder implements PluginDescriptorFinder {
|
||||||
|
|
||||||
|
private final YamlPluginFinder yamlPluginFinder;
|
||||||
|
|
||||||
|
public YamlPluginDescriptorFinder() {
|
||||||
|
yamlPluginFinder = new YamlPluginFinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable(Path pluginPath) {
|
||||||
|
return Files.exists(pluginPath)
|
||||||
|
&& (Files.isDirectory(pluginPath) || FileUtils.isJarFile(pluginPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginDescriptor find(Path pluginPath) {
|
||||||
|
PluginSpecStorage pluginSpecStorage = yamlPluginFinder.find(pluginPath);
|
||||||
|
try {
|
||||||
|
return PluginDescriptorUtils.storageOpsPluginDescriptor(pluginSpecStorage);
|
||||||
|
} finally {
|
||||||
|
PluginFacadeMemoryCache.putPluginSpec(pluginSpecStorage.getPluginId(), pluginSpecStorage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package cd.casic.plugin;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.dto.PluginSpecStorage;
|
||||||
|
import cd.casic.plugin.utils.YamlUnstructuredLoader;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.DevelopmentPluginClasspath;
|
||||||
|
import org.pf4j.PluginRuntimeException;
|
||||||
|
import org.pf4j.util.FileUtils;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 19:20
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class YamlPluginFinder {
|
||||||
|
static final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath();
|
||||||
|
public static final String DEFAULT_PROPERTIES_FILE_NAME = "plugin.yaml";
|
||||||
|
private final String propertiesFileName;
|
||||||
|
|
||||||
|
public YamlPluginFinder() {
|
||||||
|
this(DEFAULT_PROPERTIES_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public YamlPluginFinder(String propertiesFileName) {
|
||||||
|
this.propertiesFileName = propertiesFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginSpecStorage find(Path pluginPath) {
|
||||||
|
PluginSpecStorage pluginSpecStorage = readPluginDescriptor(pluginPath);
|
||||||
|
Optional.of(pluginSpecStorage.getEnable()).ifPresent(t -> {
|
||||||
|
pluginSpecStorage.setEnable(PluginSpecStorage.StatusPhase.PENDING);
|
||||||
|
});
|
||||||
|
Optional.of(pluginSpecStorage.getPluginDirPath()).ifPresent(pluginSpecStorage::setPluginDirPath);
|
||||||
|
return pluginSpecStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PluginSpecStorage readPluginDescriptor(Path pluginPath) {
|
||||||
|
Path propertiesPath = null;
|
||||||
|
try {
|
||||||
|
propertiesPath = getManifestPath(pluginPath, propertiesFileName);
|
||||||
|
if (propertiesPath == null) {
|
||||||
|
throw new PluginRuntimeException("找不到插件 manifest path");
|
||||||
|
}
|
||||||
|
log.debug("debug下插件的配置文件内容 plugin descriptor in '{}'", propertiesPath);
|
||||||
|
if (Files.notExists(propertiesPath)) {
|
||||||
|
throw new PluginRuntimeException("Cannot find '{}' path", propertiesPath);
|
||||||
|
}
|
||||||
|
Resource yamlResource = new FileSystemResource(propertiesPath);
|
||||||
|
YamlUnstructuredLoader yamlUnstructuredLoader = new YamlUnstructuredLoader(yamlResource);
|
||||||
|
return yamlUnstructuredLoader.load();
|
||||||
|
} finally {
|
||||||
|
FileUtils.closePath(propertiesPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getManifestPath(Path pluginPath, String propertiesFileName) {
|
||||||
|
if (Files.isDirectory(pluginPath)) {
|
||||||
|
for (String location : PLUGIN_CLASSPATH.getClassesDirectories()) {
|
||||||
|
var path = pluginPath.resolve(location).resolve(propertiesFileName);
|
||||||
|
Resource propertyResource = new FileSystemResource(path);
|
||||||
|
if (propertyResource.exists()) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new PluginRuntimeException(
|
||||||
|
"找不到插件配置目录: " + DEFAULT_PROPERTIES_FILE_NAME);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return FileUtils.getPath(pluginPath, propertiesFileName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PluginRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package cd.casic.plugin.annotation;
|
||||||
|
|
||||||
|
import cd.casic.plugin.constants.PluginConstants;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author:mianbin
|
||||||
|
* @Package:cd.casic.plugin.annotation
|
||||||
|
* @Project:ops
|
||||||
|
* @name:AdminGroup
|
||||||
|
* @Date:2024/03/18 17:06
|
||||||
|
* @Filename:AdminGroup
|
||||||
|
* @description:自定义菜单Group注解
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.ANNOTATION_TYPE})
|
||||||
|
@Documented
|
||||||
|
public @interface AdminGroup {
|
||||||
|
/** 菜单名称 */
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/** 菜单组id */
|
||||||
|
String groupId();
|
||||||
|
|
||||||
|
/** 菜单图标 */
|
||||||
|
String icon() default "fa-circle-o";
|
||||||
|
|
||||||
|
/** 菜单url */
|
||||||
|
String url() default "";
|
||||||
|
|
||||||
|
/** 菜单角色 */
|
||||||
|
String[] role() default {PluginConstants.ROLE_ADMIN};
|
||||||
|
|
||||||
|
/** 菜单序号 */
|
||||||
|
int seq() default 99;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package cd.casic.plugin.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author:mianbin
|
||||||
|
* @Package:cd.casic.plugin.annotation
|
||||||
|
* @Project:ops
|
||||||
|
* @name:AdminGroups
|
||||||
|
* @Date:2024/03/18 17:05
|
||||||
|
* @Filename:AdminGroups
|
||||||
|
* @description:自定义菜单groups注解
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
@Documented
|
||||||
|
public @interface AdminGroups {
|
||||||
|
/** 菜单组 */
|
||||||
|
AdminGroup[] groups();
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package cd.casic.plugin.annotation;
|
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname BaseInformation
|
|
||||||
* @Description 扩展的元数据
|
|
||||||
* @Date 2025/5/8 15:07
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Target(ElementType.TYPE)
|
|
||||||
public @interface BasePluginInformation {
|
|
||||||
|
|
||||||
String group();
|
|
||||||
|
|
||||||
String version();
|
|
||||||
|
|
||||||
String kind();
|
|
||||||
|
|
||||||
String plural();
|
|
||||||
|
|
||||||
String singular();
|
|
||||||
}
|
|
@ -0,0 +1,22 @@
|
|||||||
|
package cd.casic.plugin.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author:mianbin
|
||||||
|
* @Package:cd.casic.plugin.annotation
|
||||||
|
* @Project:ops
|
||||||
|
* @name:InterceptPath
|
||||||
|
* @Date:2024/03/18 16:09
|
||||||
|
* @Filename:InterceptPath
|
||||||
|
* @description:插件拦截器
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Documented
|
||||||
|
public @interface InterceptPath {
|
||||||
|
/**
|
||||||
|
* 拦截的路径
|
||||||
|
*/
|
||||||
|
String[] value();
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package cd.casic.plugin.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于标记插件内部的独立配置类
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface PluginConfiguration {
|
||||||
|
String fileName() default ""; // 插件配置文件的文件名
|
||||||
|
|
||||||
|
String deploySuffix() default ""; // 插件配置文件在deployment模式下的后缀
|
||||||
|
|
||||||
|
String devSuffix() default ""; // 插件配置文件在development模式下的后缀
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package cd.casic.plugin.config;
|
||||||
|
|
||||||
|
import cd.casic.plugin.utils.PathUtils;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.pf4j.RuntimeMode;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 插件Properties
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 16:06
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EnableConfigurationProperties(PluginProperties.class)
|
||||||
|
@ConfigurationProperties(prefix = "ops.plugin")
|
||||||
|
public class PluginProperties implements InitializingBean {
|
||||||
|
|
||||||
|
public static final String GRADLE_LIBS_DIR = "build/libs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto start plugin when main app is ready.
|
||||||
|
*/
|
||||||
|
private boolean enable = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认插件路径是通过文件扫描获取的。在开发模式下,你可以指定插件路径作为项目目录。
|
||||||
|
*/
|
||||||
|
private String pluginRoot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认情况下禁用的插件.
|
||||||
|
*/
|
||||||
|
private String[] disabledPlugins = new String[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认情况需要启动的插件,要在`disabledPlugins`之前.
|
||||||
|
*/
|
||||||
|
private String[] enabledPlugins = new String[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件的类目录
|
||||||
|
*/
|
||||||
|
private List<String> classesDirectories = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 插件接口统一前缀
|
||||||
|
*/
|
||||||
|
private String restPathPrefix;
|
||||||
|
/**
|
||||||
|
* 插件restful接口前缀中是否需要包含插件id,默认包含
|
||||||
|
*/
|
||||||
|
private boolean enablePluginIdAsRestPrefix = true;
|
||||||
|
/**
|
||||||
|
* 插件jar目录.
|
||||||
|
*/
|
||||||
|
private List<String> libDirectories = new ArrayList<>(List.of(GRADLE_LIBS_DIR));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行状态
|
||||||
|
*/
|
||||||
|
private RuntimeMode runtimeMode = RuntimeMode.DEPLOYMENT;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
Path pluginsRootPath = Paths.get(pluginRoot);
|
||||||
|
if (!PathUtils.exists(pluginsRootPath, false)) {
|
||||||
|
FileUtil.mkdir(pluginsRootPath.toFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package cd.casic.plugin;
|
package cd.casic.plugin.constants;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8,8 +9,27 @@ import lombok.experimental.UtilityClass;
|
|||||||
* @Date 2025/5/8 14:51
|
* @Date 2025/5/8 14:51
|
||||||
* @Created by mianbin
|
* @Created by mianbin
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class PluginConstants {
|
public class PluginConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件静态资源目录
|
||||||
|
*/
|
||||||
|
public static final String PLUGINS_RESOURCES_DIR = "ops-module-plugins/ops-module-plugins-example-web/src/main/resources/static";
|
||||||
|
/**
|
||||||
|
* 角色:管理员
|
||||||
|
*/
|
||||||
|
public static final String ROLE_ADMIN = "admin";
|
||||||
|
/**
|
||||||
|
* 角色:用户
|
||||||
|
*/
|
||||||
|
public static final String ROLE_USER = "user";
|
||||||
|
/**
|
||||||
|
* 角色:编辑
|
||||||
|
*/
|
||||||
|
public static final String ROLE_EDITOR = "editor";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin metadata labels key.
|
* Plugin metadata labels key.
|
||||||
*/
|
*/
|
||||||
@ -28,4 +48,14 @@ public class PluginConstants {
|
|||||||
String assetsRoutePrefix(String pluginName) {
|
String assetsRoutePrefix(String pluginName) {
|
||||||
return "/plugins/" + pluginName + "/assets/";
|
return "/plugins/" + pluginName + "/assets/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class Suffix {
|
||||||
|
|
||||||
|
private Suffix() { throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String JAR = "jar";
|
||||||
|
public static final String ZIP = "zip";
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.Plugin;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 20:38
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Slf4j
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PUBLIC)
|
||||||
|
public class BasePlugin extends Plugin {
|
||||||
|
|
||||||
|
protected PluginContext context;
|
||||||
|
|
||||||
|
@Deprecated(since = "1.0.0", forRemoval = true)
|
||||||
|
public BasePlugin(PluginWrapper wrapper) {
|
||||||
|
super(wrapper);
|
||||||
|
log.warn("官方说废弃 Deprecated constructor 'BasePlugin(PluginWrapper wrapper)' called, please use "
|
||||||
|
+ "'BasePlugin(PluginContext pluginContext)' instead for plugin '{}',This "
|
||||||
|
+ "constructor will be removed in 2.19.0",
|
||||||
|
wrapper.getPluginId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据插件主类获取插件包
|
||||||
|
*
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
public String scanPackage() {
|
||||||
|
return this.getClass().getPackage().getName();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import org.pf4j.DevelopmentPluginRepository;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 设置一个固定的插件路径
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/13 10:47
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class DefaultDevelopmentPluginRepository extends DevelopmentPluginRepository {
|
||||||
|
private final List<Path> fixedPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
public DefaultDevelopmentPluginRepository(Path... pluginsRoots) {
|
||||||
|
super(pluginsRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultDevelopmentPluginRepository(List<Path> pluginsRoots) {
|
||||||
|
super(pluginsRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFixedPaths(List<Path> paths) {
|
||||||
|
if (CollectionUtils.isEmpty(paths)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fixedPaths.clear();
|
||||||
|
fixedPaths.addAll(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Path> getPluginPaths() {
|
||||||
|
List<Path> paths = new ArrayList<>(fixedPaths);
|
||||||
|
paths.addAll(super.getPluginPaths());
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deletePluginPath(Path pluginPath) {
|
||||||
|
return fixedPaths.remove(pluginPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,231 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import cd.casic.plugin.PluginApplicationContextFactory;
|
||||||
|
import cd.casic.plugin.PluginRouterFunctionRegistry;
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
|
import cd.casic.plugin.event.*;
|
||||||
|
import cd.casic.plugin.function.FinderRegistry;
|
||||||
|
import cd.casic.plugin.function.WebSocketEndpointManager;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.env.PropertySourceLoader;
|
||||||
|
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.event.ContextClosedEvent;
|
||||||
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 23:48
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DefaultPluginApplicationContextFactory implements PluginApplicationContextFactory {
|
||||||
|
|
||||||
|
private final SpringPluginManager pluginManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApplicationContext create(String pluginId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FinderManager {
|
||||||
|
|
||||||
|
private final String pluginId;
|
||||||
|
|
||||||
|
private final FinderRegistry finderRegistry;
|
||||||
|
|
||||||
|
private FinderManager(String pluginId, FinderRegistry finderRegistry) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
this.finderRegistry = finderRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||||
|
this.finderRegistry.unregister(this.pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
this.finderRegistry.register(this.pluginId, event.getApplicationContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PluginWebSocketEndpointManager {
|
||||||
|
|
||||||
|
private final WebSocketEndpointManager manager;
|
||||||
|
|
||||||
|
private List<Object> endpoints;
|
||||||
|
|
||||||
|
private PluginWebSocketEndpointManager(WebSocketEndpointManager manager) {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
var context = event.getApplicationContext();
|
||||||
|
this.endpoints = context.getBeanProvider(Object.class)
|
||||||
|
.orderedStream()
|
||||||
|
.toList();
|
||||||
|
manager.register(this.endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||||
|
manager.unregister(this.endpoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PluginRouterFunctionManager {
|
||||||
|
|
||||||
|
private final PluginRouterFunctionRegistry routerFunctionRegistry;
|
||||||
|
|
||||||
|
private Collection<RouterFunction<ServerResponse>> routerFunctions;
|
||||||
|
|
||||||
|
private PluginRouterFunctionManager(PluginRouterFunctionRegistry routerFunctionRegistry) {
|
||||||
|
this.routerFunctionRegistry = routerFunctionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||||
|
if (routerFunctions != null) {
|
||||||
|
routerFunctionRegistry.unregister(routerFunctions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
var routerFunctions = event.getApplicationContext()
|
||||||
|
.<RouterFunction<ServerResponse>>getBeanProvider(
|
||||||
|
ResolvableType.forClassWithGenerics(RouterFunction.class, ServerResponse.class)
|
||||||
|
)
|
||||||
|
.orderedStream()
|
||||||
|
.toList();
|
||||||
|
routerFunctionRegistry.register(routerFunctions);
|
||||||
|
this.routerFunctions = routerFunctions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class PluginHandlerMappingManager {
|
||||||
|
private final String pluginId;
|
||||||
|
|
||||||
|
private final PluginRequestMappingHandlerMapping handlerMapping;
|
||||||
|
|
||||||
|
private PluginHandlerMappingManager(String pluginId,
|
||||||
|
PluginRequestMappingHandlerMapping handlerMapping) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
this.handlerMapping = handlerMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
var context = event.getApplicationContext();
|
||||||
|
context.getBeansWithAnnotation(Controller.class)
|
||||||
|
.values()
|
||||||
|
.forEach(controller ->
|
||||||
|
handlerMapping.registerHandlerMethods(this.pluginId, controller)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||||
|
handlerMapping.unregister(this.pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SpringPluginStoppedEventAdapter
|
||||||
|
implements ApplicationListener<ContextClosedEvent> {
|
||||||
|
|
||||||
|
private final String pluginId;
|
||||||
|
|
||||||
|
private SpringPluginStoppedEventAdapter(String pluginId) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ContextClosedEvent event) {
|
||||||
|
var plugin = pluginManager.getPlugin(pluginId).getPlugin();
|
||||||
|
if (plugin instanceof SpringPlugin springPlugin) {
|
||||||
|
event.getApplicationContext()
|
||||||
|
.publishEvent(new SpringPluginStoppedEvent(this, springPlugin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HaloPluginEventBridge {
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(SpringPluginStartedEvent event) {
|
||||||
|
var pluginContext = event.getSpringPlugin().getPluginContext();
|
||||||
|
var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());
|
||||||
|
|
||||||
|
pluginManager.getRootContext()
|
||||||
|
.publishEvent(new OpsPluginStartedEvent(this, pluginWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(SpringPluginStoppingEvent event) {
|
||||||
|
var pluginContext = event.getSpringPlugin().getPluginContext();
|
||||||
|
var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());
|
||||||
|
pluginManager.getRootContext()
|
||||||
|
.publishEvent(new OpsPluginBeforeStopEvent(this, pluginWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationEvent(SpringPluginStoppedEvent event) {
|
||||||
|
var pluginContext = event.getSpringPlugin().getPluginContext();
|
||||||
|
var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());
|
||||||
|
pluginManager.getRootContext()
|
||||||
|
.publishEvent(new OpsPluginStoppedEvent(this, pluginWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PropertySource<?>> resolvePropertySources(String pluginId,
|
||||||
|
ResourceLoader resourceLoader) {
|
||||||
|
var opsProperties = pluginManager.getRootContext()
|
||||||
|
.getBeanProvider(PluginProperties.class)
|
||||||
|
.getIfAvailable();
|
||||||
|
if (opsProperties == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
var propertySourceLoader = new YamlPropertySourceLoader();
|
||||||
|
var propertySources = new ArrayList<PropertySource<?>>();
|
||||||
|
return propertySources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PropertySource<?>> loadPropertySources(String propertySourceName,
|
||||||
|
Resource resource,
|
||||||
|
PropertySourceLoader propertySourceLoader) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Loading property sources from {}", resource);
|
||||||
|
}
|
||||||
|
if (!resource.exists()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return propertySourceLoader.load(propertySourceName, resource);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to load property sources from " + resource, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import org.pf4j.DevelopmentPluginLoader;
|
||||||
|
import org.pf4j.PluginDescriptor;
|
||||||
|
import org.pf4j.PluginManager;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/13 10:50
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class DevPluginLoader extends DevelopmentPluginLoader {
|
||||||
|
|
||||||
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
|
public DevPluginLoader(
|
||||||
|
PluginManager pluginManager,
|
||||||
|
PluginProperties pluginProperties
|
||||||
|
) {
|
||||||
|
super(pluginManager);
|
||||||
|
this.pluginProperties = pluginProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoader loadPlugin(Path pluginPath, PluginDescriptor pluginDescriptor) {
|
||||||
|
var classesDirectories = pluginProperties.getClassesDirectories();
|
||||||
|
if (classesDirectories != null) {
|
||||||
|
classesDirectories.forEach(
|
||||||
|
classesDirectory -> pluginClasspath.addClassesDirectories(classesDirectory)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var libDirectories = pluginProperties.getLibDirectories();
|
||||||
|
if (libDirectories != null) {
|
||||||
|
libDirectories.forEach(
|
||||||
|
libDirectory -> pluginClasspath.addJarsDirectories(libDirectory)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return super.loadPlugin(pluginPath, pluginDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable(Path pluginPath) {
|
||||||
|
// 目录加载插件只能在dev模式下
|
||||||
|
return Files.isDirectory(pluginPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import cd.casic.plugin.SpringPluginFactory;
|
||||||
|
import cd.casic.plugin.YamlPluginDescriptorFinder;
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
|
import cd.casic.plugin.event.PluginStartedEvent;
|
||||||
|
import cd.casic.plugin.function.PluginGetter;
|
||||||
|
import cd.casic.plugin.function.PluginsRootGetter;
|
||||||
|
import cd.casic.plugin.function.SystemVersionSupplier;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.*;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.data.util.Lazy;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: manager plugin lifecycle
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 15:36
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OpsPluginManager extends DefaultPluginManager implements SpringPluginManager, InitializingBean {
|
||||||
|
|
||||||
|
private final ApplicationContext rootContext;
|
||||||
|
private Lazy<ApplicationContext> sharedContext;
|
||||||
|
private final PluginProperties pluginProperties;
|
||||||
|
private final PluginsRootGetter pluginsRootGetter;
|
||||||
|
private final SystemVersionSupplier systemVersionSupplier;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
//super#initialize会最早初始化,留空
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ExtensionFactory createExtensionFactory() {
|
||||||
|
return new SpringExtensionFactory(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ExtensionFinder createExtensionFinder() {
|
||||||
|
var finder = new SpringComponentsFinder(this);
|
||||||
|
addPluginStateListener(finder);
|
||||||
|
return finder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginFactory createPluginFactory() {
|
||||||
|
var contextFactory = new DefaultPluginApplicationContextFactory(this);
|
||||||
|
var pluginGetter = rootContext.getBean(PluginGetter.class);
|
||||||
|
return new SpringPluginFactory(contextFactory, pluginGetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginDescriptorFinder createPluginDescriptorFinder() {
|
||||||
|
return new YamlPluginDescriptorFinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginLoader createPluginLoader() {
|
||||||
|
var compoundLoader = new CompoundPluginLoader();
|
||||||
|
compoundLoader.add(new DevPluginLoader(this, this.pluginProperties), this::isDevelopment);
|
||||||
|
compoundLoader.add(new JarPluginLoader(this));
|
||||||
|
return compoundLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginStatusProvider createPluginStatusProvider() {
|
||||||
|
if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) {
|
||||||
|
return new PropertyPluginStatusProvider(pluginProperties);
|
||||||
|
}
|
||||||
|
return super.createPluginStatusProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginRepository createPluginRepository() {
|
||||||
|
var developmentPluginRepository =
|
||||||
|
new DefaultDevelopmentPluginRepository(getPluginsRoots());
|
||||||
|
String restPathPrefix = pluginProperties.getRestPathPrefix();
|
||||||
|
Path path = Paths.get(restPathPrefix);
|
||||||
|
developmentPluginRepository
|
||||||
|
.setFixedPaths(List.of(path));
|
||||||
|
return new CompoundPluginRepository()
|
||||||
|
.add(developmentPluginRepository, this::isDevelopment)
|
||||||
|
.add(new JarPluginRepository(getPluginsRoots()))
|
||||||
|
.add(new DefaultPluginRepository(getPluginsRoots()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Path> createPluginsRoot() {
|
||||||
|
String s = pluginsRootGetter.get();
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startPlugins() {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"don't do this operation,don't don't start all plugins"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopPlugins() {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"don't do this operation.don't don't stop all plugins"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApplicationContext getRootContext() {
|
||||||
|
return rootContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApplicationContext getSharedContext() {
|
||||||
|
return sharedContext.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginWrapper> getDependents(String pluginId) {
|
||||||
|
if (Objects.isNull(getPlugin(pluginId))) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
var dependents = new ArrayList<PluginWrapper>();
|
||||||
|
var stack = new Stack<String>();
|
||||||
|
// 依赖管理
|
||||||
|
dependencyResolver.getDependents(pluginId).forEach(stack::push);
|
||||||
|
while (!stack.isEmpty()) {
|
||||||
|
var dependent = stack.pop();
|
||||||
|
var pluginWrapper = getPlugin(dependent);
|
||||||
|
if (pluginWrapper != null) {
|
||||||
|
dependents.add(pluginWrapper);
|
||||||
|
dependencyResolver.getDependents(dependent).forEach(stack::push);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dependents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
super.runtimeMode = pluginProperties.getRuntimeMode();
|
||||||
|
this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext));
|
||||||
|
setExactVersionAllowed(pluginProperties.isEnable());
|
||||||
|
setSystemVersion(systemVersionSupplier.get().toStableVersion().toString());
|
||||||
|
super.initialize();
|
||||||
|
// the listener must be after the super#initialize
|
||||||
|
addPluginStateListener(new PluginStartedListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for plugin started event.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.17.0
|
||||||
|
*/
|
||||||
|
private static class PluginStartedListener implements PluginStateListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pluginStateChanged(PluginStateEvent event) {
|
||||||
|
if (PluginState.STARTED.equals(event.getPluginState())) {
|
||||||
|
var plugin = event.getPlugin().getPlugin();
|
||||||
|
if (plugin instanceof SpringPlugin springPlugin) {
|
||||||
|
try {
|
||||||
|
springPlugin.getApplicationContext()
|
||||||
|
.publishEvent(new PluginStartedEvent(this));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
var pluginId = event.getPlugin().getPluginId();
|
||||||
|
log.warn("Error while publishing plugin started event for plugin {}",
|
||||||
|
pluginId, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.pf4j.RuntimeMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 10:58
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PluginContext {
|
||||||
|
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
public final String configMapName;
|
||||||
|
|
||||||
|
public final String version;
|
||||||
|
|
||||||
|
public final RuntimeMode runtimeMode;
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import org.springframework.util.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/13 10:35
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class PluginRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
|
||||||
|
|
||||||
|
private final MultiValueMap<String, RequestMappingInfo> pluginMappingInfo =
|
||||||
|
new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initHandlerMethods() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerHandlerMethods(String pluginId, Object handler) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatMappings(Class<?> userType, Map<Method, RequestMappingInfo> methods) {
|
||||||
|
String packageName = ClassUtils.getPackageName(userType);
|
||||||
|
String formattedType = (StringUtils.hasText(packageName)
|
||||||
|
? Arrays.stream(packageName.split("\\."))
|
||||||
|
.map(packageSegment -> packageSegment.substring(0, 1))
|
||||||
|
.collect(Collectors.joining(".", "", "." + userType.getSimpleName())) :
|
||||||
|
userType.getSimpleName());
|
||||||
|
Function<Method, String> methodFormatter =
|
||||||
|
method -> Arrays.stream(method.getParameterTypes())
|
||||||
|
.map(Class::getSimpleName)
|
||||||
|
.collect(Collectors.joining(",", "(", ")"));
|
||||||
|
return methods.entrySet().stream()
|
||||||
|
.map(e -> {
|
||||||
|
Method method = e.getKey();
|
||||||
|
return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
|
||||||
|
})
|
||||||
|
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister(String pluginId) {
|
||||||
|
Assert.notNull(pluginId, "The pluginId must not be null.");
|
||||||
|
if (!pluginMappingInfo.containsKey(pluginId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pluginMappingInfo.remove(pluginId).forEach(this::unregisterMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<RequestMappingInfo> getMappings(String pluginId) {
|
||||||
|
List<RequestMappingInfo> requestMappingInfos = pluginMappingInfo.get(pluginId);
|
||||||
|
if (requestMappingInfos == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return List.copyOf(requestMappingInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.pf4j.PluginStatusProvider;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 读取配置文件,ops.plugin.enabled-plugins读取可用和非可用的
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/13 10:44
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class PropertyPluginStatusProvider implements PluginStatusProvider {
|
||||||
|
|
||||||
|
private final List<String> enabledPlugins;
|
||||||
|
private final List<String> disabledPlugins;
|
||||||
|
|
||||||
|
public PropertyPluginStatusProvider(PluginProperties pluginProperties) {
|
||||||
|
this.enabledPlugins = pluginProperties.getEnabledPlugins() != null
|
||||||
|
? Arrays.asList(pluginProperties.getEnabledPlugins()) : new ArrayList<>();
|
||||||
|
this.disabledPlugins = pluginProperties.getDisabledPlugins() != null
|
||||||
|
? Arrays.asList(pluginProperties.getDisabledPlugins()) : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPropertySet(PluginProperties pluginProperties) {
|
||||||
|
return !ArrayUtils.isEmpty(pluginProperties.getEnabledPlugins())
|
||||||
|
&& !ArrayUtils.isEmpty(pluginProperties.getDisabledPlugins());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPluginDisabled(String pluginId) {
|
||||||
|
if (disabledPlugins.contains(pluginId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !enabledPlugins.isEmpty() && !enabledPlugins.contains(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disablePlugin(String pluginId) {
|
||||||
|
if (isPluginDisabled(pluginId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disabledPlugins.add(pluginId);
|
||||||
|
enabledPlugins.remove(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enablePlugin(String pluginId) {
|
||||||
|
if (!isPluginDisabled(pluginId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabledPlugins.add(pluginId);
|
||||||
|
disabledPlugins.remove(pluginId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 16:19
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public enum SharedApplicationContextFactory {
|
||||||
|
;
|
||||||
|
|
||||||
|
public static ApplicationContext create(ApplicationContext rootContext) {
|
||||||
|
var sharedContext = new GenericApplicationContext();
|
||||||
|
sharedContext.registerShutdownHook();
|
||||||
|
var beanFactory = sharedContext.getBeanFactory();
|
||||||
|
// register shared object here
|
||||||
|
// var reactiveExtensionClient = rootContext.getBean(ReactiveExtensionClient.class);
|
||||||
|
// beanFactory.registerSingleton("extensionClient", extensionClient);
|
||||||
|
// beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient);
|
||||||
|
//
|
||||||
|
// DefaultSchemeManager defaultSchemeManager =
|
||||||
|
// rootContext.getBean(DefaultSchemeManager.class);
|
||||||
|
// beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
|
||||||
|
// beanFactory.registerSingleton("externalUrlSupplier",
|
||||||
|
// rootContext.getBean(ExternalUrlSupplier.class));
|
||||||
|
// beanFactory.registerSingleton("serverSecurityContextRepository",
|
||||||
|
// rootContext.getBean(ServerSecurityContextRepository.class));
|
||||||
|
// beanFactory.registerSingleton("attachmentService",
|
||||||
|
// rootContext.getBean(AttachmentService.class));
|
||||||
|
// beanFactory.registerSingleton("backupRootGetter",
|
||||||
|
// rootContext.getBean(BackupRootGetter.class));
|
||||||
|
// beanFactory.registerSingleton("notificationReasonEmitter",
|
||||||
|
// rootContext.getBean(NotificationReasonEmitter.class));
|
||||||
|
// beanFactory.registerSingleton("notificationCenter",
|
||||||
|
// rootContext.getBean(NotificationCenter.class));
|
||||||
|
// beanFactory.registerSingleton("externalLinkProcessor",
|
||||||
|
// rootContext.getBean(ExternalLinkProcessor.class));
|
||||||
|
// beanFactory.registerSingleton("postContentService",
|
||||||
|
// rootContext.getBean(PostContentService.class));
|
||||||
|
// beanFactory.registerSingleton("cacheManager",
|
||||||
|
// rootContext.getBean(CacheManager.class));
|
||||||
|
// beanFactory.registerSingleton("loginHandlerEnhancer",
|
||||||
|
// rootContext.getBean(LoginHandlerEnhancer.class));
|
||||||
|
// rootContext.getBeanProvider(PluginsRootGetter.class)
|
||||||
|
// .ifUnique(pluginsRootGetter ->
|
||||||
|
// beanFactory.registerSingleton("pluginsRootGetter", pluginsRootGetter)
|
||||||
|
// );
|
||||||
|
// beanFactory.registerSingleton("extensionGetter",
|
||||||
|
// rootContext.getBean(ExtensionGetter.class));
|
||||||
|
// rootContext.getBeanProvider(CryptoService.class)
|
||||||
|
// .ifUnique(
|
||||||
|
// cryptoService -> beanFactory.registerSingleton("cryptoService", cryptoService)
|
||||||
|
// );
|
||||||
|
// rootContext.getBeanProvider(RateLimiterRegistry.class)
|
||||||
|
// .ifUnique(rateLimiterRegistry ->
|
||||||
|
// beanFactory.registerSingleton("rateLimiterRegistry", rateLimiterRegistry)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// // Authentication plugins may need this RequestCache to handle successful login redirect
|
||||||
|
// rootContext.getBeanProvider(ServerRequestCache.class)
|
||||||
|
// .ifUnique(serverRequestCache ->
|
||||||
|
// beanFactory.registerSingleton("serverRequestCache", serverRequestCache)
|
||||||
|
// );
|
||||||
|
// rootContext.getBeanProvider(UserService.class)
|
||||||
|
// .ifUnique(userService -> beanFactory.registerSingleton("userService", userService));
|
||||||
|
// rootContext.getBeanProvider(RoleService.class)
|
||||||
|
// .ifUnique(roleService -> beanFactory.registerSingleton("roleService", roleService));
|
||||||
|
// rootContext.getBeanProvider(ReactiveUserDetailsService.class)
|
||||||
|
// .ifUnique(userDetailsService ->
|
||||||
|
// beanFactory.registerSingleton("userDetailsService", userDetailsService)
|
||||||
|
// );
|
||||||
|
// rootContext.getBeanProvider(SystemInfoGetter.class)
|
||||||
|
// .ifUnique(systemInfoGetter ->
|
||||||
|
// beanFactory.registerSingleton("systemInfoGetter", systemInfoGetter)
|
||||||
|
// );
|
||||||
|
// TODO add more shared instance here
|
||||||
|
sharedContext.refresh();
|
||||||
|
return sharedContext;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.*;
|
||||||
|
import org.pf4j.processor.ExtensionStorage;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: find idx file and load extensions
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 18:58
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SpringComponentsFinder extends AbstractExtensionFinder {
|
||||||
|
|
||||||
|
public static final String EXTENSIONS_RESOURCE = "META-INF/plugin-components.idx";
|
||||||
|
|
||||||
|
public SpringComponentsFinder(PluginManager pluginManager) {
|
||||||
|
super(pluginManager);
|
||||||
|
entries = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Set<String>> readPluginsStorages() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Set<String>> readClasspathStorages() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> readPluginStorage(PluginWrapper pluginWrapper) {
|
||||||
|
var pluginId = pluginWrapper.getPluginId();
|
||||||
|
log.debug("Reading extensions storage from plugin '{}'", pluginId);
|
||||||
|
var bucket = new HashSet<String>();
|
||||||
|
try {
|
||||||
|
log.debug("Read '{}'", EXTENSIONS_RESOURCE);
|
||||||
|
var classLoader = pluginWrapper.getPluginClassLoader();
|
||||||
|
try (var resourceStream = classLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) {
|
||||||
|
if (resourceStream == null) {
|
||||||
|
log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE);
|
||||||
|
} else {
|
||||||
|
collectExtensions(resourceStream, bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugExtensions(bucket);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to read components from " + EXTENSIONS_RESOURCE, e);
|
||||||
|
}
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectExtensions(InputStream inputStream, Set<String> bucket) throws IOException {
|
||||||
|
try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
|
||||||
|
ExtensionStorage.read(reader, bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pluginStateChanged(PluginStateEvent event) {
|
||||||
|
var pluginState = event.getPluginState();
|
||||||
|
String pluginId = event.getPlugin().getPluginId();
|
||||||
|
if (pluginState == PluginState.UNLOADED) {
|
||||||
|
entries.remove(pluginId);
|
||||||
|
} else if (pluginState == PluginState.CREATED || pluginState == PluginState.RESOLVED) {
|
||||||
|
entries.computeIfAbsent(pluginId, id -> readPluginStorage(event.getPlugin()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package cd.casic.plugin.core;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.ExtensionFactory;
|
||||||
|
import org.pf4j.PluginManager;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 16:58
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SpringExtensionFactory implements ExtensionFactory {
|
||||||
|
|
||||||
|
protected final PluginManager pluginManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public <T> T create(Class<T> extensionClass) {
|
||||||
|
return getPluginApplicationContextBy(extensionClass)
|
||||||
|
.map(context -> context.getBean(extensionClass))
|
||||||
|
.orElseGet(() -> createWithoutSpring(extensionClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> T createWithoutSpring(final Class<T> extensionClass)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
final Constructor<?> constructor =
|
||||||
|
getPublicConstructorWithShortestParameterList(extensionClass)
|
||||||
|
// An extension class is required to have at least one public constructor.
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new IllegalArgumentException("Extension class '" + nameOf(extensionClass)
|
||||||
|
+ "' must have at least one public constructor."));
|
||||||
|
try {
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace("Instantiate '" + nameOf(extensionClass) + "' by calling '" + constructor
|
||||||
|
+ "'with standard Java reflection.");
|
||||||
|
}
|
||||||
|
// Creating the instance by calling the constructor with null-parameters (if there are any).
|
||||||
|
return (T) constructor.newInstance(nullParameters(constructor));
|
||||||
|
} catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
|
||||||
|
// If one of these exceptions is thrown it it most likely because of NPE inside the
|
||||||
|
// called constructor and
|
||||||
|
// not the reflective call itself as we precisely searched for a fitting constructor.
|
||||||
|
log.error(ex.getMessage(), ex);
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Most likely this exception is thrown because the called constructor ("
|
||||||
|
+ constructor + ")"
|
||||||
|
+ " cannot handle 'null' parameters. Original message was: "
|
||||||
|
+ ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Constructor<?>> getPublicConstructorWithShortestParameterList(
|
||||||
|
final Class<?> extensionClass) {
|
||||||
|
return Stream.of(extensionClass.getConstructors())
|
||||||
|
.min(Comparator.comparing(Constructor::getParameterCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object[] nullParameters(final Constructor<?> constructor) {
|
||||||
|
return new Object[constructor.getParameterCount()];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> Optional<ApplicationContext> getPluginApplicationContextBy(
|
||||||
|
final Class<T> extensionClass) {
|
||||||
|
return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass))
|
||||||
|
.map(PluginWrapper::getPlugin)
|
||||||
|
.filter(SpringPlugin.class::isInstance)
|
||||||
|
.map(plugin -> (SpringPlugin) plugin)
|
||||||
|
.map(SpringPlugin::getApplicationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> String nameOf(final Class<T> clazz) {
|
||||||
|
return clazz.getName();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package cd.casic.plugin.core.check;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 插件运行前检查器
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/16 9:34
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public interface PluginRunningBeforeChecker {
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:08
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ApplicationContextPluginProcessor implements IPluginProcessor {
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
if (plugin.getApplicationContextIsRefresh()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.getPluginApplicationContext().setClassLoader(plugin.getPluginWrapper().getPluginClassLoader());
|
||||||
|
plugin.getPluginApplicationContext().getDefaultListableBeanFactory()
|
||||||
|
.registerSingleton(plugin.getPluginWrapper().getPluginId().trim(),
|
||||||
|
plugin.getPluginWrapper().getPlugin());
|
||||||
|
plugin.getPluginApplicationContext().refresh();
|
||||||
|
plugin.setApplicationContextIsRefresh(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
// 获取插件ApplicationContext的DefaultListableBeanFactory实例
|
||||||
|
DefaultListableBeanFactory beanFactory = plugin.getPluginApplicationContext().getDefaultListableBeanFactory();
|
||||||
|
// 根据插件ID获取Bean的名称
|
||||||
|
String[] beanNames = beanFactory.getBeanNamesForType(plugin.getPluginWrapper().getPlugin().getClass());
|
||||||
|
Arrays.stream(beanNames)
|
||||||
|
.filter(beanName -> beanName.equals(plugin.getPluginWrapper().getPluginId().trim()))
|
||||||
|
.forEach(beanFactory::destroySingleton);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.BasePlugin;
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.*;
|
||||||
|
import cd.casic.plugin.dataobject.dao.PluginFacadeMemoryCache;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static cd.casic.plugin.utils.PluginsUtils.scanClassPackageName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 17:17
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ClassGroupProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
private final List<PluginClassFilter> classFilters = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
classFilters.add(new BasicBeanFilter());
|
||||||
|
classFilters.add(new ControllerFilter());
|
||||||
|
classFilters.add(new DataBaseEntityFilter());
|
||||||
|
classFilters.add(new MapperFilter());
|
||||||
|
classFilters.add(new PluginConfigurationFilter());
|
||||||
|
classFilters.add(new WebSocketFilter());
|
||||||
|
log.info("Start to hand the ClassGroup Class for plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
List<Class<?>> classList = new ArrayList<>();
|
||||||
|
Set<String> classPackageName = scanClassPackageName(plugin.getBasePlugin().scanPackage(), plugin.getBasePlugin().getWrapper());
|
||||||
|
for (String packageName : classPackageName) {
|
||||||
|
ClassLoader loader = PluginFacadeMemoryCache.getPluginClassLoader(plugin.getPluginId());
|
||||||
|
log.info("Load class {} using classloader {} for plugin {}", packageName, PluginFacadeMemoryCache.getPluginInfo(plugin.getPluginId()), plugin.getPluginId());
|
||||||
|
Class<?> clazz = loader.loadClass(packageName);
|
||||||
|
if (!BasePlugin.class.isAssignableFrom(clazz)) {
|
||||||
|
classList.add(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.setClassList(classList);
|
||||||
|
List<Class<?>> pluginClassList = plugin.getClassList().stream().filter(item -> !item.isInterface()).collect(Collectors.toList());
|
||||||
|
if (!pluginClassList.isEmpty()) {
|
||||||
|
for (Class<?> clazz : pluginClassList) {
|
||||||
|
boolean grouped = false;
|
||||||
|
for (PluginClassFilter filter : classFilters) {
|
||||||
|
if (filter.filter(clazz)) {
|
||||||
|
log.info("将类 {} 添加到 {} 分组", clazz, filter.groupName());
|
||||||
|
plugin.addGroupClass(filter.groupName(), clazz);
|
||||||
|
grouped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!grouped) {
|
||||||
|
log.info("类 {} 不属于任何已知分组", clazz);
|
||||||
|
plugin.addGroupClass("unknown", clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
plugin.getClassList().clear();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.ControllerFilter;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.extra.spring.SpringUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:10
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ControllerProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||||
|
|
||||||
|
Method getMappingForMethod;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
// 这里反射获取 getMappingForMethod
|
||||||
|
requestMappingHandlerMapping = SpringUtil.getBean(RequestMappingHandlerMapping.class);
|
||||||
|
getMappingForMethod = ReflectionUtils.findMethod(RequestMappingHandlerMapping.class, "getMappingForMethod", Method.class, Class.class);
|
||||||
|
getMappingForMethod.setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
ApplicationContext applicationContext = plugin.getMainApplicationContext();
|
||||||
|
PluginProperties pluginSystemProperties = applicationContext.getBean(PluginProperties.class);
|
||||||
|
for (Class<?> aClass : plugin.getGroupClass(ControllerFilter.GROUP_NAME)) {
|
||||||
|
setPathPrefix(plugin.getPluginId(), aClass, pluginSystemProperties);
|
||||||
|
plugin.addController(aClass);
|
||||||
|
Object bean = plugin.getPluginApplicationContext().getBean(aClass);
|
||||||
|
Method[] methods = aClass.getMethods();
|
||||||
|
for (Method method : methods) {
|
||||||
|
if (method.getAnnotation(RequestMapping.class) != null
|
||||||
|
|| method.getAnnotation(GetMapping.class) != null
|
||||||
|
|| method.getAnnotation(PostMapping.class) != null
|
||||||
|
|| method.getAnnotation(DeleteMapping.class) != null
|
||||||
|
|| method.getAnnotation(PutMapping.class) != null
|
||||||
|
|| method.getAnnotation(PatchMapping.class) != null) {
|
||||||
|
RequestMappingInfo requestMappingInfo = (RequestMappingInfo) getMappingForMethod.invoke(requestMappingHandlerMapping, method, aClass);
|
||||||
|
requestMappingHandlerMapping.registerMapping(requestMappingInfo, bean, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
for (RequestMappingInfo requestMappingInfo : getRequestMappingInfo(plugin)) {
|
||||||
|
requestMappingHandlerMapping.unregisterMapping(requestMappingInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RequestMappingInfo> getRequestMappingInfo(PluginInfo plugin) throws Exception {
|
||||||
|
List<RequestMappingInfo> requestMappingInfoList = new ArrayList<>();
|
||||||
|
for (Class<?> aClass : plugin.getGroupClass(ControllerFilter.GROUP_NAME)) {
|
||||||
|
Method[] methods = aClass.getMethods();
|
||||||
|
for (Method method : methods) {
|
||||||
|
RequestMappingInfo requestMappingInfo = (RequestMappingInfo) getMappingForMethod.invoke(requestMappingHandlerMapping, method, aClass);
|
||||||
|
requestMappingInfoList.add(requestMappingInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestMappingInfoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置请求路径前缀
|
||||||
|
*
|
||||||
|
* @param aClass controller 类
|
||||||
|
*/
|
||||||
|
private void setPathPrefix(String pluginId, Class<?> aClass, PluginProperties pluginSystemProperties) {
|
||||||
|
RequestMapping requestMapping = aClass.getAnnotation(RequestMapping.class);
|
||||||
|
if (requestMapping == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String pathPrefix = pluginSystemProperties.getRestPathPrefix();
|
||||||
|
if (pluginSystemProperties.isEnablePluginIdAsRestPrefix()) {
|
||||||
|
if (StrUtil.isNotEmpty(pathPrefix)) {
|
||||||
|
pathPrefix = joiningPath(pathPrefix, pluginId);
|
||||||
|
} else {
|
||||||
|
pathPrefix = pluginId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (StrUtil.isEmpty(pathPrefix)) {
|
||||||
|
// 不启用插件id作为路径前缀, 并且路径前缀为空, 则直接返回。
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleRequestMapping(requestMapping, pathPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void handleRequestMapping(RequestMapping requestMapping, String pathPrefix) {
|
||||||
|
InvocationHandler invocationHandler = Proxy.getInvocationHandler(requestMapping);
|
||||||
|
Set<String> definePaths = new HashSet<>();
|
||||||
|
definePaths.addAll(Arrays.asList(requestMapping.path()));
|
||||||
|
definePaths.addAll(Arrays.asList(requestMapping.value()));
|
||||||
|
try {
|
||||||
|
Field field = invocationHandler.getClass().getDeclaredField("memberValues");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Map<String, Object> memberValues = (Map<String, Object>) field.get(invocationHandler);
|
||||||
|
String[] newPath = new String[definePaths.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (String definePath : definePaths) {
|
||||||
|
// 解决插件启用、禁用后, 路径前缀重复的问题。
|
||||||
|
if (definePath.contains(pathPrefix)) {
|
||||||
|
newPath[i++] = definePath;
|
||||||
|
} else {
|
||||||
|
newPath[i++] = joiningPath(pathPrefix, definePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newPath.length == 0) {
|
||||||
|
newPath = new String[]{pathPrefix};
|
||||||
|
}
|
||||||
|
memberValues.put("path", newPath);
|
||||||
|
memberValues.put("value", new String[]{});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Define Plugin RestController pathPrefix error : {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接路径
|
||||||
|
*
|
||||||
|
* @param path1 路径1
|
||||||
|
* @param path2 路径2
|
||||||
|
* @return 拼接的路径
|
||||||
|
*/
|
||||||
|
private String joiningPath(String path1, String path2) {
|
||||||
|
if (path1 != null && path2 != null) {
|
||||||
|
if (path1.endsWith("/") && path2.startsWith("/")) {
|
||||||
|
return path1 + path2.substring(1);
|
||||||
|
} else if (!path1.endsWith("/") && !path2.startsWith("/")) {
|
||||||
|
return path1 + "/" + path2;
|
||||||
|
} else {
|
||||||
|
return path1 + path2;
|
||||||
|
}
|
||||||
|
} else if (path1 != null) {
|
||||||
|
return path1;
|
||||||
|
} else if (path2 != null) {
|
||||||
|
return path2;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import com.anwen.mongo.annotation.collection.CollectionName;
|
||||||
|
import com.anwen.mongo.conn.CollectionManager;
|
||||||
|
import com.anwen.mongo.conn.ConnectMongoDB;
|
||||||
|
import com.anwen.mongo.convert.CollectionNameConvert;
|
||||||
|
import com.anwen.mongo.manager.MongoPlusClient;
|
||||||
|
import com.anwen.mongo.mapper.BaseMapper;
|
||||||
|
import com.anwen.mongo.service.IService;
|
||||||
|
import com.anwen.mongo.service.impl.ServiceImpl;
|
||||||
|
import com.mongodb.MongoException;
|
||||||
|
import com.mongodb.client.MongoCollection;
|
||||||
|
import com.mongodb.client.MongoDatabase;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.Document;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 18:25
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class MongoProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
private AnnotationConfigApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
log.info("Start to hand the Mongo Class for plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
applicationContext = plugin.getPluginApplicationContext();
|
||||||
|
applicationContext.getBeansOfType(IService.class)
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.filter(s -> s instanceof ServiceImpl)
|
||||||
|
.forEach(s -> {
|
||||||
|
Class<?> clazz = s.getGenericityClass();
|
||||||
|
ServiceImpl<?> serviceImpl = (ServiceImpl<?>) s;
|
||||||
|
serviceImpl.setClazz(clazz);
|
||||||
|
String database = initFactory(clazz);
|
||||||
|
// 这里需要将MongoPlusClient给工厂
|
||||||
|
serviceImpl.setDatabase(database);
|
||||||
|
serviceImpl.setBaseMapper(applicationContext.getBean("baseMapper", BaseMapper.class));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
//卸载暂时都先不处理
|
||||||
|
}
|
||||||
|
|
||||||
|
private String initFactory(Class<?> clazz) {
|
||||||
|
String collectionName = clazz.getSimpleName().toLowerCase();
|
||||||
|
final String[] dataBaseName = {""};
|
||||||
|
if (clazz.isAnnotationPresent(CollectionName.class)) {
|
||||||
|
CollectionName annotation = clazz.getAnnotation(CollectionName.class);
|
||||||
|
collectionName = annotation.value();
|
||||||
|
dataBaseName[0] = annotation.database();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String finalCollectionName = collectionName;
|
||||||
|
final String[] finalDataBaseName = {dataBaseName[0]};
|
||||||
|
List<MongoDatabase> mongoDatabaseList = new ArrayList<>();
|
||||||
|
MongoPlusClient mongoPlusClient = applicationContext.getBean("mongoPlusClient", MongoPlusClient.class);
|
||||||
|
String database = mongoPlusClient.getBaseProperty().getDatabase();
|
||||||
|
Arrays.stream(database.split(",")).collect(Collectors.toList()).forEach(db -> {
|
||||||
|
CollectionNameConvert collectionNameConvert = applicationContext.getBean("collectionNameConvert", CollectionNameConvert.class);
|
||||||
|
CollectionManager collectionManager = new CollectionManager(mongoPlusClient.getMongoClient(), collectionNameConvert, db);
|
||||||
|
ConnectMongoDB connectMongodb = new ConnectMongoDB(mongoPlusClient.getMongoClient(), db, finalCollectionName);
|
||||||
|
MongoDatabase mongoDatabase = mongoPlusClient.getMongoClient().getDatabase(db);
|
||||||
|
mongoDatabaseList.add(mongoDatabase);
|
||||||
|
if (Objects.equals(db, finalDataBaseName[0])) {
|
||||||
|
MongoCollection<Document> collection = connectMongodb.open(mongoDatabase);
|
||||||
|
collectionManager.setCollectionMap(finalCollectionName, collection);
|
||||||
|
}
|
||||||
|
mongoPlusClient.getCollectionManager().put(db, collectionManager);
|
||||||
|
});
|
||||||
|
mongoPlusClient.setMongoDatabase(mongoDatabaseList);
|
||||||
|
} catch (MongoException e) {
|
||||||
|
log.error("Failed to connect to MongoDB: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return dataBaseName[0];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.util.FileUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.JarURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
|
import static cd.casic.plugin.constants.PluginConstants.PLUGINS_RESOURCES_DIR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:14
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ResourcesProcessor implements IPluginProcessor {
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
Path pluginPath = plugin.getPluginWrapper().getPluginPath();
|
||||||
|
Set<String> staticClassPathLocations = plugin.getStaticClassPathLocations();
|
||||||
|
File jarFile = null;
|
||||||
|
if (Files.isDirectory(pluginPath)) {
|
||||||
|
List<File> jars = FileUtils.getJars(pluginPath);
|
||||||
|
jarFile = jars.get(0);
|
||||||
|
} else if (pluginPath.toFile().getName().toLowerCase().endsWith(".jar")) {
|
||||||
|
jarFile = pluginPath.toFile();
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("不正确的pluginPath");
|
||||||
|
}
|
||||||
|
Enumeration<JarEntry> jarEntries = new JarFile(jarFile).entries();
|
||||||
|
File file = new File(PLUGINS_RESOURCES_DIR + File.separator + plugin.getPluginId());
|
||||||
|
if (!file.exists()) {
|
||||||
|
FileUtil.mkdir(file);
|
||||||
|
}
|
||||||
|
FileUtil.clean(file);
|
||||||
|
while (jarEntries.hasMoreElements()) {
|
||||||
|
JarEntry entry = jarEntries.nextElement();
|
||||||
|
String jarEntryName = entry.getName();
|
||||||
|
for (String staticClassPathLocation : staticClassPathLocations) { //staticClassPathLocation里读取到所有静态资源的位置 然后将以插件为单位 打包到web的static目录下 即可访问
|
||||||
|
if (!staticClassPathLocation.equals(jarEntryName) && jarEntryName.startsWith(staticClassPathLocation)
|
||||||
|
&& !jarEntryName.endsWith(".class") && !entry.isDirectory()) {
|
||||||
|
URL url = new URL("jar:file:" + jarFile.getAbsolutePath() + "!/" + jarEntryName);
|
||||||
|
JarURLConnection jarConnection = (JarURLConnection) url.openConnection();
|
||||||
|
InputStream in = jarConnection.getInputStream();
|
||||||
|
File file1 = new File(file.getAbsolutePath() + File.separator + jarEntryName);
|
||||||
|
FileUtil.touch(file1.getAbsolutePath());
|
||||||
|
int index;
|
||||||
|
byte[] bytes = new byte[1024];
|
||||||
|
FileOutputStream downloadFile = new FileOutputStream(file.getAbsolutePath() + File.separator + jarEntryName);
|
||||||
|
while ((index = in.read(bytes)) != -1) {
|
||||||
|
downloadFile.write(bytes, 0, index);
|
||||||
|
downloadFile.flush();
|
||||||
|
}
|
||||||
|
downloadFile.close();
|
||||||
|
in.close();
|
||||||
|
JarFile currJarFile = jarConnection.getJarFile();
|
||||||
|
currJarFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
File file = new File(PLUGINS_RESOURCES_DIR + File.separator + plugin.getPluginId());
|
||||||
|
if (file.exists()) {
|
||||||
|
FileUtil.del(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.BasicBeanFilter;
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.ControllerFilter;
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.MapperFilter;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
|
||||||
|
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||||
|
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:17
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SpringBeanRegisterProcessor implements IPluginProcessor {
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
List<Class<?>> basicBeanClassList = new ArrayList<>();
|
||||||
|
basicBeanClassList.addAll(plugin.getGroupClass(BasicBeanFilter.GROUP_NAME));
|
||||||
|
basicBeanClassList.addAll(plugin.getGroupClass(MapperFilter.GROUP_NAME));
|
||||||
|
basicBeanClassList.addAll(plugin.getGroupClass(ControllerFilter.GROUP_NAME));
|
||||||
|
// if(!basicBeanClassList.isEmpty()){
|
||||||
|
// plugin.getPluginApplicationContext().register(basicBeanClassList.toArray(new Class[0]));
|
||||||
|
// }
|
||||||
|
basicBeanClassList.forEach(clazz -> {
|
||||||
|
AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(clazz);
|
||||||
|
beanDefinition.setBeanClass(clazz);
|
||||||
|
BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
|
||||||
|
String beanName = beanNameGenerator.generateBeanName(beanDefinition, plugin.getPluginApplicationContext());
|
||||||
|
plugin.getPluginApplicationContext().registerBeanDefinition(beanName, beanDefinition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cn.hutool.core.util.ReflectUtil;
|
||||||
|
import cn.hutool.extra.spring.SpringUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springdoc.api.AbstractOpenApiResource;
|
||||||
|
import org.springdoc.core.service.OpenAPIService;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:17
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SpringDocProcessor implements IPluginProcessor {
|
||||||
|
private Set<Class<?>> restControllers;
|
||||||
|
private OpenAPIService openAPIService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
ApplicationContext applicationContext = SpringUtil.getApplicationContext();
|
||||||
|
AbstractOpenApiResource openApiResource = applicationContext.getBean(AbstractOpenApiResource.class);
|
||||||
|
try {
|
||||||
|
// 获取OpenApiResource的ADDITIONAL_REST_CONTROLLERS字段
|
||||||
|
Field additionalRestControllers = ReflectUtil.getField(openApiResource.getClass(), "ADDITIONAL_REST_CONTROLLERS");
|
||||||
|
additionalRestControllers.setAccessible(true);
|
||||||
|
restControllers = (Set<Class<?>>) additionalRestControllers.get(openApiResource);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
restControllers = null;
|
||||||
|
}
|
||||||
|
openAPIService = applicationContext.getBean(OpenAPIService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
if (restControllers != null) {
|
||||||
|
// 将插件中的Controller类添加到OpenApiResource的ADDITIONAL_REST_CONTROLLERS字段
|
||||||
|
restControllers.addAll(plugin.getControllers());
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
if (restControllers != null && !restControllers.isEmpty()) {
|
||||||
|
for (Class<?> clazz : plugin.getControllers()) {
|
||||||
|
// 从OpenApiResource的ADDITIONAL_REST_CONTROLLERS字段中移除插件中的Controller类
|
||||||
|
restControllers.remove(clazz);
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
plugin.getControllers().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refresh() {
|
||||||
|
if (openAPIService != null) {
|
||||||
|
// 手动刷新OpenApiResource
|
||||||
|
openAPIService.setCachedOpenAPI(null, Locale.getDefault());
|
||||||
|
// openAPIService.resetCalculatedOpenAPI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package cd.casic.plugin.core.register;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:19
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class StartPluginManagerProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private List<IPluginProcessor> iPluginProcessors;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
iPluginProcessors.stream().forEach(var -> {
|
||||||
|
try {
|
||||||
|
var.initialize();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
iPluginProcessors.stream().forEach(var -> {
|
||||||
|
try {
|
||||||
|
var.registry(plugin);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
iPluginProcessors.stream().forEach(var -> {
|
||||||
|
try {
|
||||||
|
var.unRegistry(plugin);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package cd.casic.plugin.core.register.config;
|
||||||
|
|
||||||
|
import cd.casic.plugin.annotation.PluginConfiguration;
|
||||||
|
import cd.casic.plugin.config.PluginProperties;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cd.casic.plugin.utils.ConfigFileUtils;
|
||||||
|
import cd.casic.plugin.utils.PathUtils;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.pf4j.RuntimeMode;
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 自定义的配置文件
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 9:45
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ConfigurationFileProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
log.info("Start to hand the configuration Class for plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
RuntimeMode runtimeMode = pluginProperties.getRuntimeMode();
|
||||||
|
for (Class<?> aClass : plugin.getClassList()) {
|
||||||
|
PluginConfiguration configDefinition = aClass.getAnnotation(PluginConfiguration.class);
|
||||||
|
if (configDefinition == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String fileName = getConfigFileName(configDefinition, runtimeMode);
|
||||||
|
DefaultListableBeanFactory defaultListableBeanFactory = plugin.getPluginApplicationContext().getDefaultListableBeanFactory();
|
||||||
|
String name = aClass.getSimpleName();
|
||||||
|
Path yamlPath = PathUtils.getYamlPath(plugin.getPluginWrapper().getPluginPath(), fileName);
|
||||||
|
Resource resource = new FileSystemResource(yamlPath);
|
||||||
|
Object parseObject = convert(resource, aClass);
|
||||||
|
if (!defaultListableBeanFactory.containsSingleton(name)) {
|
||||||
|
defaultListableBeanFactory.registerSingleton(name, parseObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
if (!plugin.getPluginConfigObjects().isEmpty()) {
|
||||||
|
plugin.getPluginConfigObjects().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件独立配置文件的文件名
|
||||||
|
*
|
||||||
|
* @param pluginConfiguration 插件配置类上的自定义注解
|
||||||
|
* @param runtimeMode 插件系统运行模式
|
||||||
|
* @return 插件独立配置文件的文件名
|
||||||
|
*/
|
||||||
|
private String getConfigFileName(PluginConfiguration pluginConfiguration, RuntimeMode runtimeMode) {
|
||||||
|
String fileName = pluginConfiguration.fileName();
|
||||||
|
if (StringUtils.isBlank(fileName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String suffix = "";
|
||||||
|
if (runtimeMode == RuntimeMode.DEPLOYMENT) {
|
||||||
|
// deployment模式
|
||||||
|
suffix = pluginConfiguration.deploySuffix();
|
||||||
|
} else if (runtimeMode == RuntimeMode.DEVELOPMENT) {
|
||||||
|
// development模式
|
||||||
|
suffix = pluginConfiguration.devSuffix();
|
||||||
|
}
|
||||||
|
return ConfigFileUtils.joinConfigFileName(fileName, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private Object convert(Resource resource, Class<?> configClass) throws Exception {
|
||||||
|
// 创建 ObjectMapper 和 YAMLFactory 实例
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
YAMLFactory yamlFactory = new YAMLFactory();
|
||||||
|
|
||||||
|
// 使用 try-with-resources 语句自动管理资源
|
||||||
|
try (InputStream inputStream = resource.getInputStream();
|
||||||
|
YAMLParser yamlParser = yamlFactory.createParser(inputStream)) {
|
||||||
|
final JsonNode node = objectMapper.readTree(yamlParser);
|
||||||
|
if (node == null) {
|
||||||
|
return configClass.getConstructor().newInstance();
|
||||||
|
}
|
||||||
|
try (TreeTraversingParser treeTraversingParser = new TreeTraversingParser(node)) {
|
||||||
|
return objectMapper.readValue(treeTraversingParser, configClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,250 @@
|
|||||||
|
package cd.casic.plugin.core.register.config;
|
||||||
|
|
||||||
|
import cd.casic.plugin.OpsPluginDescriptor;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cd.casic.plugin.utils.ConfigFileUtils;
|
||||||
|
import cd.casic.plugin.utils.PathUtils;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.PluginDescriptor;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.boot.env.PropertiesPropertySourceLoader;
|
||||||
|
import org.springframework.boot.env.PropertySourceLoader;
|
||||||
|
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 11:08
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SpringAutoConfigurationFileProcessor implements IPluginProcessor {
|
||||||
|
List<PropertySourceLoader> propertySourceLoaders = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "active profiles" property name.
|
||||||
|
*/
|
||||||
|
private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "includes profiles" property name.
|
||||||
|
*/
|
||||||
|
private static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
propertySourceLoaders.add(new YamlPropertySourceLoader());
|
||||||
|
propertySourceLoaders.add(new PropertiesPropertySourceLoader());
|
||||||
|
log.info("Start to hand the SpringAutoConfiguration Class for plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
PluginDescriptor descriptor = plugin.getPluginWrapper().getDescriptor();
|
||||||
|
ConfigurableEnvironment environment = plugin.getPluginApplicationContext().getEnvironment();
|
||||||
|
if (!(descriptor instanceof OpsPluginDescriptor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Resource> resourcesFromDescriptor = getConfigResourceFromDescriptor(plugin);
|
||||||
|
List<PropertySource<?>> propProfiles = getPropProfiles(resourcesFromDescriptor);
|
||||||
|
if (ObjectUtils.isEmpty(propProfiles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (PropertySource<?> propertySource : propProfiles) {
|
||||||
|
environment.getPropertySources().addLast(propertySource);
|
||||||
|
}
|
||||||
|
// 发现原始文件中配置的 profiles
|
||||||
|
List<Profile> profiles = getProfiles(environment);
|
||||||
|
if (!ObjectUtils.isEmpty(profiles)) {
|
||||||
|
loadProfilesConfig(plugin.getPluginWrapper().getPluginPath(), (OpsPluginDescriptor) descriptor, environment, profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationPropertiesBindingPostProcessor.register(plugin.getPluginApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
// 暂时无法处理,目前关闭pluginApplicationContext,但是后陆要处理
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Resource> getConfigResourceFromDescriptor(PluginInfo plugin) {
|
||||||
|
log.info("插件 {} 尝试从插件描述中读取配置文件名", plugin.getPluginId());
|
||||||
|
PluginDescriptor descriptor = plugin.getPluginWrapper().getDescriptor();
|
||||||
|
OpsPluginDescriptor opsPluginDescriptor = (OpsPluginDescriptor) descriptor;
|
||||||
|
String configFileName = opsPluginDescriptor.getConfigFileName();
|
||||||
|
if (StrUtil.isEmpty(configFileName)) {
|
||||||
|
log.info("插件 {} 的插件描述中未设置配置文件名", plugin.getPluginId());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> configFileActiveNames = getConfigFileActiveNames(configFileName, opsPluginDescriptor.getConfigFileActive());
|
||||||
|
log.info("插件{} 的配置文件为 {} {}", plugin.getPluginId(), configFileName, configFileActiveNames);
|
||||||
|
Path pluginPath = plugin.getPluginWrapper().getPluginPath();
|
||||||
|
|
||||||
|
List<Resource> configFileResources = configFileActiveNames.stream()
|
||||||
|
.map(configFileActiveName -> PathUtils.getYamlPath(pluginPath, configFileActiveName))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(FileSystemResource::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
configFileResources.add(new FileSystemResource(PathUtils.getYamlPath(pluginPath, configFileName)));
|
||||||
|
return configFileResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 spring.profiles.active/spring.profiles.include 定义的配置
|
||||||
|
* @param environment ConfigurableEnvironment
|
||||||
|
* @param profiles 主配置文件中定义的值
|
||||||
|
* @throws Exception Exception
|
||||||
|
*/
|
||||||
|
private void loadProfilesConfig(Path pluginPath, OpsPluginDescriptor opsPluginDescriptor,
|
||||||
|
ConfigurableEnvironment environment, List<Profile> profiles) throws Exception {
|
||||||
|
// 解析当前文件名称
|
||||||
|
for (Profile profile : profiles) {
|
||||||
|
String name = profile.getName();
|
||||||
|
String fileName = ConfigFileUtils.joinConfigFileName(opsPluginDescriptor.getConfigFileName(), name);
|
||||||
|
|
||||||
|
|
||||||
|
Path configFilePath = PathUtils.getYamlPath(pluginPath, fileName);
|
||||||
|
FileSystemResource configFileResource = new FileSystemResource(configFilePath);
|
||||||
|
if(ObjectUtils.isEmpty(configFileResource)){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<PropertySource<?>> propProfiles = getPropProfiles(Collections.singletonList(configFileResource));
|
||||||
|
if(ObjectUtils.isEmpty(propProfiles)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (PropertySource<?> propertySource : propProfiles) {
|
||||||
|
environment.getPropertySources().addLast(propertySource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重新设置 ActiveProfiles
|
||||||
|
String[] names = profiles.stream()
|
||||||
|
.filter((profile) -> profile != null && !profile.isDefaultProfile())
|
||||||
|
.map(Profile::getName)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
environment.setActiveProfiles(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<String> getConfigFileActiveNames(String configFileName, List<String> configFileActives) {
|
||||||
|
return configFileActives.stream()
|
||||||
|
.map(configFileActive -> ConfigFileUtils.joinConfigFileName(configFileName, configFileActive))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canLoadFileExtension(PropertySourceLoader loader, String name) {
|
||||||
|
return Arrays.stream(loader.getFileExtensions())
|
||||||
|
.anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name,
|
||||||
|
fileExtension));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Resource 中解析出 PropertySource
|
||||||
|
*
|
||||||
|
* @param resources resources
|
||||||
|
* @return List
|
||||||
|
* @throws IOException 加载文件 IOException 异常
|
||||||
|
*/
|
||||||
|
private List<PropertySource<?>> getPropProfiles(List<Resource> resources) throws IOException {
|
||||||
|
List<PropertySource<?>> propProfiles = new ArrayList<>();
|
||||||
|
if (resources == null || resources.isEmpty()) {
|
||||||
|
return propProfiles;
|
||||||
|
}
|
||||||
|
for (Resource resource : resources) {
|
||||||
|
if (resource == null || !resource.exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String filename = resource.getFilename();
|
||||||
|
if (ObjectUtils.isEmpty(filename)) {
|
||||||
|
log.error("File name is empty!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {
|
||||||
|
if (!canLoadFileExtension(propertySourceLoader, filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info("正在从 {} 读取插件配置", filename);
|
||||||
|
List<PropertySource<?>> propertySources = propertySourceLoader.load(filename, resource);
|
||||||
|
if (ObjectUtils.isEmpty(propertySources)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
propProfiles.addAll(propertySources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return propProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Profile> getProfiles(Environment environment) {
|
||||||
|
List<Profile> profiles = new ArrayList<>();
|
||||||
|
Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty(environment);
|
||||||
|
profiles.addAll(getOtherActiveProfiles(environment, activatedViaProperty));
|
||||||
|
profiles.addAll(activatedViaProperty);
|
||||||
|
profiles.removeIf(
|
||||||
|
(profile) -> (profile != null && profile.isDefaultProfile()));
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Profile> getProfilesActivatedViaProperty(Environment environment) {
|
||||||
|
if (!environment.containsProperty(ACTIVE_PROFILES_PROPERTY)
|
||||||
|
&& !environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
Binder binder = Binder.get(environment);
|
||||||
|
Set<Profile> activeProfiles = new LinkedHashSet<>();
|
||||||
|
activeProfiles.addAll(getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
|
||||||
|
activeProfiles.addAll(getProfiles(binder, ACTIVE_PROFILES_PROPERTY));
|
||||||
|
return activeProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Profile> getOtherActiveProfiles(Environment environment, Set<Profile> activatedViaProperty) {
|
||||||
|
return Arrays.stream(environment.getActiveProfiles()).map(Profile::new)
|
||||||
|
.filter((profile) -> !activatedViaProperty.contains(profile))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Profile> getProfiles(Binder binder, String name) {
|
||||||
|
return binder.bind(name, String[].class).map(this::asProfileSet)
|
||||||
|
.orElse(Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Profile> asProfileSet(String[] profileNames) {
|
||||||
|
List<Profile> profiles = new ArrayList<>();
|
||||||
|
for (String profileName : profileNames) {
|
||||||
|
profiles.add(new Profile(profileName));
|
||||||
|
}
|
||||||
|
return new LinkedHashSet<>(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class Profile {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private boolean defaultProfile;
|
||||||
|
|
||||||
|
Profile(String name) {
|
||||||
|
this(name, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package cd.casic.plugin.core.register.database;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 目前只支持这两个
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 17:06
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum DBEnums {
|
||||||
|
MYSQL("mysql"),
|
||||||
|
SQLITE("sqlite");
|
||||||
|
|
||||||
|
final String DBType;
|
||||||
|
|
||||||
|
DBEnums(String DBType) {
|
||||||
|
this.DBType = DBType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DBEnums getEnumByString(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case "mysql":
|
||||||
|
return MYSQL;
|
||||||
|
case "sqlite":
|
||||||
|
return SQLITE;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package cd.casic.plugin.core.register.database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 17:05
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public interface DataBaseProperty {
|
||||||
|
DBEnums type();
|
||||||
|
|
||||||
|
String driver();
|
||||||
|
|
||||||
|
String url();
|
||||||
|
|
||||||
|
String username();
|
||||||
|
|
||||||
|
String password();
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package cd.casic.plugin.core.register.database;
|
||||||
|
|
||||||
|
import cd.casic.plugin.OpsPluginDescriptor;
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.DataBaseEntityFilter;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cd.casic.plugin.utils.dBUtils.SQLGenerator;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.PluginDescriptor;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 17:07
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class DatabaseProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
log.info("Start to hand the Database Class for plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
DataBaseProperty dataBaseProperty;
|
||||||
|
SQLGenerator sqlGenerator;
|
||||||
|
PluginDescriptor descriptor = plugin.getPluginWrapper().getDescriptor();
|
||||||
|
if (!(descriptor instanceof OpsPluginDescriptor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OpsPluginDescriptor opsPluginDescriptor = (OpsPluginDescriptor) descriptor;
|
||||||
|
if (StrUtil.isNotEmpty(opsPluginDescriptor.getConfigFileName())) {
|
||||||
|
// 使用插件独立数据源
|
||||||
|
AnnotationConfigApplicationContext pluginApplicationContext = plugin.getPluginApplicationContext();
|
||||||
|
try {
|
||||||
|
dataBaseProperty = pluginApplicationContext.getBean(DataBaseProperty.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
dataBaseProperty = null;
|
||||||
|
log.info("插件未配置独立数据源");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataBaseProperty = null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sqlGenerator = plugin.getMainApplicationContext().getBean(SQLGenerator.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("无法获取SqlGenerator类型的Bean对象");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Class<?>> entities = plugin.getGroupClass(DataBaseEntityFilter.GROUP_NAME);
|
||||||
|
for (Class<?> entity : entities) {
|
||||||
|
sqlGenerator.sqlDeleteGenerator(entity, dataBaseProperty, plugin.getPluginId());
|
||||||
|
sqlGenerator.sqlGenerator(entity, dataBaseProperty, plugin.getPluginId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter;
|
||||||
|
|
||||||
|
public interface PluginClassFilter{
|
||||||
|
String groupName();
|
||||||
|
|
||||||
|
void initialize();
|
||||||
|
|
||||||
|
boolean filter(Class<?> clazz);
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
import cd.casic.plugin.utils.AnnotationUtils;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
public class BasicBeanFilter implements PluginClassFilter {
|
||||||
|
public static final String GROUP_NAME = "basic_bean";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String groupName() {
|
||||||
|
return GROUP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Class<?> clazz) {
|
||||||
|
return AnnotationUtils.hasAnnotations(clazz, false, Bean.class,
|
||||||
|
Service.class,
|
||||||
|
Component.class,
|
||||||
|
Configuration.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter.impl;
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
import cd.casic.plugin.utils.AnnotationUtils;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller 过滤器,筛选带有Controller或者RestController注解的类
|
||||||
|
*/
|
||||||
|
public class ControllerFilter implements PluginClassFilter {
|
||||||
|
public static final String GROUP_NAME = "spring_controller";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String groupName() {
|
||||||
|
return GROUP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Class<?> clazz) {
|
||||||
|
return AnnotationUtils.hasAnnotations(clazz, false, Controller.class, RestController.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
import cd.casic.plugin.utils.AnnotationUtils;
|
||||||
|
import cd.casic.plugin.utils.dBUtils.annotation.Entity;
|
||||||
|
|
||||||
|
public class DataBaseEntityFilter implements PluginClassFilter {
|
||||||
|
public static final String GROUP_NAME = "database_entity";
|
||||||
|
@Override
|
||||||
|
public String groupName() {
|
||||||
|
return GROUP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Class<?> clazz) {
|
||||||
|
return AnnotationUtils.hasAnnotations(clazz, false, Entity.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
public class MapperFilter implements PluginClassFilter {
|
||||||
|
public static final String GROUP_NAME = "mapper";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String groupName() {
|
||||||
|
return GROUP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Class<?> clazz) {
|
||||||
|
return clazz.isAnnotationPresent(Mapper.class) || clazz.isAnnotationPresent(Repository.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.plugin.annotation.PluginConfiguration;
|
||||||
|
import cd.casic.plugin.core.BasePlugin;
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
|
||||||
|
|
||||||
|
public class PluginConfigurationFilter implements PluginClassFilter {
|
||||||
|
public static final String GROUP_NAME = "plugin_config";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String groupName() {
|
||||||
|
return GROUP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Class<?> clazz) {
|
||||||
|
if (BasePlugin.class.isAssignableFrom(clazz)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return clazz.isAnnotationPresent(PluginConfiguration.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package cd.casic.plugin.core.register.filter.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.PluginClassFilter;
|
||||||
|
import jakarta.websocket.server.ServerEndpoint;
|
||||||
|
|
||||||
|
public class WebSocketFilter implements PluginClassFilter {
|
||||||
|
|
||||||
|
public static final String GROUP_NAME = "websocket";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String groupName() {
|
||||||
|
return GROUP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Class<?> clazz) {
|
||||||
|
return clazz.isAnnotationPresent(ServerEndpoint.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
package cd.casic.plugin.core.register.mybatis;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactory;
|
||||||
|
import org.mybatis.spring.SqlSessionTemplate;
|
||||||
|
import org.mybatis.spring.mapper.MapperFactoryBean;
|
||||||
|
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||||
|
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||||
|
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||||
|
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||||
|
import org.springframework.context.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 19:49
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class MapperHandler {
|
||||||
|
|
||||||
|
private static final String MAPPER_INTERFACE_NAMES = "MybatisMapperInterfaceNames";
|
||||||
|
|
||||||
|
private final ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理插件中的Mapper
|
||||||
|
*
|
||||||
|
* @param pluginInfo 插件信息
|
||||||
|
* @param processMapper Mapper的具体处理者
|
||||||
|
*/
|
||||||
|
public void processMapper(PluginInfo pluginInfo,
|
||||||
|
MapperHandler.ProcessMapper processMapper) {
|
||||||
|
AnnotationConfigApplicationContext applicationContext = pluginInfo.getPluginApplicationContext();
|
||||||
|
// TODO 这里可以把类进行分组,就不用每次都扫mapper
|
||||||
|
List<Class<?>> mapperClassList = new ArrayList<>();
|
||||||
|
for (Class<?> aClass : pluginInfo.getClassList()) {
|
||||||
|
Mapper annotation = aClass.getAnnotation(Mapper.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
mapperClassList.add(aClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String pluginId = pluginInfo.getPluginId();
|
||||||
|
for (Class<?> mapperClass : mapperClassList) {
|
||||||
|
if (mapperClass == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// BeanNameGenerator beanNameGenerator = new PluginAnnotationBeanNameGenerator(pluginId);
|
||||||
|
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(mapperClass);
|
||||||
|
ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(abd);
|
||||||
|
abd.setScope(scopeMetadata.getScopeName());
|
||||||
|
// String beanName = beanNameGenerator.generateBeanName(abd, applicationContext);
|
||||||
|
String beanName = abd.getBeanClassName();
|
||||||
|
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
|
||||||
|
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
|
||||||
|
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, applicationContext);
|
||||||
|
try {
|
||||||
|
processMapper.process(definitionHolder, mapperClass);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("process mapper '{}' error. {}", mapperClass.getName(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公共注册生成代理Mapper接口
|
||||||
|
*
|
||||||
|
* @param holder ignore
|
||||||
|
* @param mapperClass ignore
|
||||||
|
* @param sqlSessionFactory ignore
|
||||||
|
* @param sqlSessionTemplate ignore
|
||||||
|
*/
|
||||||
|
public void commonProcessMapper(BeanDefinitionHolder holder,
|
||||||
|
Class<?> mapperClass,
|
||||||
|
SqlSessionFactory sqlSessionFactory,
|
||||||
|
SqlSessionTemplate sqlSessionTemplate) {
|
||||||
|
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
|
||||||
|
definition.getConstructorArgumentValues().addGenericArgumentValue(mapperClass);
|
||||||
|
definition.setBeanClass(MapperFactoryBean.class);
|
||||||
|
definition.getPropertyValues().add("addToConfig", true);
|
||||||
|
definition.getPropertyValues().add("sqlSessionFactory", sqlSessionFactory);
|
||||||
|
definition.getPropertyValues().add("sqlSessionTemplate", sqlSessionTemplate);
|
||||||
|
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ProcessMapper {
|
||||||
|
void process(BeanDefinitionHolder holder, Class<?> mapperClass) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package cd.casic.plugin.core.register.mybatis;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 19:44
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public interface MybatisCommonConfig {
|
||||||
|
/**
|
||||||
|
* 数据库表对应的实体类的包名集合。可配置多个
|
||||||
|
* @return Set
|
||||||
|
*/
|
||||||
|
Set<String> entityPackage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mybatis xml mapper 匹配规则 <br>
|
||||||
|
* ? 匹配一个字符 <br>
|
||||||
|
* * 匹配零个或多个字符 <br>
|
||||||
|
* ** 匹配路径中的零或多个目录 <br>
|
||||||
|
* 例如: <br>
|
||||||
|
* 文件路径配置为 <p>file:D://xml/*PluginMapper.xml<p> <br>
|
||||||
|
* resources路径配置为 <p>classpath:xml/mapper/*PluginMapper.xml<p> <br>
|
||||||
|
* 包路径配置为 <p>package:com.plugin.xml.mapper.*PluginMapper.xml<p> <br>
|
||||||
|
* @return Set
|
||||||
|
*/
|
||||||
|
Set<String> xmlLocationsMatch();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件是否自主启用配置. 默认进行禁用, 使用主程序的配置
|
||||||
|
* @return 返回true, 表示进行插件自主进行Mybatis相关配置
|
||||||
|
*/
|
||||||
|
default boolean enableOneselfConfig(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package cd.casic.plugin.core.register.mybatis;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||||
|
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 19:55
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public interface MybatisPlusConfig extends MybatisCommonConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件自主配置Mybatis-Plus的MybatisSqlSessionFactoryBean
|
||||||
|
* MybatisSqlSessionFactoryBean 具体配置说明参考 Mybatis-plus 官网
|
||||||
|
*
|
||||||
|
* @param sqlSessionFactoryBean MybatisSqlSessionFactoryBean
|
||||||
|
*/
|
||||||
|
default void oneselfConfig(MybatisSqlSessionFactoryBean sqlSessionFactoryBean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写设置配置
|
||||||
|
* 只有 enableOneselfConfig 返回 false, 实现该方法才生效
|
||||||
|
*
|
||||||
|
* @param configuration 当前 MybatisConfiguration
|
||||||
|
* @param globalConfig 当前全局配置GlobalConfig
|
||||||
|
*/
|
||||||
|
default void reSetMainConfig(MybatisConfiguration configuration, GlobalConfig globalConfig) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
package cd.casic.plugin.core.register.mybatis;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||||
|
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.io.Resources;
|
||||||
|
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||||
|
import org.apache.ibatis.plugin.Interceptor;
|
||||||
|
import org.apache.ibatis.scripting.LanguageDriver;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactory;
|
||||||
|
import org.mybatis.spring.SqlSessionTemplate;
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 19:59
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MybatisPlusProcessor implements IPluginProcessor {
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
MybatisPlusConfig config = getObjectByInterfaceClass(plugin.getPluginConfigObjects(), MybatisPlusConfig.class);
|
||||||
|
if (config == null) return;
|
||||||
|
final MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
|
||||||
|
|
||||||
|
if (config.enableOneselfConfig()) {
|
||||||
|
config.oneselfConfig(factory);
|
||||||
|
} else {
|
||||||
|
PluginFollowCoreConfig followCoreConfig = new PluginFollowCoreConfig(
|
||||||
|
plugin.getMainApplicationContext()
|
||||||
|
);
|
||||||
|
MybatisConfiguration mybatisPlusConfiguration = followCoreConfig.getMybatisPlusConfiguration();
|
||||||
|
factory.setDataSource(followCoreConfig.getDataSource());
|
||||||
|
factory.setConfiguration(mybatisPlusConfiguration);
|
||||||
|
Interceptor[] interceptor = followCoreConfig.getInterceptor();
|
||||||
|
if (interceptor != null && interceptor.length > 0) {
|
||||||
|
factory.setPlugins(interceptor);
|
||||||
|
}
|
||||||
|
DatabaseIdProvider databaseIdProvider = followCoreConfig.getDatabaseIdProvider();
|
||||||
|
if (databaseIdProvider != null) {
|
||||||
|
factory.setDatabaseIdProvider(databaseIdProvider);
|
||||||
|
}
|
||||||
|
LanguageDriver[] languageDriver = followCoreConfig.getLanguageDriver();
|
||||||
|
if (languageDriver != null) {
|
||||||
|
factory.setScriptingLanguageDrivers(languageDriver);
|
||||||
|
}
|
||||||
|
// 配置mybatis-plus私有的配置
|
||||||
|
GlobalConfig globalConfig = mybatisPlusFollowCoreConfig(factory, plugin.getMainApplicationContext());
|
||||||
|
config.reSetMainConfig(mybatisPlusConfiguration, globalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginResourceFinder pluginResourceFinder = new PluginResourceFinder(plugin);
|
||||||
|
|
||||||
|
Class<?>[] aliasesClasses = pluginResourceFinder.getAliasesClasses(config.entityPackage());
|
||||||
|
if (aliasesClasses != null && aliasesClasses.length > 0) {
|
||||||
|
factory.setTypeAliases(aliasesClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource[] xmlResource = pluginResourceFinder.getXmlResource(config.xmlLocationsMatch());
|
||||||
|
if (xmlResource != null && xmlResource.length > 0) {
|
||||||
|
factory.setMapperLocations(xmlResource);
|
||||||
|
}
|
||||||
|
ClassLoader defaultClassLoader = Resources.getDefaultClassLoader();
|
||||||
|
try {
|
||||||
|
Resources.setDefaultClassLoader(plugin.getPluginWrapper().getPluginClassLoader());
|
||||||
|
SqlSessionFactory sqlSessionFactory = factory.getObject();
|
||||||
|
if (sqlSessionFactory == null) {
|
||||||
|
throw new Exception("Get mybatis-plus sqlSessionFactory is null");
|
||||||
|
}
|
||||||
|
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
|
||||||
|
MapperHandler mapperHandler = new MapperHandler();
|
||||||
|
mapperHandler.processMapper(plugin, (holder, mapperClass) ->
|
||||||
|
mapperHandler.commonProcessMapper(holder, mapperClass, sqlSessionFactory, sqlSessionTemplate));
|
||||||
|
DefaultListableBeanFactory beanFactory = plugin.getPluginApplicationContext().getDefaultListableBeanFactory();
|
||||||
|
beanFactory.registerSingleton("sqlSessionFactory", sqlSessionFactory);
|
||||||
|
beanFactory.registerSingleton("sqlSession", sqlSessionTemplate);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
Resources.setDefaultClassLoader(defaultClassLoader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
// 不做操作,直接通过关闭PluginApplicationContext完成注销。
|
||||||
|
}
|
||||||
|
|
||||||
|
private GlobalConfig mybatisPlusFollowCoreConfig(MybatisSqlSessionFactoryBean factory,
|
||||||
|
ApplicationContext mainApplicationContext) {
|
||||||
|
MybatisPlusProperties plusProperties = mainApplicationContext.getBean(MybatisPlusProperties.class);
|
||||||
|
|
||||||
|
GlobalConfig currentGlobalConfig = new GlobalConfig();
|
||||||
|
currentGlobalConfig.setBanner(false);
|
||||||
|
GlobalConfig globalConfig = plusProperties.getGlobalConfig();
|
||||||
|
if (globalConfig != null) {
|
||||||
|
currentGlobalConfig.setDbConfig(globalConfig.getDbConfig());
|
||||||
|
currentGlobalConfig.setIdentifierGenerator(globalConfig.getIdentifierGenerator());
|
||||||
|
currentGlobalConfig.setMetaObjectHandler(globalConfig.getMetaObjectHandler());
|
||||||
|
currentGlobalConfig.setSqlInjector(globalConfig.getSqlInjector());
|
||||||
|
}
|
||||||
|
factory.setGlobalConfig(currentGlobalConfig);
|
||||||
|
return currentGlobalConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO 临时放这里,先跑通mybatis-plus集成测试
|
||||||
|
public static <T> T getObjectByInterfaceClass(Set<Object> sourceObject, Class<T> interfaceClass) {
|
||||||
|
if (sourceObject == null || sourceObject.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Object configSingletonObject : sourceObject) {
|
||||||
|
Set<Class<?>> allInterfacesForClassAsSet = ClassUtils
|
||||||
|
.getAllInterfacesAsSet(configSingletonObject);
|
||||||
|
if (allInterfacesForClassAsSet.contains(interfaceClass)) {
|
||||||
|
return (T) configSingletonObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
package cd.casic.plugin.core.register.mybatis;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||||
|
import org.apache.ibatis.plugin.Interceptor;
|
||||||
|
import org.apache.ibatis.scripting.LanguageDriver;
|
||||||
|
import org.apache.ibatis.scripting.LanguageDriverRegistry;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactory;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 插件跟随主程序时, 获取主程序的Mybatis定义的一些配置
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 19:47
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PluginFollowCoreConfig {
|
||||||
|
|
||||||
|
private final ApplicationContext mainApplicationContext;
|
||||||
|
|
||||||
|
public DataSource getDataSource() {
|
||||||
|
return mainApplicationContext.getBean(DataSource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MybatisConfiguration getMybatisPlusConfiguration() {
|
||||||
|
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||||
|
try {
|
||||||
|
Map<String, ConfigurationCustomizer> customizerMap =
|
||||||
|
mainApplicationContext.getBeansOfType(com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer.class);
|
||||||
|
if (!customizerMap.isEmpty()) {
|
||||||
|
for (com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer customizer : customizerMap.values()) {
|
||||||
|
customizer.customize(configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Interceptor[] getInterceptor() {
|
||||||
|
Map<Class<? extends Interceptor>, Interceptor> interceptorMap = new HashMap<>();
|
||||||
|
try {
|
||||||
|
SqlSessionFactory sqlSessionFactory = mainApplicationContext.getBean(SqlSessionFactory.class);
|
||||||
|
// 先从 SqlSessionFactory 工厂中获取拦截器
|
||||||
|
List<Interceptor> interceptors = sqlSessionFactory.getConfiguration().getInterceptors();
|
||||||
|
if (interceptors != null) {
|
||||||
|
for (Interceptor interceptor : interceptors) {
|
||||||
|
if (interceptor == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
interceptorMap.put(interceptor.getClass(), interceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// 再从定义Bean中获取拦截器
|
||||||
|
Map<String, Interceptor> beanInterceptorMap = mainApplicationContext.getBeansOfType(Interceptor.class);
|
||||||
|
if (!beanInterceptorMap.isEmpty()) {
|
||||||
|
beanInterceptorMap.forEach((k, v) -> {
|
||||||
|
// 如果Class一致, 则会覆盖
|
||||||
|
interceptorMap.put(v.getClass(), v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (interceptorMap.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return interceptorMap.values().toArray(new Interceptor[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseIdProvider getDatabaseIdProvider() {
|
||||||
|
String[] beanNamesForType = mainApplicationContext.getBeanNamesForType(DatabaseIdProvider.class, false, false);
|
||||||
|
if (beanNamesForType.length > 0) {
|
||||||
|
return mainApplicationContext.getBean(DatabaseIdProvider.class);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public LanguageDriver[] getLanguageDriver() {
|
||||||
|
Map<Class<? extends LanguageDriver>, LanguageDriver> languageDriverMap = new HashMap<>();
|
||||||
|
try {
|
||||||
|
SqlSessionFactory sqlSessionFactory = mainApplicationContext.getBean(SqlSessionFactory.class);
|
||||||
|
LanguageDriverRegistry languageRegistry = sqlSessionFactory.getConfiguration()
|
||||||
|
.getLanguageRegistry();
|
||||||
|
// 先从 SqlSessionFactory 工厂中获取LanguageDriver
|
||||||
|
Field proxyTypesField = ReflectionUtils.findField(languageRegistry.getClass(), "LANGUAGE_DRIVER_MAP");
|
||||||
|
Map<Class<? extends LanguageDriver>, LanguageDriver> driverMap = null;
|
||||||
|
if (proxyTypesField != null) {
|
||||||
|
proxyTypesField.setAccessible(true);
|
||||||
|
driverMap = (Map<Class<? extends LanguageDriver>, LanguageDriver>) proxyTypesField.get(languageRegistry);
|
||||||
|
}
|
||||||
|
if (driverMap != null) {
|
||||||
|
languageDriverMap.putAll(driverMap);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
Map<String, LanguageDriver> beansLanguageDriver = mainApplicationContext.getBeansOfType(LanguageDriver.class);
|
||||||
|
if (!beansLanguageDriver.isEmpty()) {
|
||||||
|
beansLanguageDriver.forEach((k, v) -> {
|
||||||
|
// 如果Class一致, 则会覆盖
|
||||||
|
languageDriverMap.put(v.getClass(), v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (languageDriverMap.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return languageDriverMap.values().toArray(new LanguageDriver[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package cd.casic.plugin.core.register.mybatis;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.utils.ResourceUtils;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.core.type.ClassMetadata;
|
||||||
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
|
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 插件资源发现
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 19:45
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PluginResourceFinder {
|
||||||
|
|
||||||
|
private final ClassLoader classLoader;
|
||||||
|
private final ResourcePatternResolver resourcePatternResolver;
|
||||||
|
private final MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
|
||||||
|
|
||||||
|
public PluginResourceFinder(PluginInfo pluginInfo) {
|
||||||
|
this.classLoader = pluginInfo.getPluginWrapper().getPluginClassLoader();
|
||||||
|
this.resourcePatternResolver = new PathMatchingResourcePatternResolver(classLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件中xml资源
|
||||||
|
* @param xmlLocationsMatchSet xml资源匹配集合
|
||||||
|
* @return xml Resource 数组
|
||||||
|
* @throws IOException 获取xml资源异常
|
||||||
|
*/
|
||||||
|
public Resource[] getXmlResource(Set<String> xmlLocationsMatchSet) throws IOException {
|
||||||
|
if(xmlLocationsMatchSet == null || xmlLocationsMatchSet.isEmpty()){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Resource> resources = new ArrayList<>();
|
||||||
|
for (String xmlLocationsMatch : xmlLocationsMatchSet) {
|
||||||
|
if(xmlLocationsMatchSet.isEmpty()){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Resource> loadResources = getXmlResources(xmlLocationsMatch);
|
||||||
|
if(loadResources != null && !loadResources.isEmpty()){
|
||||||
|
resources.addAll(loadResources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(resources.isEmpty()){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resources.toArray(new Resource[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件的实体类及其别名
|
||||||
|
* @param packagePatterns 实体类包名
|
||||||
|
* @return class 数组
|
||||||
|
* @throws IOException 获取医院异常
|
||||||
|
*/
|
||||||
|
public Class<?>[] getAliasesClasses(Set<String> packagePatterns) throws IOException {
|
||||||
|
if(packagePatterns == null || packagePatterns.isEmpty()){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Set<Class<?>> aliasesClasses = new HashSet<>();
|
||||||
|
for (String packagePattern : packagePatterns) {
|
||||||
|
Resource[] resources = resourcePatternResolver.getResources(
|
||||||
|
ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
|
||||||
|
+ ClassUtils.convertClassNameToResourcePath(packagePattern) + "/**/*.class");
|
||||||
|
for (Resource resource : resources) {
|
||||||
|
try {
|
||||||
|
ClassMetadata classMetadata = metadataReaderFactory.getMetadataReader(resource).getClassMetadata();
|
||||||
|
Class<?> clazz = classLoader.loadClass(classMetadata.getClassName());
|
||||||
|
aliasesClasses.add(clazz);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
log.warn("Cannot load the '{}'. Cause by {}", resource, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aliasesClasses.toArray(new Class<?>[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Xml资源
|
||||||
|
* @param mybatisMapperXmlLocationMatch mybatis xml 批量规则
|
||||||
|
* @return 匹配到的xml资源
|
||||||
|
* @throws IOException IO 异常
|
||||||
|
*/
|
||||||
|
private List<Resource> getXmlResources(String mybatisMapperXmlLocationMatch) throws IOException {
|
||||||
|
String matchLocation = ResourceUtils.getMatchLocation(mybatisMapperXmlLocationMatch);
|
||||||
|
if(matchLocation == null){
|
||||||
|
log.error("mybatisMapperXmlLocation {} illegal", mybatisMapperXmlLocationMatch);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Resource[] resources = resourcePatternResolver.getResources(matchLocation);
|
||||||
|
if(resources.length > 0){
|
||||||
|
return Arrays.asList(resources);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("mybatis xml resource '{}' match error : {}", mybatisMapperXmlLocationMatch,
|
||||||
|
e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package cd.casic.plugin.core.register.websocket;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:03
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public interface BaseServerEndpoint extends DisposableBean {
|
||||||
|
}
|
@ -0,0 +1,214 @@
|
|||||||
|
package cd.casic.plugin.core.register.websocket;
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.register.filter.impl.WebSocketFilter;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.function.IPluginProcessor;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import jakarta.servlet.ServletContext;
|
||||||
|
import jakarta.websocket.DeploymentException;
|
||||||
|
import jakarta.websocket.EndpointConfig;
|
||||||
|
import jakarta.websocket.Session;
|
||||||
|
import jakarta.websocket.server.ServerContainer;
|
||||||
|
import jakarta.websocket.server.ServerEndpoint;
|
||||||
|
import jakarta.websocket.server.ServerEndpointConfig;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.util.StringUtils;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/22 20:04
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSocketProcessor implements IPluginProcessor {
|
||||||
|
|
||||||
|
private final ApplicationContext mainApplicationContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registry(PluginInfo plugin) throws Exception {
|
||||||
|
ServerContainer serverContainer = getServerContainer();
|
||||||
|
if (serverContainer == null) return;
|
||||||
|
|
||||||
|
List<Class<?>> webSocketClassList = plugin.getGroupClass(WebSocketFilter.GROUP_NAME);
|
||||||
|
if (webSocketClassList.isEmpty()) return;
|
||||||
|
String pluginId = plugin.getPluginId();
|
||||||
|
webSocketClassList.forEach(websocketClass -> {
|
||||||
|
ServerEndpoint serverEndpoint = websocketClass.getDeclaredAnnotation(ServerEndpoint.class);
|
||||||
|
if (serverEndpoint == null) {
|
||||||
|
log.warn("WebSocket class {} doesn't has annotation {}", websocketClass.getName(), ServerEndpoint.class.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String sourcePath = serverEndpoint.value();
|
||||||
|
if (StringUtils.isNullOrEmpty(sourcePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String processPath = sourcePath;
|
||||||
|
if (!processPath.startsWith("/")) {
|
||||||
|
processPath = "/".concat(processPath);
|
||||||
|
}
|
||||||
|
UriTemplate uriTemplate;
|
||||||
|
try {
|
||||||
|
uriTemplate = new UriTemplate(processPath);
|
||||||
|
} catch (DeploymentException e) {
|
||||||
|
log.error("Websocket path validate failed.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String newWebsocketPath = "/".concat(pluginId).concat(processPath);
|
||||||
|
String newWebsocketTemplatePath = "/".concat(pluginId).concat(uriTemplate.getPath());
|
||||||
|
Map<String, Object> annotationsUpdater;
|
||||||
|
try {
|
||||||
|
InvocationHandler invocationHandler = Proxy.getInvocationHandler(serverEndpoint);
|
||||||
|
Field field = invocationHandler.getClass().getDeclaredField("memberValues");
|
||||||
|
field.setAccessible(true);
|
||||||
|
annotationsUpdater = (Map<String, Object>) field.get(invocationHandler);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Process and update websocket path '{}' annotation exception.", sourcePath, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
annotationsUpdater.put("value", newWebsocketPath);
|
||||||
|
serverContainer.addEndpoint(websocketClass);
|
||||||
|
plugin.getWebSocketPathMap().put(newWebsocketPath, newWebsocketTemplatePath);
|
||||||
|
log.info("Succeed to create websocket service for path {}", newWebsocketPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Create websocket service for websocket class " + websocketClass.getName() + " failed.", e);
|
||||||
|
} finally {
|
||||||
|
annotationsUpdater.put("value", sourcePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unRegistry(PluginInfo plugin) throws Exception {
|
||||||
|
// 不做操作,直接通过关闭PluginApplicationContext完成注销。
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 得到 Tomcat ServerContainer
|
||||||
|
*
|
||||||
|
* @return ServerContainer
|
||||||
|
*/
|
||||||
|
private ServerContainer getServerContainer() {
|
||||||
|
try {
|
||||||
|
mainApplicationContext.getBean(ServerEndpointExporter.class);
|
||||||
|
} catch (BeansException e) {
|
||||||
|
log.debug("The required bean of {} not found, if you want to use plugin websocket, please create it.", ServerEndpointExporter.class.getName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(mainApplicationContext instanceof WebApplicationContext)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
WebApplicationContext webApplicationContext = (WebApplicationContext) mainApplicationContext;
|
||||||
|
ServletContext servletContext = webApplicationContext.getServletContext();
|
||||||
|
if (servletContext == null) {
|
||||||
|
log.warn("Servlet context is null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object obj = servletContext.getAttribute("javax.websocket.server.ServerContainer");
|
||||||
|
if (!(obj instanceof ServerContainer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (ServerContainer) obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭session
|
||||||
|
*
|
||||||
|
* @param session session
|
||||||
|
* @param websocketPath websocketPath 路径
|
||||||
|
* @return 如果需要关闭并且关闭成功, 则返回true。 否则返回false
|
||||||
|
* @throws Exception 关闭异常
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
private boolean closeSession(Session session, String websocketPath) throws Exception {
|
||||||
|
EndpointConfig endpointConfig = (EndpointConfig) session.getClass().getDeclaredField("endpointConfig").get(session);
|
||||||
|
ServerEndpointConfig perEndpointConfig = (ServerEndpointConfig) endpointConfig.getClass().getDeclaredField("perEndpointConfig").get(endpointConfig);
|
||||||
|
String path = perEndpointConfig.getPath();
|
||||||
|
if (path.equals(websocketPath)) {
|
||||||
|
session.close();
|
||||||
|
log.info("Closed websocket session {} for path {}", session.getId(), websocketPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket路径解析类,主要用于处理参数
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private static class UriTemplate {
|
||||||
|
|
||||||
|
private final Map<String, Integer> paramMap = new ConcurrentHashMap<>();
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
private UriTemplate(String path) throws DeploymentException {
|
||||||
|
if (StrUtil.isEmpty(path) || !path.startsWith("/") || path.contains("/../") || path.contains("/./") || path.contains("//")) {
|
||||||
|
throw new DeploymentException(String.format("The path [%s] is not valid.", path));
|
||||||
|
}
|
||||||
|
StringBuilder normalized = new StringBuilder(path.length());
|
||||||
|
Set<String> paramNames = new HashSet<>();
|
||||||
|
|
||||||
|
// Include empty segments.
|
||||||
|
String[] segments = path.split("/", -1);
|
||||||
|
int paramCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
String segment = segments[i];
|
||||||
|
if (segment.isEmpty()) {
|
||||||
|
if (i == 0 || (i == segments.length - 1 && paramCount == 0)) {
|
||||||
|
// Ignore the first empty segment as the path must always
|
||||||
|
// start with '/'
|
||||||
|
// Ending with a '/' is also OK for instances used for
|
||||||
|
// matches but not for parameterised templates.
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// As per EG discussion, all other empty segments are
|
||||||
|
// invalid
|
||||||
|
throw new DeploymentException(String.format("The path [%s] contains one or more empty segments which is not permitted", path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized.append('/');
|
||||||
|
if (segment.startsWith("{") && segment.endsWith("}")) {
|
||||||
|
segment = segment.substring(1, segment.length() - 1);
|
||||||
|
normalized.append('{');
|
||||||
|
normalized.append(paramCount++);
|
||||||
|
normalized.append('}');
|
||||||
|
if (!paramNames.add(segment)) {
|
||||||
|
throw new DeploymentException(String.format("The parameter [%s] appears more than once in the path which is not permitted", segment));
|
||||||
|
}
|
||||||
|
paramMap.put(segment, paramCount - 1);
|
||||||
|
} else {
|
||||||
|
if (segment.contains("{") || segment.contains("}")) {
|
||||||
|
throw new DeploymentException(String.format("The segment [%s] is not valid in the provided path [%s]", segment, path));
|
||||||
|
}
|
||||||
|
normalized.append(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.path = normalized.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package cd.casic.plugin.dataobject.dao;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginFacade;
|
||||||
|
import cd.casic.plugin.dataobject.pojo.PluginInfo;
|
||||||
|
import cd.casic.plugin.dataobject.dto.PluginSpecStorage;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 插件信息门面类 , 暂时的办法
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/20 16:00
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PluginFacadeMemoryCache {
|
||||||
|
private static final Map<String, PluginFacade> pluginFacadeMap = new ConcurrentHashMap<>(8);
|
||||||
|
|
||||||
|
public static void putPluginSpec(String pluginId, PluginSpecStorage pluginDescriptorStorage) {
|
||||||
|
pluginFacadeMap.computeIfAbsent(pluginId, k -> new PluginFacade())
|
||||||
|
.setPluginSpecStorage(pluginDescriptorStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PluginSpecStorage getPluginSepc(String pluginId) {
|
||||||
|
return Optional.ofNullable(pluginFacadeMap.get(pluginId))
|
||||||
|
.map(PluginFacade::getPluginSpecStorage)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putPluginInfo(String pluginId, PluginInfo pluginInfo) {
|
||||||
|
pluginFacadeMap.computeIfAbsent(pluginId, k -> new PluginFacade())
|
||||||
|
.setPluginInfo(pluginInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PluginInfo getPluginInfo(String pluginId) {
|
||||||
|
return Optional.ofNullable(pluginFacadeMap.get(pluginId))
|
||||||
|
.map(PluginFacade::getPluginInfo)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putPluginClassloader(String pluginId, ClassLoader classLoader) {
|
||||||
|
pluginFacadeMap.computeIfAbsent(pluginId, k -> new PluginFacade())
|
||||||
|
.setClassLoader(classLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClassLoader getPluginClassLoader(String pluginId) {
|
||||||
|
return Optional.ofNullable(pluginFacadeMap.get(pluginId))
|
||||||
|
.map(PluginFacade::getClassLoader)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putPluginApplicationContext(String pluginId, AnnotationConfigApplicationContext applicationContext) {
|
||||||
|
pluginFacadeMap.computeIfAbsent(pluginId, k -> new PluginFacade())
|
||||||
|
.setApplicationContext(applicationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnnotationConfigApplicationContext getPluginApplicationContext(String pluginId) {
|
||||||
|
return Optional.ofNullable(pluginFacadeMap.get(pluginId))
|
||||||
|
.map(PluginFacade::getApplicationContext)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removePlugin(String pluginId) {
|
||||||
|
pluginFacadeMap.remove(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, PluginFacade> getAllPlugin() {
|
||||||
|
return Collections.unmodifiableMap(pluginFacadeMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package cd.casic.plugin.dataobject.dao;
|
||||||
|
|
||||||
|
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import cd.casic.plugin.dataobject.dto.PluginInformation;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/13 15:47
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface PluginInformationDao extends BaseMapperX<PluginInformation> {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package cd.casic.plugin.dataobject.dto;
|
||||||
|
|
||||||
|
import cd.casic.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/19 10:12
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName(value = "plugin_information")
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PluginInformation extends BaseDO {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
//插件名
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
//插件路径
|
||||||
|
private String path;
|
||||||
|
|
||||||
|
//插件描述
|
||||||
|
private String desc;
|
||||||
|
|
||||||
|
//插件版本
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
//插件作者
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
//插件状态
|
||||||
|
private Integer status = 0;
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package cd.casic.plugin.dataobject.dto;
|
||||||
|
|
||||||
|
import cd.casic.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import lombok.*;
|
||||||
|
import org.pf4j.Plugin;
|
||||||
|
import org.pf4j.PluginDependency;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/19 17:13
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@TableName(value = "plugin_spec_storage")
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PluginSpecStorage extends BaseDO {
|
||||||
|
@TableId
|
||||||
|
private String pluginId;
|
||||||
|
private String pluginDescription;
|
||||||
|
private String pluginClass = Plugin.class.getName();
|
||||||
|
private String version;
|
||||||
|
private String requires = "*";
|
||||||
|
private String provider;
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private List<PluginDependency> dependencies;
|
||||||
|
private String license;
|
||||||
|
private String mapperXmlDir;
|
||||||
|
private String staticDir;
|
||||||
|
private StatusPhase enable;
|
||||||
|
private String path;
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String configFileName;
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<String> configFileActive;
|
||||||
|
private String pluginDirPath;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum EnableStatus {
|
||||||
|
/**
|
||||||
|
* 启用
|
||||||
|
*/
|
||||||
|
ENABLE(1, "启用"),
|
||||||
|
/**
|
||||||
|
* 禁用
|
||||||
|
*/
|
||||||
|
DISABLE(0, "禁用");
|
||||||
|
private final Integer code;
|
||||||
|
private final String value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StatusPhase {
|
||||||
|
PENDING,
|
||||||
|
STARTING,
|
||||||
|
CREATED,
|
||||||
|
DISABLING,
|
||||||
|
DISABLED,
|
||||||
|
RESOLVED,
|
||||||
|
STARTED,
|
||||||
|
STOPPED,
|
||||||
|
FAILED,
|
||||||
|
UNKNOWN,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package cd.casic.plugin.dataobject.pojo;
|
||||||
|
|
||||||
|
import cd.casic.plugin.dataobject.dto.PluginSpecStorage;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/20 15:55
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PluginFacade {
|
||||||
|
private PluginInfo pluginInfo;
|
||||||
|
private ClassLoader classLoader;
|
||||||
|
private PluginSpecStorage pluginSpecStorage;
|
||||||
|
private AnnotationConfigApplicationContext applicationContext;
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
package cd.casic.plugin.dataobject.pojo;
|
||||||
|
|
||||||
|
import cd.casic.plugin.core.BasePlugin;
|
||||||
|
import cd.casic.plugin.dataobject.dao.PluginFacadeMemoryCache;
|
||||||
|
import cd.casic.plugin.utils.PluginsUtils;
|
||||||
|
import cn.hutool.setting.dialect.Props;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 重构了内容
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/20 15:12
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Slf4j
|
||||||
|
public class PluginInfo {
|
||||||
|
|
||||||
|
// jar或zip的list集合
|
||||||
|
private List<Class<?>> classList;
|
||||||
|
private ApplicationContext mainApplicationContext;
|
||||||
|
private Boolean applicationContextIsRefresh = false;
|
||||||
|
private AnnotationConfigApplicationContext pluginApplicationContext;
|
||||||
|
private PluginWrapper pluginWrapper;
|
||||||
|
private List<Class<?>> adminGroupsClassList = new ArrayList<>();
|
||||||
|
private List<String> websocketPaths = new ArrayList<>();
|
||||||
|
private String pluginId;
|
||||||
|
private String mapperXmlDir;
|
||||||
|
private final BasePlugin basePlugin;
|
||||||
|
private List<HandlerInterceptor> handlerInterceptorList = new ArrayList<>();
|
||||||
|
private Set<String> staticClassPathLocations = new HashSet<>();
|
||||||
|
private Set<String> staticFileLocations = new HashSet<>();
|
||||||
|
private List<Class<?>> controllers = new ArrayList<>();
|
||||||
|
private Set<Object> pluginConfigObjects = new HashSet<>();
|
||||||
|
private Map<String, String> webSocketPathMap = new ConcurrentHashMap<>();
|
||||||
|
// private ConcurrentHashMap<Class<?>, Object> beanCache = new ConcurrentHashMap<>();
|
||||||
|
// TODO 这个map用于替代前面的ClassList
|
||||||
|
private Map<String, List<Class<?>>> classGroups = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public PluginInfo(PluginWrapper pluginWrapper, ApplicationContext applicationContext) throws Exception {
|
||||||
|
this.classList = new ArrayList<>();
|
||||||
|
this.pluginWrapper = pluginWrapper;
|
||||||
|
this.pluginId = pluginWrapper.getPluginId();
|
||||||
|
this.pluginApplicationContext = getContext();
|
||||||
|
this.mainApplicationContext = applicationContext;
|
||||||
|
this.basePlugin = (BasePlugin) pluginWrapper.getPlugin();
|
||||||
|
this.pluginApplicationContext.setParent(mainApplicationContext);
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从配置文件加载插件设置
|
||||||
|
*/
|
||||||
|
private void loadSettings() throws Exception {
|
||||||
|
Props setting = PluginsUtils.getSetting(pluginWrapper.getPluginId());
|
||||||
|
if (!setting.isEmpty()) {
|
||||||
|
this.mapperXmlDir = setting.getStr("mybatis.mapper.location", null);
|
||||||
|
String locations = setting.getStr("static.locations", null);
|
||||||
|
if (StringUtils.isNotBlank(locations)) {
|
||||||
|
loadResources(locations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnnotationConfigApplicationContext getContext() {
|
||||||
|
AnnotationConfigApplicationContext pluginApplicationContext = PluginFacadeMemoryCache.getPluginApplicationContext(pluginWrapper.getPluginId());
|
||||||
|
if (pluginApplicationContext == null) {
|
||||||
|
pluginApplicationContext = new AnnotationConfigApplicationContext();
|
||||||
|
pluginApplicationContext.setClassLoader(pluginWrapper.getPluginClassLoader());
|
||||||
|
PluginFacadeMemoryCache.putPluginApplicationContext(pluginWrapper.getPluginId(), pluginApplicationContext);
|
||||||
|
}
|
||||||
|
return PluginFacadeMemoryCache.getPluginApplicationContext(pluginWrapper.getPluginId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadResources(String locations) {
|
||||||
|
String[] staticLocations = locations.split(",");
|
||||||
|
for (String staticLocation : staticLocations) {
|
||||||
|
String processedLocation = processResourceLocation(staticLocation);
|
||||||
|
if (staticLocation.contains("classpath:")) {
|
||||||
|
this.staticClassPathLocations.add(processedLocation);
|
||||||
|
} else {
|
||||||
|
this.staticFileLocations.add(processedLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理资源路径,去除 "classpath:" 前缀和开头的斜杠
|
||||||
|
*
|
||||||
|
* @param location 原始资源路径
|
||||||
|
* @return 处理后的资源路径
|
||||||
|
*/
|
||||||
|
private String processResourceLocation(String location) {
|
||||||
|
if (location.contains("classpath:")) {
|
||||||
|
location = location.replace("classpath:", "");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(location) && location.startsWith("/")) {
|
||||||
|
location = location.substring(1);
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMapperXmlDir() {
|
||||||
|
return processResourceLocation(mapperXmlDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理ApplicationContext
|
||||||
|
*/
|
||||||
|
public void clearApplicationContext() {
|
||||||
|
PluginFacadeMemoryCache.removePlugin(this.getPluginId().trim());
|
||||||
|
if (pluginApplicationContext != null) {
|
||||||
|
pluginApplicationContext.getDefaultListableBeanFactory().destroySingletons();
|
||||||
|
pluginApplicationContext.close();
|
||||||
|
}
|
||||||
|
this.applicationContextIsRefresh = false;
|
||||||
|
this.pluginApplicationContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param c class
|
||||||
|
* @return java.util.List<java.lang.Class < ?>>
|
||||||
|
* @description 获取插件内实现指定类的bean
|
||||||
|
* @author dolphin
|
||||||
|
*/
|
||||||
|
public <T> T getPluginBean(Class<T> c) {
|
||||||
|
if (pluginApplicationContext != null && pluginApplicationContext.containsBeanDefinition(c.getName())) {
|
||||||
|
try {
|
||||||
|
return pluginApplicationContext.getBean(c);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录日志,可根据实际情况添加日志框架
|
||||||
|
log.error("获取插件 bean 失败,类型: " + c.getName() + ", 错误信息: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addController(Class<?> controller) {
|
||||||
|
this.controllers.add(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPluginConfigObject(Object config) {
|
||||||
|
this.pluginConfigObjects.add(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addGroupClass(String groupName, Class<?> clazz) {
|
||||||
|
classGroups.computeIfAbsent(groupName, k -> new ArrayList<>()).add(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Class<?>> getGroupClass(String groupName) {
|
||||||
|
return classGroups.getOrDefault(groupName, Collections.emptyList());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package cd.casic.plugin.dataobject.pojo;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.plugin.PluginApplicationContextFactory;
|
||||||
|
import cd.casic.plugin.core.PluginContext;
|
||||||
|
import cd.casic.plugin.event.SpringPluginStartedEvent;
|
||||||
|
import cd.casic.plugin.event.SpringPluginStartingEvent;
|
||||||
|
import cd.casic.plugin.event.SpringPluginStoppingEvent;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.pf4j.Plugin;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Classname SpringPlugin
|
||||||
|
* @Description TODO
|
||||||
|
* @Date 2025/5/8 14:48
|
||||||
|
* @Created by mianbin
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SpringPlugin extends Plugin {
|
||||||
|
|
||||||
|
private ApplicationContext context;
|
||||||
|
|
||||||
|
private Plugin delegate;
|
||||||
|
|
||||||
|
private final PluginApplicationContextFactory contextFactory;
|
||||||
|
|
||||||
|
private final PluginContext pluginContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
log.info("Preparing starting plugin {}", pluginContext.getName());
|
||||||
|
var pluginId = pluginContext.getName();
|
||||||
|
try {
|
||||||
|
// initialize context
|
||||||
|
this.context = contextFactory.create(pluginId);
|
||||||
|
log.info("Application context {} for plugin {} is created", this.context, pluginId);
|
||||||
|
Optional<Plugin> pluginOpt = context.getBeanProvider(Plugin.class)
|
||||||
|
.stream()
|
||||||
|
.findFirst();
|
||||||
|
log.info("Before publishing plugin starting event for plugin {}", pluginId);
|
||||||
|
context.publishEvent(new SpringPluginStartingEvent(this, this));
|
||||||
|
log.info("After publishing plugin starting event for plugin {}", pluginId);
|
||||||
|
pluginOpt.ifPresent(t -> {
|
||||||
|
this.delegate = t;
|
||||||
|
log.info("Starting {} for plugin {}", this.delegate, pluginId);
|
||||||
|
this.delegate.start();
|
||||||
|
log.info("Started {} for plugin {}", this.delegate, pluginId);
|
||||||
|
});
|
||||||
|
log.info("Before publishing plugin started event for plugin {}", pluginId);
|
||||||
|
context.publishEvent(new SpringPluginStartedEvent(this, this));
|
||||||
|
log.info("After publishing plugin started event for plugin {}", pluginId);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// try to stop plugin for cleaning resources if something went wrong
|
||||||
|
log.error(
|
||||||
|
"Cleaning up plugin resources for plugin {} due to not being able to start plugin.",
|
||||||
|
pluginId);
|
||||||
|
this.stop();
|
||||||
|
// propagate exception to invoker.
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
try {
|
||||||
|
Optional.ofNullable(context).ifPresent(v -> {
|
||||||
|
log.info("Before publishing plugin stopping event for plugin {}",
|
||||||
|
pluginContext.getName());
|
||||||
|
v.publishEvent(new SpringPluginStoppingEvent(this, this));
|
||||||
|
log.info("After publishing plugin stopping event for plugin {}",
|
||||||
|
pluginContext.getName());
|
||||||
|
});
|
||||||
|
Optional.ofNullable(this.delegate).ifPresent(d -> {
|
||||||
|
log.info("Stopping {} for plugin {}", this.delegate, pluginContext.getName());
|
||||||
|
d.stop();
|
||||||
|
log.info("Stopped {} for plugin {}", this.delegate, pluginContext.getName());
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (context instanceof ConfigurableApplicationContext configurableContext) {
|
||||||
|
log.info("Closing plugin context for plugin {}", pluginContext.getName());
|
||||||
|
configurableContext.close();
|
||||||
|
log.info("Closed plugin context for plugin {}", pluginContext.getName());
|
||||||
|
}
|
||||||
|
log.info("Reset plugin context for plugin {}", pluginContext.getName());
|
||||||
|
context = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete() {
|
||||||
|
if (delegate != null) {
|
||||||
|
delegate.delete();
|
||||||
|
}
|
||||||
|
this.delegate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationContext getApplicationContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,12 @@ import org.springframework.util.Assert;
|
|||||||
* @Created by mianbin
|
* @Created by mianbin
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class PluginBeforeStopEvent extends ApplicationEvent {
|
public class OpsPluginBeforeStopEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
|
||||||
private final PluginWrapper plugin;
|
private final PluginWrapper plugin;
|
||||||
|
|
||||||
public PluginBeforeStopEvent(Object source, PluginWrapper plugin) {
|
public OpsPluginBeforeStopEvent(Object source, PluginWrapper plugin) {
|
||||||
super(source);
|
super(source);
|
||||||
Assert.notNull(plugin, "插件不能为空");
|
Assert.notNull(plugin, "插件不能为空");
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
@ -0,0 +1,22 @@
|
|||||||
|
package cd.casic.plugin.event;
|
||||||
|
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Classname PluginStartedEvent
|
||||||
|
* @Description 插件启动,发布事件到上下文
|
||||||
|
* @Date 2025/5/8 14:43
|
||||||
|
* @Created by mianbin
|
||||||
|
*/
|
||||||
|
public class OpsPluginStartedEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private final PluginWrapper plugin;
|
||||||
|
|
||||||
|
public OpsPluginStartedEvent(Object source, PluginWrapper plugin) {
|
||||||
|
super(source);
|
||||||
|
Assert.notNull(plugin, "插件不能为空");
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
}
|
@ -11,11 +11,11 @@ import org.springframework.context.ApplicationEvent;
|
|||||||
* @Created by mianbin
|
* @Created by mianbin
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class PluginStoppedEvent extends ApplicationEvent {
|
public class OpsPluginStoppedEvent extends ApplicationEvent {
|
||||||
|
|
||||||
private final PluginWrapper plugin;
|
private final PluginWrapper plugin;
|
||||||
|
|
||||||
public PluginStoppedEvent(Object source, PluginWrapper plugin) {
|
public OpsPluginStoppedEvent(Object source, PluginWrapper plugin) {
|
||||||
super(source);
|
super(source);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
@ -1,22 +1,15 @@
|
|||||||
package cd.casic.plugin.event;
|
package cd.casic.plugin.event;
|
||||||
|
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Classname PluginStartedEvent
|
* @description: 插件真正启动时发布的事件,仅供插件内部使用
|
||||||
* @Description 插件启动,发布事件到上下文
|
* @author: mianbin
|
||||||
* @Date 2025/5/8 14:43
|
* @date: 2025/5/12 16:31
|
||||||
* @Created by mianbin
|
* @version: 1.0
|
||||||
*/
|
*/
|
||||||
public class PluginStartedEvent extends ApplicationEvent {
|
public class PluginStartedEvent extends ApplicationEvent {
|
||||||
|
public PluginStartedEvent(Object source) {
|
||||||
private final PluginWrapper plugin;
|
|
||||||
|
|
||||||
public PluginStartedEvent(Object source, PluginWrapper plugin) {
|
|
||||||
super(source);
|
super(source);
|
||||||
Assert.notNull(plugin, "插件不能为空");
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package cd.casic.plugin.event;
|
package cd.casic.plugin.event;
|
||||||
|
|
||||||
import cd.casic.plugin.SpringPlugin;
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package cd.casic.plugin.event;
|
package cd.casic.plugin.event;
|
||||||
|
|
||||||
import cd.casic.plugin.SpringPlugin;
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package cd.casic.plugin.event;
|
package cd.casic.plugin.event;
|
||||||
|
|
||||||
import cd.casic.plugin.SpringPlugin;
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package cd.casic.plugin.event;
|
package cd.casic.plugin.event;
|
||||||
|
|
||||||
import cd.casic.plugin.SpringPlugin;
|
import cd.casic.plugin.dataobject.pojo.SpringPlugin;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 11:27
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class AccessDenyException extends ResponseStatusException {
|
||||||
|
|
||||||
|
public AccessDenyException() {
|
||||||
|
this("Access to the resource is forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessDenyException(String reason) {
|
||||||
|
this(reason, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessDenyException(String reason, String detailCode, Object[] detailArgs) {
|
||||||
|
super(HttpStatus.FORBIDDEN, reason, null, detailCode, detailArgs);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 14:45
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class PluginAlreadyExistsException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public static final String PLUGIN_ALREADY_EXISTS_TYPE =
|
||||||
|
"https://127.0.0.1:8090/probs/plugin-alreay-exists";
|
||||||
|
|
||||||
|
public PluginAlreadyExistsException(String pluginName) {
|
||||||
|
super("Plugin already exists.", null, null, null, new Object[] {pluginName});
|
||||||
|
setType(URI.create(PLUGIN_ALREADY_EXISTS_TYPE));
|
||||||
|
getBody().setProperty("pluginName", pluginName);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 15:23
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class PluginDependenciesNotEnabledException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public static final URI TYPE =
|
||||||
|
URI.create("https://127.0.0.1:8090/probs/plugin-dependencies-not-enabled");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Plugin dependencies not enabled exception.
|
||||||
|
*
|
||||||
|
* @param dependencies dependencies that are not enabled
|
||||||
|
*/
|
||||||
|
public PluginDependenciesNotEnabledException(List<String> dependencies) {
|
||||||
|
super("Plugin dependencies are not fully enabled, please enable them first.",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Object[] {dependencies});
|
||||||
|
setType(TYPE);
|
||||||
|
getBody().setProperty("dependencies", dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import org.pf4j.DependencyResolver;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 10:53
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
public abstract class PluginDependencyException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public PluginDependencyException(String reason) {
|
||||||
|
super(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginDependencyException(String reason, Throwable cause) {
|
||||||
|
super(reason, null, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PluginDependencyException(String reason, Throwable cause,
|
||||||
|
String messageDetailCode, Object[] messageDetailArguments) {
|
||||||
|
super(reason, null, cause, messageDetailCode, messageDetailArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CyclicException extends PluginDependencyException {
|
||||||
|
|
||||||
|
public static final String TYPE = "https://127.0.0.1:8090/probs/plugin-cyclic-dependency";
|
||||||
|
|
||||||
|
public CyclicException() {
|
||||||
|
super("A cyclic dependency was detected.");
|
||||||
|
setType(URI.create(TYPE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NotFoundException extends PluginDependencyException {
|
||||||
|
|
||||||
|
public static final String TYPE = "https://127.0.0.1:8090/probs/plugin-dependencies-not-found";
|
||||||
|
|
||||||
|
public NotFoundException(List<String> dependencies) {
|
||||||
|
super("Dependencies were not found.", null, null, new Object[]{dependencies});
|
||||||
|
setType(URI.create(TYPE));
|
||||||
|
getBody().setProperty("dependencies", dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WrongVersionsException extends PluginDependencyException {
|
||||||
|
|
||||||
|
public static final String TYPE =
|
||||||
|
"https://127.0.0.1:8090/probs/plugin-dependencies-with-wrong-versions";
|
||||||
|
|
||||||
|
public WrongVersionsException(List<DependencyResolver.WrongDependencyVersion> versions) {
|
||||||
|
super("Dependencies have wrong version.", null, null, new Object[]{versions});
|
||||||
|
setType(URI.create(TYPE));
|
||||||
|
getBody().setProperty("versions", versions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 15:24
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class PluginDependentsNotDisabledException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public static final URI TYPE =
|
||||||
|
URI.create("https://127.0.0.1:8090/probs/probs/plugin-dependents-not-disabled");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Plugin dependents not disabled exception.
|
||||||
|
*
|
||||||
|
* @param dependents dependents that are not disabled
|
||||||
|
*/
|
||||||
|
public PluginDependentsNotDisabledException(List<String> dependents) {
|
||||||
|
super("Plugin dependents are not fully disabled, please disable them first.",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Object[] {dependents});
|
||||||
|
setType(TYPE);
|
||||||
|
getBody().setProperty("dependents", dependents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Null;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 15:25
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class PluginInstallationException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public PluginInstallationException(String reason, @Nullable String messageDetailCode,
|
||||||
|
@Null Object[] messageDetailArguments) {
|
||||||
|
super(reason, null, null, messageDetailCode, messageDetailArguments);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Null;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 参数不满足的异常
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/14 15:27
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class UnsatisfiedAttributeValueException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public UnsatisfiedAttributeValueException(String reason) {
|
||||||
|
super(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsatisfiedAttributeValueException(String reason, @Nullable String messageDetailCode,
|
||||||
|
@Null Object[] messageDetailArguments) {
|
||||||
|
super(reason, null, null, messageDetailCode, messageDetailArguments);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package cd.casic.plugin.exception;
|
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/19 16:57
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class YmalNotExistsException extends ServerWebInputException {
|
||||||
|
public static final URI TYPE =
|
||||||
|
URI.create("https://127.0.0.1:8090/probs/plugin-yaml-not-exists");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Plugin dependencies not enabled exception.
|
||||||
|
*
|
||||||
|
* @param dependencies dependencies that are not enabled
|
||||||
|
*/
|
||||||
|
public YmalNotExistsException(List<String> dependencies) {
|
||||||
|
super("plugin.yaml. are not exists, please checking checking.",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Object[]{dependencies});
|
||||||
|
setType(TYPE);
|
||||||
|
getBody().setProperty("plugin.yaml", dependencies);
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname AbstractExtension
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 19:59
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public abstract class AbstractExtension implements Extension {
|
|
||||||
|
|
||||||
private String apiVersion;
|
|
||||||
|
|
||||||
private String kind;
|
|
||||||
|
|
||||||
private MetadataOperator metadata;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getApiVersion() {
|
|
||||||
var apiVersionFromGvk = Extension.super.getApiVersion();
|
|
||||||
return apiVersionFromGvk != null ? apiVersionFromGvk : this.apiVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getKind() {
|
|
||||||
var kindFromGvk = Extension.super.getKind();
|
|
||||||
return kindFromGvk != null ? kindFromGvk : this.kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname Extension
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 19:33
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public interface Extension extends ExtensionOperator, Comparable<Extension>{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
default int compareTo(Extension another) {
|
|
||||||
if (another == null || another.getMetadata() == null) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (getMetadata() == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return Objects.compare(getMetadata().getName(), another.getMetadata().getName(),
|
|
||||||
Comparator.naturalOrder());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname ExtensionClient
|
|
||||||
* @Description 接口
|
|
||||||
* @Date 2025/5/9 15:43
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public interface ExtensionClient {
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname ExtensionMatcher
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/9 17:03
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public interface ExtensionMatcher {
|
|
||||||
|
|
||||||
boolean match(Extension extension);
|
|
||||||
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import cd.casic.plugin.annotation.BasePluginInformation;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import kotlin.Metadata;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname ExtensionOperator
|
|
||||||
* @Description 扩展类的操作
|
|
||||||
* @Date 2025/5/8 15:12
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public interface ExtensionOperator {
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED)
|
|
||||||
@JsonProperty("apiVersion")
|
|
||||||
default String getApiVersion() {
|
|
||||||
final var basePluginInformation = getClass().getAnnotation(BasePluginInformation.class);
|
|
||||||
if (basePluginInformation == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (StringUtils.hasText(basePluginInformation.group())) {
|
|
||||||
return basePluginInformation.group() + "/" + basePluginInformation.version();
|
|
||||||
}
|
|
||||||
return basePluginInformation.version();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED)
|
|
||||||
@JsonProperty("kind")
|
|
||||||
default String getKind() {
|
|
||||||
final var basePluginInformation = getClass().getAnnotation(BasePluginInformation.class);
|
|
||||||
if (basePluginInformation == null) {
|
|
||||||
// return null if having no GVK annotation
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return basePluginInformation.kind();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED, implementation = Metadata.class)
|
|
||||||
@JsonProperty("metadata")
|
|
||||||
MetadataOperator getMetadata();
|
|
||||||
|
|
||||||
void setApiVersion(String apiVersion);
|
|
||||||
|
|
||||||
void setKind(String kind);
|
|
||||||
|
|
||||||
void setMetadata(MetadataOperator metadata);
|
|
||||||
|
|
||||||
default void groupVersionKind(GroupVersionKind gvk) {
|
|
||||||
setApiVersion(gvk.groupVersion().toString());
|
|
||||||
setKind(gvk.kind());
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
default GroupVersionKind groupVersionKind() {
|
|
||||||
return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind());
|
|
||||||
}
|
|
||||||
|
|
||||||
static <T extends ExtensionOperator> Predicate<T> isNotDeleted() {
|
|
||||||
return ext -> ext.getMetadata().getDeletionTimestamp() == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isDeleted(ExtensionOperator extension) {
|
|
||||||
return ExtensionUtil.isDeleted(extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.springframework.data.domain.Sort.Order.asc;
|
|
||||||
import static org.springframework.data.domain.Sort.Order.desc;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname ExtensionUtil
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 15:12
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public enum ExtensionUtil {
|
|
||||||
;
|
|
||||||
|
|
||||||
public static boolean isDeleted(ExtensionOperator extension) {
|
|
||||||
return extension.getMetadata() != null
|
|
||||||
&& extension.getMetadata().getDeletionTimestamp() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean addFinalizers(MetadataOperator metadata, Set<String> finalizers) {
|
|
||||||
var modifiableFinalizers = new HashSet<>(
|
|
||||||
metadata.getFinalizers() == null ? Collections.emptySet() : metadata.getFinalizers());
|
|
||||||
var added = modifiableFinalizers.addAll(finalizers);
|
|
||||||
if (added) {
|
|
||||||
metadata.setFinalizers(modifiableFinalizers);
|
|
||||||
}
|
|
||||||
return added;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean removeFinalizers(MetadataOperator metadata, Set<String> finalizers) {
|
|
||||||
if (metadata.getFinalizers() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var existingFinalizers = new HashSet<>(metadata.getFinalizers());
|
|
||||||
var removed = existingFinalizers.removeAll(finalizers);
|
|
||||||
if (removed) {
|
|
||||||
metadata.setFinalizers(existingFinalizers);
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query for not deleting.
|
|
||||||
*
|
|
||||||
* @return Query
|
|
||||||
*/
|
|
||||||
public static boolean notDeleting() {
|
|
||||||
return StringUtils.isNotBlank("metadata.deletionTimestamp");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default sort by creation timestamp desc and name asc.
|
|
||||||
*
|
|
||||||
* @return Sort
|
|
||||||
*/
|
|
||||||
public static Sort defaultSort() {
|
|
||||||
return Sort.by(
|
|
||||||
desc("metadata.creationTimestamp"),
|
|
||||||
asc("metadata.name")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname GroupKind
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 19:33
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public record GroupKind(String group, String kind) {
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname GroupVersion
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 19:33
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public record GroupVersion(String group, String version) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return StringUtils.hasText(group) ? group + "/" + version : version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GroupVersion parseAPIVersion(String apiVersion) {
|
|
||||||
Assert.hasText(apiVersion, "API version must not be blank");
|
|
||||||
|
|
||||||
var groupVersion = apiVersion.split("/");
|
|
||||||
return switch (groupVersion.length) {
|
|
||||||
case 1 -> new GroupVersion("", apiVersion);
|
|
||||||
case 2 -> new GroupVersion(groupVersion[0], groupVersion[1]);
|
|
||||||
default ->
|
|
||||||
throw new IllegalArgumentException("Unexpected APIVersion string: " + apiVersion);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import cd.casic.plugin.annotation.BasePluginInformation;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname GroupVersionKind
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 19:33
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public record GroupVersionKind(String group, String version, String kind) {
|
|
||||||
|
|
||||||
public GroupVersionKind {
|
|
||||||
Assert.hasText(version, "Version must not be blank");
|
|
||||||
Assert.hasText(kind, "Kind must not be blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
public GroupVersion groupVersion() {
|
|
||||||
return new GroupVersion(group, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GroupKind groupKind() {
|
|
||||||
return new GroupKind(group, kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasGroup() {
|
|
||||||
return StringUtils.hasText(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GroupVersionKind fromAPIVersionAndKind(String apiVersion, String kind) {
|
|
||||||
Assert.hasText(kind, "Kind must not be blank");
|
|
||||||
|
|
||||||
var gv = GroupVersion.parseAPIVersion(apiVersion);
|
|
||||||
return new GroupVersionKind(gv.group(), gv.version(), kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends Extension> GroupVersionKind fromExtension(Class<T> extension) {
|
|
||||||
BasePluginInformation gvk = extension.getAnnotation(BasePluginInformation.class);
|
|
||||||
return new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
if (hasGroup()) {
|
|
||||||
return group + "/" + version + "/" + kind;
|
|
||||||
}
|
|
||||||
return version + "/" + kind;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname Metadata
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 19:52
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(exclude = "version")
|
|
||||||
public class Metadata implements MetadataOperator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata name. 唯一的。
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private String generateName;
|
|
||||||
|
|
||||||
private Map<String, String> labels;
|
|
||||||
|
|
||||||
private Map<String, String> annotations;
|
|
||||||
|
|
||||||
private Long version;
|
|
||||||
|
|
||||||
private Instant creationTimestamp;
|
|
||||||
|
|
||||||
private Instant deletionTimestamp;
|
|
||||||
|
|
||||||
private Set<String> finalizers;
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import kotlin.Metadata;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname MetadataOperator
|
|
||||||
* @Description 元数据的处理
|
|
||||||
* @Date 2025/5/8 15:17
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
@JsonDeserialize(as = Metadata.class)
|
|
||||||
@Schema(implementation = Metadata.class)
|
|
||||||
public interface MetadataOperator {
|
|
||||||
|
|
||||||
@Schema(name = "name", description = "Metadata name", requiredMode = REQUIRED)
|
|
||||||
@JsonProperty("name")
|
|
||||||
String getName();
|
|
||||||
|
|
||||||
@Schema(name = "generateName", description = "名字获取失败就自动生成,根据generateName字段")
|
|
||||||
String getGenerateName();
|
|
||||||
|
|
||||||
@Schema(name = "labels")
|
|
||||||
@JsonProperty("labels")
|
|
||||||
Map<String, String> getLabels();
|
|
||||||
|
|
||||||
@Schema(name = "annotations")
|
|
||||||
@JsonProperty("annotations")
|
|
||||||
Map<String, String> getAnnotations();
|
|
||||||
|
|
||||||
@Schema(name = "version", nullable = true)
|
|
||||||
@JsonProperty("version")
|
|
||||||
Long getVersion();
|
|
||||||
|
|
||||||
@Schema(name = "creationTimestamp", nullable = true)
|
|
||||||
@JsonProperty("creationTimestamp")
|
|
||||||
Instant getCreationTimestamp();
|
|
||||||
|
|
||||||
@Schema(name = "deletionTimestamp", nullable = true)
|
|
||||||
@JsonProperty("deletionTimestamp")
|
|
||||||
Instant getDeletionTimestamp();
|
|
||||||
|
|
||||||
@Schema(name = "finalizers", nullable = true)
|
|
||||||
Set<String> getFinalizers();
|
|
||||||
|
|
||||||
void setName(String name);
|
|
||||||
|
|
||||||
void setGenerateName(String generateName);
|
|
||||||
|
|
||||||
void setLabels(Map<String, String> labels);
|
|
||||||
|
|
||||||
void setAnnotations(Map<String, String> annotations);
|
|
||||||
|
|
||||||
void setVersion(Long version);
|
|
||||||
|
|
||||||
void setCreationTimestamp(Instant creationTimestamp);
|
|
||||||
|
|
||||||
void setDeletionTimestamp(Instant deletionTimestamp);
|
|
||||||
|
|
||||||
void setFinalizers(Set<String> finalizers);
|
|
||||||
|
|
||||||
static boolean metadataDeepEquals(MetadataOperator left, MetadataOperator right) {
|
|
||||||
if (left == null && right == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (left == null || right == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getName(), right.getName())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getLabels(), right.getLabels())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getAnnotations(), right.getAnnotations())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getCreationTimestamp(), right.getCreationTimestamp())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getDeletionTimestamp(), right.getDeletionTimestamp())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getVersion(), right.getVersion())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(left.getFinalizers(), right.getFinalizers())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
package cd.casic.plugin.extension;
|
|
||||||
|
|
||||||
import cd.casic.plugin.annotation.BasePluginInformation;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.google.common.collect.EvictingQueue;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.ToString;
|
|
||||||
import org.pf4j.PluginState;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname Plugin
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/8 20:03
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@BasePluginInformation(group = "plugin.ops", version = "1.0.0", kind = "Plugin", plural = "plugins",
|
|
||||||
singular = "plugin")
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class Plugin extends AbstractExtension {
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED)
|
|
||||||
private PluginSpec spec;
|
|
||||||
|
|
||||||
private PluginStatus status;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@JsonIgnore
|
|
||||||
public PluginStatus statusNonNull() {
|
|
||||||
if (this.status == null) {
|
|
||||||
this.status = new PluginStatus();
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class PluginSpec {
|
|
||||||
|
|
||||||
private String displayName;
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED,
|
|
||||||
pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-("
|
|
||||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\."
|
|
||||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\"
|
|
||||||
+ ".[0-9a-zA-Z-]+)*))?$")
|
|
||||||
private String version;
|
|
||||||
|
|
||||||
private PluginAuthor author;
|
|
||||||
|
|
||||||
private String logo;
|
|
||||||
|
|
||||||
private Map<String, String> pluginDependencies = new HashMap<>(4);
|
|
||||||
|
|
||||||
private String homepage;
|
|
||||||
|
|
||||||
private String repo;
|
|
||||||
|
|
||||||
private String issues;
|
|
||||||
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
private List<License> license;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SemVer format.
|
|
||||||
*/
|
|
||||||
private String requires = "*";
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
private String pluginClass;
|
|
||||||
|
|
||||||
private Boolean enabled = false;
|
|
||||||
|
|
||||||
private String settingName;
|
|
||||||
|
|
||||||
private String configMapName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class License {
|
|
||||||
private String name;
|
|
||||||
private String url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class PluginStatus {
|
|
||||||
|
|
||||||
private Phase phase;
|
|
||||||
|
|
||||||
private EvictingQueue queue;
|
|
||||||
|
|
||||||
private Instant lastStartTime;
|
|
||||||
|
|
||||||
private PluginState lastProbeState;
|
|
||||||
|
|
||||||
private String entry;
|
|
||||||
|
|
||||||
private String stylesheet;
|
|
||||||
|
|
||||||
private String logo;
|
|
||||||
|
|
||||||
@Schema(description = "Load location of the plugin, often a path.")
|
|
||||||
private URI loadLocation;
|
|
||||||
|
|
||||||
// 这里实现一个先进先出的队列
|
|
||||||
public static EvictingQueue nullSafeConditions(@NonNull PluginStatus status) {
|
|
||||||
Assert.notNull(status, "The status must not be null.");
|
|
||||||
if (status.getQueue() == null) {
|
|
||||||
status.setQueue(EvictingQueue.create(20));
|
|
||||||
}
|
|
||||||
return status.getQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Phase {
|
|
||||||
PENDING,
|
|
||||||
STARTING,
|
|
||||||
CREATED,
|
|
||||||
DISABLING,
|
|
||||||
DISABLED,
|
|
||||||
RESOLVED,
|
|
||||||
STARTED,
|
|
||||||
STOPPED,
|
|
||||||
FAILED,
|
|
||||||
UNKNOWN,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@ToString
|
|
||||||
public static class PluginAuthor {
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED, minLength = 1)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private String website;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package cd.casic.plugin.function;
|
|
||||||
|
|
||||||
import reactor.core.Disposable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Classname Controller
|
|
||||||
* @Description TODO
|
|
||||||
* @Date 2025/5/9 15:55
|
|
||||||
* @Created by mianbin
|
|
||||||
*/
|
|
||||||
public interface Controller extends Disposable {
|
|
||||||
|
|
||||||
String getName();
|
|
||||||
|
|
||||||
void start();
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,19 @@
|
|||||||
|
package cd.casic.plugin.function;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: TODO
|
||||||
|
* @author: mianbin
|
||||||
|
* @date: 2025/5/12 23:52
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public interface FinderRegistry {
|
||||||
|
Map<String, Object> getFinders();
|
||||||
|
|
||||||
|
void register(String pluginId, ApplicationContext pluginContext);
|
||||||
|
|
||||||
|
void unregister(String pluginId);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user