From 6ac4a14ded98f907025ac919ef3846921c67be4f Mon Sep 17 00:00:00 2001 From: mianbin Date: Fri, 23 May 2025 14:55:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=90=8E=E7=BB=AD=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/inspectionProfiles/Project_Default.xml | 3 + .idea/jarRepositories.xml | 10 +- dependencies/.flattened-pom.xml | 15 + dependencies/pom.xml | 17 +- .../framework/commons/util/io/FileUtils.java | 299 +++++++++++++++++- .../commons/util/json/JsonUtils.java | 40 +++ framework/spring-boot-starter-plugin/pom.xml | 11 + .../cd/casic/plugin/DefaultPluginGetter.java | 18 ++ .../cd/casic/plugin/OpsPluginDescriptor.java | 29 ++ .../java/cd/casic/plugin/PluginService.java | 46 --- .../cd/casic/plugin/PluginServiceImpl.java | 180 +++++++++++ .../casic/plugin/PluginsRootGetterImpl.java | 30 ++ .../java/cd/casic/plugin/SpringPlugin.java | 10 - .../cd/casic/plugin/SpringPluginFactory.java | 34 ++ .../plugin/YamlPluginDescriptorFinder.java | 42 +++ .../cd/casic/plugin/YamlPluginFinder.java | 84 +++++ .../casic/plugin/annotation/AdminGroup.java | 37 +++ .../casic/plugin/annotation/AdminGroups.java | 20 ++ .../annotation/BasePluginInformation.java | 27 -- .../plugin/annotation/InterceptPath.java | 22 ++ .../annotation/PluginConfiguration.java | 17 + .../casic/plugin/config/PluginProperties.java | 78 +++++ .../{ => constants}/PluginConstants.java | 32 +- .../java/cd/casic/plugin/core/BasePlugin.java | 40 +++ .../DefaultDevelopmentPluginRepository.java | 47 +++ ...efaultPluginApplicationContextFactory.java | 231 ++++++++++++++ .../cd/casic/plugin/core/DevPluginLoader.java | 52 +++ .../casic/plugin/core/OpsPluginManager.java | 187 +++++++++++ .../cd/casic/plugin/core/PluginContext.java | 26 ++ .../PluginRequestMappingHandlerMapping.java | 69 ++++ .../core/PropertyPluginStatusProvider.java | 59 ++++ .../core/SharedApplicationContextFactory.java | 83 +++++ .../plugin/core/SpringComponentsFinder.java | 80 +++++ .../plugin/core/SpringExtensionFactory.java | 89 ++++++ .../check/PluginRunningBeforeChecker.java | 10 + .../ApplicationContextPluginProcessor.java | 48 +++ .../core/register/ClassGroupProcessor.java | 78 +++++ .../core/register/ControllerProcessor.java | 167 ++++++++++ .../plugin/core/register/MongoProcessor.java | 99 ++++++ .../core/register/ResourcesProcessor.java | 92 ++++++ .../register/SpringBeanRegisterProcessor.java | 53 ++++ .../core/register/SpringDocProcessor.java | 72 +++++ .../register/StartPluginManagerProcessor.java | 56 ++++ .../config/ConfigurationFileProcessor.java | 113 +++++++ .../SpringAutoConfigurationFileProcessor.java | 250 +++++++++++++++ .../core/register/database/DBEnums.java | 32 ++ .../register/database/DataBaseProperty.java | 19 ++ .../register/database/DatabaseProcessor.java | 69 ++++ .../register/filter/PluginClassFilter.java | 9 + .../register/filter/impl/BasicBeanFilter.java | 31 ++ .../filter/impl/ControllerFilter.java | 28 ++ .../filter/impl/DataBaseEntityFilter.java | 24 ++ .../register/filter/impl/MapperFilter.java | 25 ++ .../impl/PluginConfigurationFilter.java | 29 ++ .../register/filter/impl/WebSocketFilter.java | 25 ++ .../core/register/mybatis/MapperHandler.java | 102 ++++++ .../register/mybatis/MybatisCommonConfig.java | 39 +++ .../register/mybatis/MybatisPlusConfig.java | 34 ++ .../mybatis/MybatisPlusProcessor.java | 136 ++++++++ .../mybatis/PluginFollowCoreConfig.java | 126 ++++++++ .../mybatis/PluginResourceFinder.java | 120 +++++++ .../websocket/BaseServerEndpoint.java | 12 + .../websocket/WebSocketProcessor.java | 214 +++++++++++++ .../dao/PluginFacadeMemoryCache.java | 76 +++++ .../dataobject/dao/PluginInformationDao.java | 16 + .../dataobject/dto/PluginInformation.java | 42 +++ .../dataobject/dto/PluginSpecStorage.java | 76 +++++ .../plugin/dataobject/pojo/PluginFacade.java | 23 ++ .../plugin/dataobject/pojo/PluginInfo.java | 162 ++++++++++ .../plugin/dataobject/pojo/SpringPlugin.java | 108 +++++++ ...ent.java => OpsPluginBeforeStopEvent.java} | 5 +- .../plugin/event/OpsPluginStartedEvent.java | 22 ++ ...dEvent.java => OpsPluginStoppedEvent.java} | 4 +- .../plugin/event/PluginStartedEvent.java | 17 +- .../event/SpringPluginStartedEvent.java | 2 +- .../event/SpringPluginStartingEvent.java | 2 +- .../event/SpringPluginStoppedEvent.java | 2 +- .../event/SpringPluginStoppingEvent.java | 2 +- .../plugin/exception/AccessDenyException.java | 25 ++ .../PluginAlreadyExistsException.java | 23 ++ ...PluginDependenciesNotEnabledException.java | 35 ++ .../exception/PluginDependencyException.java | 64 ++++ .../PluginDependentsNotDisabledException.java | 34 ++ .../PluginInstallationException.java | 19 ++ .../UnsatisfiedAttributeValueException.java | 23 ++ .../exception/YmalNotExistsException.java | 32 ++ .../plugin/extension/AbstractExtension.java | 32 -- .../cd/casic/plugin/extension/Extension.java | 26 -- .../plugin/extension/ExtensionClient.java | 11 - .../plugin/extension/ExtensionMatcher.java | 13 - .../plugin/extension/ExtensionOperator.java | 74 ----- .../casic/plugin/extension/ExtensionUtil.java | 69 ---- .../cd/casic/plugin/extension/GroupKind.java | 10 - .../casic/plugin/extension/GroupVersion.java | 30 -- .../plugin/extension/GroupVersionKind.java | 51 --- .../cd/casic/plugin/extension/Metadata.java | 38 --- .../plugin/extension/MetadataOperator.java | 101 ------ .../cd/casic/plugin/extension/Plugin.java | 151 --------- .../cd/casic/plugin/function/Controller.java | 17 - .../casic/plugin/function/FinderRegistry.java | 19 ++ .../casic/plugin/function/IPluginManager.java | 85 +++++ .../plugin/function/IPluginProcessor.java | 34 ++ .../plugin/{ => function}/PluginGetter.java | 4 +- .../plugin/function/PluginLifecycle.java | 20 ++ .../plugin/function/PluginsRootGetter.java | 3 +- .../cd/casic/plugin/function/Reconciler.java | 29 -- .../cd/casic/plugin/function/Watcher.java | 97 ------ .../function/WebSocketEndpointManager.java | 15 + .../casic/plugin/utils/AnnotationUtils.java | 21 ++ .../plugin/utils/BundleResourceUtils.java | 57 ++++ .../cd/casic/plugin/utils/CommonUtils.java | 152 +++++++++ .../casic/plugin/utils/ConfigFileUtils.java | 43 +++ .../java/cd/casic/plugin/utils/PathUtils.java | 151 +++++++++ .../plugin/utils/PluginDescriptorUtils.java | 108 +++++++ .../cd/casic/plugin/utils/PluginsUtils.java | 87 +++++ .../cd/casic/plugin/utils/ResourceUtils.java | 92 ++++++ .../plugin/{ => utils}/VersionUtils.java | 2 +- .../plugin/utils/YamlUnstructuredLoader.java | 41 +++ .../utils/dBUtils/AnnotationScanner.java | 72 +++++ .../plugin/utils/dBUtils/SQLGenerator.java | 225 +++++++++++++ .../utils/dBUtils/annotation/Entity.java | 10 + .../plugin/utils/dBUtils/annotation/SQL.java | 14 + .../spring-boot-starter-security/pom.xml | 17 + .../setting/impl/AuthHostServiceImpl.java | 1 - 124 files changed, 6146 insertions(+), 866 deletions(-) create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/DefaultPluginGetter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/OpsPluginDescriptor.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginService.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginServiceImpl.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginsRootGetterImpl.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPlugin.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPluginFactory.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginDescriptorFinder.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginFinder.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroup.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroups.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/BasePluginInformation.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/InterceptPath.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/PluginConfiguration.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/config/PluginProperties.java rename framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/{ => constants}/PluginConstants.java (51%) create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/BasePlugin.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultDevelopmentPluginRepository.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultPluginApplicationContextFactory.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DevPluginLoader.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/OpsPluginManager.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginContext.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginRequestMappingHandlerMapping.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PropertyPluginStatusProvider.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SharedApplicationContextFactory.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringComponentsFinder.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringExtensionFactory.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/check/PluginRunningBeforeChecker.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ApplicationContextPluginProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ClassGroupProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ControllerProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/MongoProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ResourcesProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringBeanRegisterProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringDocProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/StartPluginManagerProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/ConfigurationFileProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/SpringAutoConfigurationFileProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DBEnums.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DataBaseProperty.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DatabaseProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/PluginClassFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/BasicBeanFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/ControllerFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/DataBaseEntityFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/MapperFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/PluginConfigurationFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/WebSocketFilter.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MapperHandler.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisCommonConfig.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusConfig.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginFollowCoreConfig.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginResourceFinder.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/BaseServerEndpoint.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/WebSocketProcessor.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginFacadeMemoryCache.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginInformationDao.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginInformation.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginSpecStorage.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginFacade.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginInfo.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/SpringPlugin.java rename framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/{PluginBeforeStopEvent.java => OpsPluginBeforeStopEvent.java} (76%) create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStartedEvent.java rename framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/{PluginStoppedEvent.java => OpsPluginStoppedEvent.java} (75%) create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/AccessDenyException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginAlreadyExistsException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependenciesNotEnabledException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependencyException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependentsNotDisabledException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginInstallationException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/UnsatisfiedAttributeValueException.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/YmalNotExistsException.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/AbstractExtension.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Extension.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionClient.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionMatcher.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionOperator.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionUtil.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupKind.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersion.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersionKind.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Metadata.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/MetadataOperator.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Plugin.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Controller.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/FinderRegistry.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginManager.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginProcessor.java rename framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/{ => function}/PluginGetter.java (75%) create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginLifecycle.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Reconciler.java delete mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Watcher.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/WebSocketEndpointManager.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/AnnotationUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/BundleResourceUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/CommonUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ConfigFileUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PathUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginDescriptorUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginsUtils.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ResourceUtils.java rename framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/{ => utils}/VersionUtils.java (97%) create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/YamlUnstructuredLoader.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/AnnotationScanner.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/SQLGenerator.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/Entity.java create mode 100644 framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/SQL.java diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index ee9f695f..aeccedfa 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -63,6 +63,9 @@ + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 0a0608cd..1945c076 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -1,16 +1,16 @@ - - + + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/DefaultPluginGetter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/DefaultPluginGetter.java new file mode 100644 index 00000000..e6b535d6 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/DefaultPluginGetter.java @@ -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; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/OpsPluginDescriptor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/OpsPluginDescriptor.java new file mode 100644 index 00000000..53b1fe37 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/OpsPluginDescriptor.java @@ -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 configFileActive; + + public OpsPluginDescriptor(String pluginId, String pluginDescription, String pluginClass, + String version, String requires, String provider, String license, + String configFileName, List configFileActive) { + super(pluginId, pluginDescription, pluginClass, version, requires, provider, license); + this.configFileActive = configFileActive; + this.configFileName = configFileName; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginService.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginService.java deleted file mode 100644 index 0362a859..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginService.java +++ /dev/null @@ -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 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 getRequiredDependencies(Plugin plugin, - Predicate predicate); -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginServiceImpl.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginServiceImpl.java new file mode 100644 index 00000000..6c194c56 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginServiceImpl.java @@ -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 plugins) throws Exception { + + } + + @Override + public List 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; + } +} \ No newline at end of file diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginsRootGetterImpl.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginsRootGetterImpl.java new file mode 100644 index 00000000..37dfc607 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginsRootGetterImpl.java @@ -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(); + } + +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPlugin.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPlugin.java deleted file mode 100644 index 92acef17..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPlugin.java +++ /dev/null @@ -1,10 +0,0 @@ -package cd.casic.plugin; - -/** - * @Classname SpringPlugin - * @Description TODO - * @Date 2025/5/8 14:48 - * @Created by mianbin - */ -public class SpringPlugin { -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPluginFactory.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPluginFactory.java new file mode 100644 index 00000000..85978fae --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/SpringPluginFactory.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginDescriptorFinder.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginDescriptorFinder.java new file mode 100644 index 00000000..5d26c5bd --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginDescriptorFinder.java @@ -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); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginFinder.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginFinder.java new file mode 100644 index 00000000..f50b0843 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/YamlPluginFinder.java @@ -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); + } + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroup.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroup.java new file mode 100644 index 00000000..efe99727 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroup.java @@ -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; +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroups.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroups.java new file mode 100644 index 00000000..191c5f49 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/AdminGroups.java @@ -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(); +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/BasePluginInformation.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/BasePluginInformation.java deleted file mode 100644 index 14d256a3..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/BasePluginInformation.java +++ /dev/null @@ -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(); -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/InterceptPath.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/InterceptPath.java new file mode 100644 index 00000000..88b8171d --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/InterceptPath.java @@ -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(); +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/PluginConfiguration.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/PluginConfiguration.java new file mode 100644 index 00000000..e6a94ece --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/annotation/PluginConfiguration.java @@ -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模式下的后缀 +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/config/PluginProperties.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/config/PluginProperties.java new file mode 100644 index 00000000..6bf92554 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/config/PluginProperties.java @@ -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 classesDirectories = new ArrayList<>(); + /** + * 插件接口统一前缀 + */ + private String restPathPrefix; + /** + * 插件restful接口前缀中是否需要包含插件id,默认包含 + */ + private boolean enablePluginIdAsRestPrefix = true; + /** + * 插件jar目录. + */ + private List 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()); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginConstants.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/constants/PluginConstants.java similarity index 51% rename from framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginConstants.java rename to framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/constants/PluginConstants.java index 1491ee6c..ec8b25ee 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginConstants.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/constants/PluginConstants.java @@ -1,5 +1,6 @@ -package cd.casic.plugin; +package cd.casic.plugin.constants; +import lombok.Data; import lombok.experimental.UtilityClass; /** @@ -8,8 +9,27 @@ import lombok.experimental.UtilityClass; * @Date 2025/5/8 14:51 * @Created by mianbin */ +@Data @UtilityClass 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. */ @@ -28,4 +48,14 @@ public class PluginConstants { String assetsRoutePrefix(String pluginName) { 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"; + } } diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/BasePlugin.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/BasePlugin.java new file mode 100644 index 00000000..d1ab8384 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/BasePlugin.java @@ -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(); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultDevelopmentPluginRepository.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultDevelopmentPluginRepository.java new file mode 100644 index 00000000..4eb08fc8 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultDevelopmentPluginRepository.java @@ -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 fixedPaths = new ArrayList<>(); + + public DefaultDevelopmentPluginRepository(Path... pluginsRoots) { + super(pluginsRoots); + } + + public DefaultDevelopmentPluginRepository(List pluginsRoots) { + super(pluginsRoots); + } + + public void setFixedPaths(List paths) { + if (CollectionUtils.isEmpty(paths)) { + return; + } + fixedPaths.clear(); + fixedPaths.addAll(paths); + } + + @Override + public List getPluginPaths() { + List paths = new ArrayList<>(fixedPaths); + paths.addAll(super.getPluginPaths()); + return paths; + } + + @Override + public boolean deletePluginPath(Path pluginPath) { + return fixedPaths.remove(pluginPath); + } +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultPluginApplicationContextFactory.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultPluginApplicationContextFactory.java new file mode 100644 index 00000000..ad725a5f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DefaultPluginApplicationContextFactory.java @@ -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 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> 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() + .>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 { + + 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> 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>(); + return propertySources; + } + + private List> 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); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DevPluginLoader.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DevPluginLoader.java new file mode 100644 index 00000000..6372e178 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/DevPluginLoader.java @@ -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); + } +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/OpsPluginManager.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/OpsPluginManager.java new file mode 100644 index 00000000..93f51587 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/OpsPluginManager.java @@ -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 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 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 getDependents(String pluginId) { + if (Objects.isNull(getPlugin(pluginId))) { + return List.of(); + } + var dependents = new ArrayList(); + var stack = new Stack(); + // 依赖管理 + 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); + } + } + } + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginContext.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginContext.java new file mode 100644 index 00000000..0560d85f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginContext.java @@ -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; +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginRequestMappingHandlerMapping.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginRequestMappingHandlerMapping.java new file mode 100644 index 00000000..326fd77a --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PluginRequestMappingHandlerMapping.java @@ -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 pluginMappingInfo = + new LinkedMultiValueMap<>(); + + @Override + protected void initHandlerMethods() { + } + + public void registerHandlerMethods(String pluginId, Object handler) { + + } + + private String formatMappings(Class userType, Map 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 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 getMappings(String pluginId) { + List requestMappingInfos = pluginMappingInfo.get(pluginId); + if (requestMappingInfos == null) { + return Collections.emptyList(); + } + return List.copyOf(requestMappingInfos); + } +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PropertyPluginStatusProvider.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PropertyPluginStatusProvider.java new file mode 100644 index 00000000..933a9e00 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/PropertyPluginStatusProvider.java @@ -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 enabledPlugins; + private final List 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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SharedApplicationContextFactory.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SharedApplicationContextFactory.java new file mode 100644 index 00000000..cb6d2307 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SharedApplicationContextFactory.java @@ -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; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringComponentsFinder.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringComponentsFinder.java new file mode 100644 index 00000000..bf5466e2 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringComponentsFinder.java @@ -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> readPluginsStorages() { + throw new UnsupportedOperationException(); + } + + @Override + public Map> readClasspathStorages() { + throw new UnsupportedOperationException(); + } + + private Set readPluginStorage(PluginWrapper pluginWrapper) { + var pluginId = pluginWrapper.getPluginId(); + log.debug("Reading extensions storage from plugin '{}'", pluginId); + var bucket = new HashSet(); + 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 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())); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringExtensionFactory.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringExtensionFactory.java new file mode 100644 index 00000000..499fd83e --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/SpringExtensionFactory.java @@ -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 create(Class extensionClass) { + return getPluginApplicationContextBy(extensionClass) + .map(context -> context.getBean(extensionClass)) + .orElseGet(() -> createWithoutSpring(extensionClass)); + } + + @SuppressWarnings("unchecked") + protected T createWithoutSpring(final Class 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> 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 Optional getPluginApplicationContextBy( + final Class extensionClass) { + return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass)) + .map(PluginWrapper::getPlugin) + .filter(SpringPlugin.class::isInstance) + .map(plugin -> (SpringPlugin) plugin) + .map(SpringPlugin::getApplicationContext); + } + + private String nameOf(final Class clazz) { + return clazz.getName(); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/check/PluginRunningBeforeChecker.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/check/PluginRunningBeforeChecker.java new file mode 100644 index 00000000..800285f1 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/check/PluginRunningBeforeChecker.java @@ -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 { +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ApplicationContextPluginProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ApplicationContextPluginProcessor.java new file mode 100644 index 00000000..b7bf7fca --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ApplicationContextPluginProcessor.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ClassGroupProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ClassGroupProcessor.java new file mode 100644 index 00000000..f618d91f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ClassGroupProcessor.java @@ -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 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> classList = new ArrayList<>(); + Set 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> 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(); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ControllerProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ControllerProcessor.java new file mode 100644 index 00000000..ffb04e8c --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ControllerProcessor.java @@ -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 getRequestMappingInfo(PluginInfo plugin) throws Exception { + List 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 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 memberValues = (Map) 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 ""; + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/MongoProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/MongoProcessor.java new file mode 100644 index 00000000..e1f3970a --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/MongoProcessor.java @@ -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 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 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]; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ResourcesProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ResourcesProcessor.java new file mode 100644 index 00000000..b7697d8c --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/ResourcesProcessor.java @@ -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 staticClassPathLocations = plugin.getStaticClassPathLocations(); + File jarFile = null; + if (Files.isDirectory(pluginPath)) { + List 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 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()); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringBeanRegisterProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringBeanRegisterProcessor.java new file mode 100644 index 00000000..402b74f0 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringBeanRegisterProcessor.java @@ -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> 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 { + + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringDocProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringDocProcessor.java new file mode 100644 index 00000000..27d63877 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/SpringDocProcessor.java @@ -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> 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>) 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(); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/StartPluginManagerProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/StartPluginManagerProcessor.java new file mode 100644 index 00000000..80ac638d --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/StartPluginManagerProcessor.java @@ -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 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); + } + }); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/ConfigurationFileProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/ConfigurationFileProcessor.java new file mode 100644 index 00000000..6b0af6c1 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/ConfigurationFileProcessor.java @@ -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); + } + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/SpringAutoConfigurationFileProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/SpringAutoConfigurationFileProcessor.java new file mode 100644 index 00000000..a77ed207 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/config/SpringAutoConfigurationFileProcessor.java @@ -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 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 resourcesFromDescriptor = getConfigResourceFromDescriptor(plugin); + List> propProfiles = getPropProfiles(resourcesFromDescriptor); + if (ObjectUtils.isEmpty(propProfiles)) { + return; + } + for (PropertySource propertySource : propProfiles) { + environment.getPropertySources().addLast(propertySource); + } + // 发现原始文件中配置的 profiles + List 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 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 configFileActiveNames = getConfigFileActiveNames(configFileName, opsPluginDescriptor.getConfigFileActive()); + log.info("插件{} 的配置文件为 {} {}", plugin.getPluginId(), configFileName, configFileActiveNames); + Path pluginPath = plugin.getPluginWrapper().getPluginPath(); + + List 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 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> 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 getConfigFileActiveNames(String configFileName, List 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> getPropProfiles(List resources) throws IOException { + List> 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> propertySources = propertySourceLoader.load(filename, resource); + if (ObjectUtils.isEmpty(propertySources)) { + continue; + } + propProfiles.addAll(propertySources); + } + } + return propProfiles; + } + + private List getProfiles(Environment environment) { + List profiles = new ArrayList<>(); + Set activatedViaProperty = getProfilesActivatedViaProperty(environment); + profiles.addAll(getOtherActiveProfiles(environment, activatedViaProperty)); + profiles.addAll(activatedViaProperty); + profiles.removeIf( + (profile) -> (profile != null && profile.isDefaultProfile())); + return profiles; + } + + private Set getProfilesActivatedViaProperty(Environment environment) { + if (!environment.containsProperty(ACTIVE_PROFILES_PROPERTY) + && !environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) { + return Collections.emptySet(); + } + Binder binder = Binder.get(environment); + Set activeProfiles = new LinkedHashSet<>(); + activeProfiles.addAll(getProfiles(binder, INCLUDE_PROFILES_PROPERTY)); + activeProfiles.addAll(getProfiles(binder, ACTIVE_PROFILES_PROPERTY)); + return activeProfiles; + } + + private List getOtherActiveProfiles(Environment environment, Set activatedViaProperty) { + return Arrays.stream(environment.getActiveProfiles()).map(Profile::new) + .filter((profile) -> !activatedViaProperty.contains(profile)) + .collect(Collectors.toList()); + } + + private Set getProfiles(Binder binder, String name) { + return binder.bind(name, String[].class).map(this::asProfileSet) + .orElse(Collections.emptySet()); + } + + private Set asProfileSet(String[] profileNames) { + List 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); + } + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DBEnums.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DBEnums.java new file mode 100644 index 00000000..c3146daf --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DBEnums.java @@ -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; + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DataBaseProperty.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DataBaseProperty.java new file mode 100644 index 00000000..3f98f157 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DataBaseProperty.java @@ -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(); +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DatabaseProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DatabaseProcessor.java new file mode 100644 index 00000000..e9e9e225 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/database/DatabaseProcessor.java @@ -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> 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 { + + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/PluginClassFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/PluginClassFilter.java new file mode 100644 index 00000000..00e25369 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/PluginClassFilter.java @@ -0,0 +1,9 @@ +package cd.casic.plugin.core.register.filter; + +public interface PluginClassFilter{ + String groupName(); + + void initialize(); + + boolean filter(Class clazz); +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/BasicBeanFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/BasicBeanFilter.java new file mode 100644 index 00000000..35645721 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/BasicBeanFilter.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/ControllerFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/ControllerFilter.java new file mode 100644 index 00000000..3b6fdfa4 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/ControllerFilter.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/DataBaseEntityFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/DataBaseEntityFilter.java new file mode 100644 index 00000000..6befd9c8 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/DataBaseEntityFilter.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/MapperFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/MapperFilter.java new file mode 100644 index 00000000..0ecc20cb --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/MapperFilter.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/PluginConfigurationFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/PluginConfigurationFilter.java new file mode 100644 index 00000000..4354c223 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/PluginConfigurationFilter.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/WebSocketFilter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/WebSocketFilter.java new file mode 100644 index 00000000..d2e2713e --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/filter/impl/WebSocketFilter.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MapperHandler.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MapperHandler.java new file mode 100644 index 00000000..bd254e26 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MapperHandler.java @@ -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> 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; + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisCommonConfig.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisCommonConfig.java new file mode 100644 index 00000000..889f29f6 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisCommonConfig.java @@ -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 entityPackage(); + + /** + * mybatis xml mapper 匹配规则
+ * ? 匹配一个字符
+ * * 匹配零个或多个字符
+ * ** 匹配路径中的零或多个目录
+ * 例如:
+ * 文件路径配置为

file:D://xml/*PluginMapper.xml


+ * resources路径配置为

classpath:xml/mapper/*PluginMapper.xml


+ * 包路径配置为

package:com.plugin.xml.mapper.*PluginMapper.xml


+ * @return Set + */ + Set xmlLocationsMatch(); + + /** + * 插件是否自主启用配置. 默认进行禁用, 使用主程序的配置 + * @return 返回true, 表示进行插件自主进行Mybatis相关配置 + */ + default boolean enableOneselfConfig(){ + return false; + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusConfig.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusConfig.java new file mode 100644 index 00000000..0fbbfcc1 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusConfig.java @@ -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) { + + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusProcessor.java new file mode 100644 index 00000000..92a64f72 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/MybatisPlusProcessor.java @@ -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 getObjectByInterfaceClass(Set sourceObject, Class interfaceClass) { + if (sourceObject == null || sourceObject.isEmpty()) { + return null; + } + for (Object configSingletonObject : sourceObject) { + Set> allInterfacesForClassAsSet = ClassUtils + .getAllInterfacesAsSet(configSingletonObject); + if (allInterfacesForClassAsSet.contains(interfaceClass)) { + return (T) configSingletonObject; + } + } + return null; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginFollowCoreConfig.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginFollowCoreConfig.java new file mode 100644 index 00000000..d572eb7b --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginFollowCoreConfig.java @@ -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 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, Interceptor> interceptorMap = new HashMap<>(); + try { + SqlSessionFactory sqlSessionFactory = mainApplicationContext.getBean(SqlSessionFactory.class); + // 先从 SqlSessionFactory 工厂中获取拦截器 + List 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 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, 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, LanguageDriver> driverMap = null; + if (proxyTypesField != null) { + proxyTypesField.setAccessible(true); + driverMap = (Map, LanguageDriver>) proxyTypesField.get(languageRegistry); + } + if (driverMap != null) { + languageDriverMap.putAll(driverMap); + } + } catch (Exception e) { + // ignore + } + Map 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]); + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginResourceFinder.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginResourceFinder.java new file mode 100644 index 00000000..03447d42 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/mybatis/PluginResourceFinder.java @@ -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 xmlLocationsMatchSet) throws IOException { + if(xmlLocationsMatchSet == null || xmlLocationsMatchSet.isEmpty()){ + return null; + } + List resources = new ArrayList<>(); + for (String xmlLocationsMatch : xmlLocationsMatchSet) { + if(xmlLocationsMatchSet.isEmpty()){ + continue; + } + List 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 packagePatterns) throws IOException { + if(packagePatterns == null || packagePatterns.isEmpty()){ + return null; + } + Set> 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 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; + } + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/BaseServerEndpoint.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/BaseServerEndpoint.java new file mode 100644 index 00000000..4f1f93b0 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/BaseServerEndpoint.java @@ -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 { +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/WebSocketProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/WebSocketProcessor.java new file mode 100644 index 00000000..7a0de8b7 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/core/register/websocket/WebSocketProcessor.java @@ -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> 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 annotationsUpdater; + try { + InvocationHandler invocationHandler = Proxy.getInvocationHandler(serverEndpoint); + Field field = invocationHandler.getClass().getDeclaredField("memberValues"); + field.setAccessible(true); + annotationsUpdater = (Map) 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 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 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(); + } + + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginFacadeMemoryCache.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginFacadeMemoryCache.java new file mode 100644 index 00000000..a7663a5e --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginFacadeMemoryCache.java @@ -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 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 getAllPlugin() { + return Collections.unmodifiableMap(pluginFacadeMap); + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginInformationDao.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginInformationDao.java new file mode 100644 index 00000000..05ee1afb --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dao/PluginInformationDao.java @@ -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 { + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginInformation.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginInformation.java new file mode 100644 index 00000000..7eaa0a5c --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginInformation.java @@ -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; +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginSpecStorage.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginSpecStorage.java new file mode 100644 index 00000000..5287476a --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/dto/PluginSpecStorage.java @@ -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 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 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, + ; + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginFacade.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginFacade.java new file mode 100644 index 00000000..594f5f7b --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginFacade.java @@ -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; +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginInfo.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginInfo.java new file mode 100644 index 00000000..087dac2f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/PluginInfo.java @@ -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> classList; + private ApplicationContext mainApplicationContext; + private Boolean applicationContextIsRefresh = false; + private AnnotationConfigApplicationContext pluginApplicationContext; + private PluginWrapper pluginWrapper; + private List> adminGroupsClassList = new ArrayList<>(); + private List websocketPaths = new ArrayList<>(); + private String pluginId; + private String mapperXmlDir; + private final BasePlugin basePlugin; + private List handlerInterceptorList = new ArrayList<>(); + private Set staticClassPathLocations = new HashSet<>(); + private Set staticFileLocations = new HashSet<>(); + private List> controllers = new ArrayList<>(); + private Set pluginConfigObjects = new HashSet<>(); + private Map webSocketPathMap = new ConcurrentHashMap<>(); + // private ConcurrentHashMap, Object> beanCache = new ConcurrentHashMap<>(); + // TODO 这个map用于替代前面的ClassList + private Map>> 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> + * @description 获取插件内实现指定类的bean + * @author dolphin + */ + public T getPluginBean(Class 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> getGroupClass(String groupName) { + return classGroups.getOrDefault(groupName, Collections.emptyList()); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/SpringPlugin.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/SpringPlugin.java new file mode 100644 index 00000000..f96fe996 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/dataobject/pojo/SpringPlugin.java @@ -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 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; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginBeforeStopEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginBeforeStopEvent.java similarity index 76% rename from framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginBeforeStopEvent.java rename to framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginBeforeStopEvent.java index 6d940cc6..ac441dde 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginBeforeStopEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginBeforeStopEvent.java @@ -12,11 +12,12 @@ import org.springframework.util.Assert; * @Created by mianbin */ @Getter -public class PluginBeforeStopEvent extends ApplicationEvent { +public class OpsPluginBeforeStopEvent extends ApplicationEvent { + private final PluginWrapper plugin; - public PluginBeforeStopEvent(Object source, PluginWrapper plugin) { + public OpsPluginBeforeStopEvent(Object source, PluginWrapper plugin) { super(source); Assert.notNull(plugin, "插件不能为空"); this.plugin = plugin; diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStartedEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStartedEvent.java new file mode 100644 index 00000000..1b183f02 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStartedEvent.java @@ -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; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStoppedEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStoppedEvent.java similarity index 75% rename from framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStoppedEvent.java rename to framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStoppedEvent.java index 1f21f332..68ba684f 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStoppedEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/OpsPluginStoppedEvent.java @@ -11,11 +11,11 @@ import org.springframework.context.ApplicationEvent; * @Created by mianbin */ @Getter -public class PluginStoppedEvent extends ApplicationEvent { +public class OpsPluginStoppedEvent extends ApplicationEvent { private final PluginWrapper plugin; - public PluginStoppedEvent(Object source, PluginWrapper plugin) { + public OpsPluginStoppedEvent(Object source, PluginWrapper plugin) { super(source); this.plugin = plugin; } diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStartedEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStartedEvent.java index 998799ac..25634d57 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStartedEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/PluginStartedEvent.java @@ -1,22 +1,15 @@ 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 + * @description: 插件真正启动时发布的事件,仅供插件内部使用 + * @author: mianbin + * @date: 2025/5/12 16:31 + * @version: 1.0 */ public class PluginStartedEvent extends ApplicationEvent { - - private final PluginWrapper plugin; - - public PluginStartedEvent(Object source, PluginWrapper plugin) { + public PluginStartedEvent(Object source) { super(source); - Assert.notNull(plugin, "插件不能为空"); - this.plugin = plugin; } } diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartedEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartedEvent.java index bd86826d..19f57cc7 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartedEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartedEvent.java @@ -1,6 +1,6 @@ package cd.casic.plugin.event; -import cd.casic.plugin.SpringPlugin; +import cd.casic.plugin.dataobject.pojo.SpringPlugin; import lombok.Getter; import org.springframework.context.ApplicationEvent; diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartingEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartingEvent.java index 842b1093..32e3791e 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartingEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStartingEvent.java @@ -1,6 +1,6 @@ package cd.casic.plugin.event; -import cd.casic.plugin.SpringPlugin; +import cd.casic.plugin.dataobject.pojo.SpringPlugin; import lombok.Getter; import org.springframework.context.ApplicationEvent; diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppedEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppedEvent.java index 23e5100c..4214590e 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppedEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppedEvent.java @@ -1,6 +1,6 @@ package cd.casic.plugin.event; -import cd.casic.plugin.SpringPlugin; +import cd.casic.plugin.dataobject.pojo.SpringPlugin; import lombok.Getter; import org.springframework.context.ApplicationEvent; diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppingEvent.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppingEvent.java index 6cd0ad8d..f402706f 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppingEvent.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/event/SpringPluginStoppingEvent.java @@ -1,6 +1,6 @@ package cd.casic.plugin.event; -import cd.casic.plugin.SpringPlugin; +import cd.casic.plugin.dataobject.pojo.SpringPlugin; import lombok.Getter; import org.springframework.context.ApplicationEvent; diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/AccessDenyException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/AccessDenyException.java new file mode 100644 index 00000000..a5b43541 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/AccessDenyException.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginAlreadyExistsException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginAlreadyExistsException.java new file mode 100644 index 00000000..5c02d6a7 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginAlreadyExistsException.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependenciesNotEnabledException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependenciesNotEnabledException.java new file mode 100644 index 00000000..5b28813d --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependenciesNotEnabledException.java @@ -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 dependencies) { + super("Plugin dependencies are not fully enabled, please enable them first.", + null, + null, + null, + new Object[] {dependencies}); + setType(TYPE); + getBody().setProperty("dependencies", dependencies); + } + +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependencyException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependencyException.java new file mode 100644 index 00000000..31166f4f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependencyException.java @@ -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 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 versions) { + super("Dependencies have wrong version.", null, null, new Object[]{versions}); + setType(URI.create(TYPE)); + getBody().setProperty("versions", versions); + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependentsNotDisabledException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependentsNotDisabledException.java new file mode 100644 index 00000000..b692a0c0 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginDependentsNotDisabledException.java @@ -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 dependents) { + super("Plugin dependents are not fully disabled, please disable them first.", + null, + null, + null, + new Object[] {dependents}); + setType(TYPE); + getBody().setProperty("dependents", dependents); + } +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginInstallationException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginInstallationException.java new file mode 100644 index 00000000..4b52e5d1 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/PluginInstallationException.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/UnsatisfiedAttributeValueException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/UnsatisfiedAttributeValueException.java new file mode 100644 index 00000000..5f4a512f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/UnsatisfiedAttributeValueException.java @@ -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); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/YmalNotExistsException.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/YmalNotExistsException.java new file mode 100644 index 00000000..61a335c9 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/exception/YmalNotExistsException.java @@ -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 dependencies) { + super("plugin.yaml. are not exists, please checking checking.", + null, + null, + null, + new Object[]{dependencies}); + setType(TYPE); + getBody().setProperty("plugin.yaml", dependencies); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/AbstractExtension.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/AbstractExtension.java deleted file mode 100644 index 6984ebaa..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/AbstractExtension.java +++ /dev/null @@ -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; - } - -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Extension.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Extension.java deleted file mode 100644 index e542a18e..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Extension.java +++ /dev/null @@ -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{ - - @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()); - } - -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionClient.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionClient.java deleted file mode 100644 index de23378b..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionClient.java +++ /dev/null @@ -1,11 +0,0 @@ -package cd.casic.plugin.extension; - -/** - * @Classname ExtensionClient - * @Description 接口 - * @Date 2025/5/9 15:43 - * @Created by mianbin - */ -public interface ExtensionClient { - -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionMatcher.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionMatcher.java deleted file mode 100644 index 9eeea9f6..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionMatcher.java +++ /dev/null @@ -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); - -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionOperator.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionOperator.java deleted file mode 100644 index 44146c7e..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionOperator.java +++ /dev/null @@ -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 Predicate isNotDeleted() { - return ext -> ext.getMetadata().getDeletionTimestamp() == null; - } - - static boolean isDeleted(ExtensionOperator extension) { - return ExtensionUtil.isDeleted(extension); - } - -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionUtil.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionUtil.java deleted file mode 100644 index 6687cf38..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/ExtensionUtil.java +++ /dev/null @@ -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 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 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") - ); - } -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupKind.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupKind.java deleted file mode 100644 index 84b6de96..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupKind.java +++ /dev/null @@ -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) { -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersion.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersion.java deleted file mode 100644 index 892e93c3..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersion.java +++ /dev/null @@ -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); - }; - } -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersionKind.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersionKind.java deleted file mode 100644 index ec415786..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/GroupVersionKind.java +++ /dev/null @@ -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 GroupVersionKind fromExtension(Class 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; - } -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Metadata.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Metadata.java deleted file mode 100644 index df4bb9f3..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Metadata.java +++ /dev/null @@ -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 labels; - - private Map annotations; - - private Long version; - - private Instant creationTimestamp; - - private Instant deletionTimestamp; - - private Set finalizers; -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/MetadataOperator.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/MetadataOperator.java deleted file mode 100644 index 7dea10f1..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/MetadataOperator.java +++ /dev/null @@ -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 getLabels(); - - @Schema(name = "annotations") - @JsonProperty("annotations") - Map 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 getFinalizers(); - - void setName(String name); - - void setGenerateName(String generateName); - - void setLabels(Map labels); - - void setAnnotations(Map annotations); - - void setVersion(Long version); - - void setCreationTimestamp(Instant creationTimestamp); - - void setDeletionTimestamp(Instant deletionTimestamp); - - void setFinalizers(Set 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; - } -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Plugin.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Plugin.java deleted file mode 100644 index 4e033f42..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/extension/Plugin.java +++ /dev/null @@ -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 pluginDependencies = new HashMap<>(4); - - private String homepage; - - private String repo; - - private String issues; - - private String description; - - private List 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; - } -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Controller.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Controller.java deleted file mode 100644 index d1ca0c41..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Controller.java +++ /dev/null @@ -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(); - -} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/FinderRegistry.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/FinderRegistry.java new file mode 100644 index 00000000..0699d69e --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/FinderRegistry.java @@ -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 getFinders(); + + void register(String pluginId, ApplicationContext pluginContext); + + void unregister(String pluginId); +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginManager.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginManager.java new file mode 100644 index 00000000..75e11a94 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginManager.java @@ -0,0 +1,85 @@ +package cd.casic.plugin.function; + +import cd.casic.plugin.dataobject.pojo.PluginInfo; +import cd.casic.plugin.dataobject.dto.PluginInformation; +import org.pf4j.PluginState; +import org.pf4j.PluginWrapper; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; + +import java.nio.file.Path; +import java.util.List; + +/** + * @Classname PluginService + * @Description 插件服务类 + * @Date 2025/5/8 19:58 + * @Created by mianbin + */ +public interface IPluginManager { + + /** + * @param path 插件路径 + * @description 安装插件 + */ + PluginInfo install(Path path) throws Exception; + + /** + * 安装插件后置操作 + * + * @param pluginId pluginId + */ + void installAfter(String pluginId); + + /** + * @description 卸载插件 + */ + void unInstall(String pluginId, boolean isUpdate) throws Exception; + + /** + * 启动插件 + * + * @param pluginId + * @return + */ + PluginState startPlugin(String pluginId); + + /** + * 停止插件 + * + * @param pluginId pluginId + * @return PluginState + */ + PluginState stopPlugin(String pluginId); + + /** + * @description 插件初始化 + */ + void initPlugins(List plugins) throws Exception; + + /** + * @description 获取所有插件 + */ + List getInstallPlugins(); + + + PluginInfo upgrade(String name, Path path); + + /** + * 重新加载插件,插件spec.enabled 设置为 true + */ + PluginInfo reload(String name); + + DataBuffer uglifyJsBundle(); + + DataBuffer uglifyCssBundle(); + + String generateBundleVersion(); + + Resource getJsBundle(String version); + + Resource getCssBundle(String version); + + PluginInfo changeState(String pluginName, boolean requestToEnable, boolean wait); + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginProcessor.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginProcessor.java new file mode 100644 index 00000000..47f8c7ef --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/IPluginProcessor.java @@ -0,0 +1,34 @@ +package cd.casic.plugin.function; + +import cd.casic.plugin.dataobject.pojo.PluginInfo; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/22 9:44 + * @version: 1.0 + */ +public interface IPluginProcessor { + /** + * 插件组件初始化 + * + * @throws Exception + */ + void initialize() throws Exception; + + /** + * 插件组件注册 + * + * @param plugin + * @throws Exception + */ + void registry(PluginInfo plugin) throws Exception; + + /** + * 插件组件卸载注册 + * + * @param plugin + * @throws Exception + */ + void unRegistry(PluginInfo plugin) throws Exception; +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginGetter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginGetter.java similarity index 75% rename from framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginGetter.java rename to framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginGetter.java index 63172273..0c3efcab 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/PluginGetter.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginGetter.java @@ -1,6 +1,6 @@ -package cd.casic.plugin; +package cd.casic.plugin.function; -import cd.casic.plugin.extension.Plugin; +import org.pf4j.Plugin; /** * @Classname PluginGetter diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginLifecycle.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginLifecycle.java new file mode 100644 index 00000000..bbb186c1 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginLifecycle.java @@ -0,0 +1,20 @@ +package cd.casic.plugin.function; + +/** + * @description: 启动前后 + * @author: mianbin + * @date: 2025/5/19 15:07 + * @version: 1.0 + */ +public interface PluginLifecycle { + /** + * 插件启动前的操作,比如需要单独对插件进行某些特定操作, + */ + public void beforeWork(); + + /** + * 料理后世的操作 + */ + public void AfterWork(); + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginsRootGetter.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginsRootGetter.java index 1c72ecbe..63a29479 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginsRootGetter.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/PluginsRootGetter.java @@ -1,6 +1,5 @@ package cd.casic.plugin.function; -import java.nio.file.Path; import java.util.function.Supplier; /** @@ -9,5 +8,5 @@ import java.util.function.Supplier; * @Date 2025/5/9 14:34 * @Created by mianbin */ -public interface PluginsRootGetter extends Supplier { +public interface PluginsRootGetter extends Supplier { } diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Reconciler.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Reconciler.java deleted file mode 100644 index 84b1525e..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Reconciler.java +++ /dev/null @@ -1,29 +0,0 @@ -package cd.casic.plugin.function; - -import java.time.Duration; - -/** - * @Classname Reconciler - * @Description TODO - * @Date 2025/5/9 15:54 - * @Created by mianbin - */ -public interface Reconciler { - - Result reconcile(R request); - - record Request(String name) { - } - - record Result(boolean reEnqueue, Duration retryAfter) { - - public static Result doNotRetry() { - return new Result(false, null); - } - - public static Result requeue(Duration retryAfter) { - return new Result(true, retryAfter); - } - } -} - diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Watcher.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Watcher.java deleted file mode 100644 index 67982e50..00000000 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/Watcher.java +++ /dev/null @@ -1,97 +0,0 @@ -package cd.casic.plugin.function; - -import cd.casic.plugin.extension.Extension; -import reactor.core.Disposable; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * @Classname Watcher - * @Description TODO - * @Date 2025/5/9 15:54 - * @Created by mianbin - */ -public interface Watcher extends Disposable { - - default void onAdd(Reconciler.Request request) { - // Do nothing here, just for sync all on start. - } - - default void onAdd(Extension extension) { - // Do nothing here - } - - default void onUpdate(Extension oldExtension, Extension newExtension) { - // Do nothing here - } - - default void onDelete(Extension extension) { - // Do nothing here - } - - default void registerDisposeHook(Runnable dispose) { - } - - class WatcherComposite implements Watcher { - - private final List watchers; - - private volatile boolean disposed = false; - - private Runnable disposeHook; - - public WatcherComposite() { - watchers = new CopyOnWriteArrayList<>(); - } - - @Override - public void onAdd(Extension extension) { - // TODO Deep copy extension and execute onAdd asynchronously - watchers.forEach(watcher -> watcher.onAdd(extension)); - } - - @Override - public void onUpdate(Extension oldExtension, Extension newExtension) { - // TODO Deep copy extension and execute onUpdate asynchronously - watchers.forEach(watcher -> watcher.onUpdate(oldExtension, newExtension)); - } - - @Override - public void onDelete(Extension extension) { - // TODO Deep copy extension and execute onDelete asynchronously - watchers.forEach(watcher -> watcher.onDelete(extension)); - } - - public void addWatcher(Watcher watcher) { - if (!watcher.isDisposed() && !watchers.contains(watcher)) { - watchers.add(watcher); - watcher.registerDisposeHook(() -> removeWatcher(watcher)); - } - } - - public void removeWatcher(Watcher watcher) { - watchers.remove(watcher); - } - - @Override - public void registerDisposeHook(Runnable dispose) { - this.disposeHook = dispose; - } - - @Override - public void dispose() { - this.disposed = true; - this.watchers.clear(); - if (this.disposeHook != null) { - this.disposeHook.run(); - } - } - - @Override - public boolean isDisposed() { - return this.disposed; - } - } -} - diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/WebSocketEndpointManager.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/WebSocketEndpointManager.java new file mode 100644 index 00000000..f57c7086 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/function/WebSocketEndpointManager.java @@ -0,0 +1,15 @@ +package cd.casic.plugin.function; + +import java.util.Collection; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/13 10:42 + * @version: 1.0 + */ +public interface WebSocketEndpointManager { + void register(Collection endpoints); + + void unregister(Collection endpoints); +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/AnnotationUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/AnnotationUtils.java new file mode 100644 index 00000000..f321f52d --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/AnnotationUtils.java @@ -0,0 +1,21 @@ +package cd.casic.plugin.utils; + +import java.lang.annotation.Annotation; + +public class AnnotationUtils { + @SafeVarargs + public static boolean hasAnnotations(Class clazz, + boolean allRequired, + Class... annotations) { + if (clazz == null) return false; + if (annotations == null) return false; + for (Class annotation : annotations) { + if (clazz.isAnnotationPresent(annotation)) { + if (!allRequired) return true; + } else { + if (allRequired) return false; + } + } + return allRequired; + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/BundleResourceUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/BundleResourceUtils.java new file mode 100644 index 00000000..4d12a28d --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/BundleResourceUtils.java @@ -0,0 +1,57 @@ +package cd.casic.plugin.utils; + +import cd.casic.framework.commons.util.io.FileUtils; +import org.pf4j.PluginManager; +import org.pf4j.PluginWrapper; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.annotation.Nullable; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/14 11:01 + * @version: 1.0 + */ +public abstract class BundleResourceUtils { + private static final String CONSOLE_BUNDLE_LOCATION = "console"; + public static final String JS_BUNDLE = "main.js"; + public static final String CSS_BUNDLE = "style.css"; + + /** + * Gets js bundle resource by plugin name in console location. + * + * @return js bundle resource if exists, otherwise null + */ + @Nullable + public static Resource getJsBundleResource(PluginManager pluginManager, String pluginName, + String bundleName) { + Assert.hasText(pluginName, "The pluginName must not be blank"); + Assert.hasText(bundleName, "Bundle name must not be blank"); + + DefaultResourceLoader resourceLoader = getResourceLoader(pluginManager, pluginName); + if (resourceLoader == null) { + return null; + } + String path = PathUtils.combinePath(CONSOLE_BUNDLE_LOCATION, bundleName); + String simplifyPath = StringUtils.cleanPath(path); + FileUtils.checkDirectoryTraversal("/" + CONSOLE_BUNDLE_LOCATION, simplifyPath); + Resource resource = resourceLoader.getResource(simplifyPath); + return resource.exists() ? resource : null; + } + + @Nullable + public static DefaultResourceLoader getResourceLoader(PluginManager pluginManager, + String pluginName) { + Assert.notNull(pluginManager, "Plugin manager must not be null"); + PluginWrapper plugin = pluginManager.getPlugin(pluginName); + if (plugin == null) { + return null; + } + return new DefaultResourceLoader(plugin.getPluginClassLoader()); + } +} + diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/CommonUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/CommonUtils.java new file mode 100644 index 00000000..4c614bb6 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/CommonUtils.java @@ -0,0 +1,152 @@ +package cd.casic.plugin.utils; + +import cd.casic.plugin.config.PluginProperties; +import cn.hutool.core.util.StrUtil; +import org.pf4j.util.StringUtils; + +import java.io.File; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +/** + * 通用工具 + */ +public class CommonUtils { + + private CommonUtils() { + } + + /** + * list按照int排序. 数字越大, 越排在前面 + * + * @param list list集合 + * @param orderImpl 排序实现 + * @param T + * @return List + */ + public static List order(List list, Function orderImpl) { + if (list == null) { + return null; + } + list.sort(Comparator.comparing(orderImpl, Comparator.nullsLast(Comparator.reverseOrder()))); + return list; + } + + + /** + * 得到插件接口前缀 + * + * @param configuration 配置 + * @param pluginId 插件id + * @return 接口前缀 + */ + public static String getPluginRestPrefix(PluginProperties configuration, String pluginId) { + String pathPrefix = configuration.getRestPathPrefix(); + if (configuration.isEnable()) { + if (StrUtil.isNotEmpty(pathPrefix)) { + pathPrefix = restJoiningPath(pathPrefix, pluginId); + } else { + pathPrefix = pluginId; + } + return pathPrefix; + } else { + if (StrUtil.isEmpty(pathPrefix)) { + // 不启用插件id作为路径前缀, 并且路径前缀为空, 则直接返回。 + return null; + } + } + return pathPrefix; + } + + + /** + * rest接口拼接路径 + * + * @param path1 路径1 + * @param path2 路径2 + * @return 拼接的路径 + */ + public static String restJoiningPath(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 ""; + } + } + + + /** + * 拼接url路径 + * + * @param paths 拼接的路径 + * @return 拼接的路径 + */ + public static String joiningPath(String... paths) { + if (paths == null || paths.length == 0) { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + int length = paths.length; + for (int i = 0; i < length; i++) { + String path = paths[i]; + if (StringUtils.isNullOrEmpty(path)) { + continue; + } + if ((i < length - 1) && path.endsWith("/")) { + path = path.substring(path.lastIndexOf("/")); + } + if (path.startsWith("/")) { + stringBuilder.append(path); + } else { + stringBuilder.append("/").append(path); + } + } + + return stringBuilder.toString(); + } + + /** + * 拼接file路径 + * + * @param paths 拼接的路径 + * @return 拼接的路径 + */ + public static String joiningFilePath(String... paths) { + if (paths == null || paths.length == 0) { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + int length = paths.length; + for (int i = 0; i < length; i++) { + String path = paths[i]; + if (StringUtils.isNullOrEmpty(path)) { + continue; + } + if (i > 0) { + if (path.startsWith(File.separator) || path.startsWith("/") || + path.startsWith("\\") || path.startsWith("//")) { + stringBuilder.append(path); + } else { + stringBuilder.append(File.separator).append(path); + } + } else { + stringBuilder.append(path); + } + } + + return stringBuilder.toString(); + } + + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ConfigFileUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ConfigFileUtils.java new file mode 100644 index 00000000..3791b755 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ConfigFileUtils.java @@ -0,0 +1,43 @@ +package cd.casic.plugin.utils; + +import org.pf4j.util.StringUtils; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/21 19:45 + * @version: 1.0 + */ +public class ConfigFileUtils { + /** + * 拼接插件独立配置文件的文件名和环境后缀 + * + * @param fileName 插件独立配置文件文件名 + * @param suffix 配置文件环境后缀,PluginConfiguration中定义 + * @return 拼接后的文件名 + */ + public static String joinConfigFileName(String fileName, String suffix) { + // 若文件名为空,直接返回 null + if (StringUtils.isNullOrEmpty(fileName)) { + return null; + } + + // 处理后缀为空的情况 + if (suffix == null) { + suffix = ""; + } + // 若后缀非空且不以 - 开头,添加 - 前缀 + if (StringUtils.isNotNullOrEmpty(suffix) && !suffix.startsWith("-")) { + suffix = "-" + suffix; + } + + // 查找文件名中最后一个 . 的位置 + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex == -1) { + // 若没有 .,直接拼接文件名和后缀 + return fileName + suffix; + } + // 分割文件名和扩展名并拼接后缀 + return fileName.substring(0, dotIndex) + suffix + fileName.substring(dotIndex); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PathUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PathUtils.java new file mode 100644 index 00000000..12b8859f --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PathUtils.java @@ -0,0 +1,151 @@ +package cd.casic.plugin.utils; + +import cn.hutool.core.io.file.PathUtil; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +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.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/14 11:10 + * @version: 1.0 + */ +@Slf4j +@UtilityClass +public class PathUtils extends PathUtil { + + /** + * Every HTTP URL conforms to the syntax of a generic URI. The URI generic syntax consists of + * components organized hierarchically in order of decreasing significance from left to + * right: + *
+     * URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
+     * 
+ * The authority component consists of subcomponents: + *
+     * authority = [userinfo "@"] host [":" port]
+     * 
+ * Examples of popular schemes include http, https, ftp, mailto, file, data and irc. URI + * schemes should be registered with the + * Internet Assigned Numbers Authority (IANA), although + * non-registered schemes are used in practice. + * + * @param uriString url or path + * @return true if the linkBase is absolute, otherwise false + * @see URL + */ + public static boolean isAbsoluteUri(final String uriString) { + if (StringUtils.isBlank(uriString)) { + return false; + } + try { + URI uri = new URI(uriString); + return uri.isAbsolute(); + } catch (URISyntaxException e) { + log.debug("Failed to parse uri: " + uriString, e); + return false; + } + } + + /** + * Combine paths based on the passed in path segments parameters. + *

+ * This method doesn't work for Windows system currently. + * + * @param pathSegments Path segments to be combined + * @return the combined path + */ + public static String combinePath(String... pathSegments) { + StringBuilder sb = new StringBuilder(); + for (String path : pathSegments) { + if (path == null) { + continue; + } + String s = path.startsWith("/") ? path : "/" + path; + String segment = s.endsWith("/") ? s.substring(0, s.length() - 1) : s; + sb.append(segment); + } + return sb.toString(); + } + + + /** + *

Append a {@code '/'} if the path does not end with a {@code '/'}.

+ * Examples are as follows: + *
+     *     PathUtils.appendPathSeparatorIfMissing("hello") -> hello/
+     *     PathUtils.appendPathSeparatorIfMissing("some-path/") -> some-path/
+     *     PathUtils.appendPathSeparatorIfMissing(null) -> null
+     * 
+ * + * @param path a path + * @return A new String if suffix was appended, the same string otherwise. + */ + public static String appendPathSeparatorIfMissing(String path) { + return StringUtils.appendIfMissing(path, "/", "/"); + } + + /** + *

Remove the regex in the path pattern placeholder.

+ *

For example:

+ *
    + *
  • '{@code /{year:\d{4}}/{month:\d{2}}}' → '{@code /{year}/{month}}'
  • + *
  • '{@code /archives/{year:\d{4}}/{month:\d{2}}}' → '{@code /archives/{year}/{month} + * }'
  • + *
  • '{@code /archives/{year:\d{4}}/{slug}}' → '{@code /archives/{year}/{slug}}'
  • + *
+ * + * @param pattern path pattern + * @return Simplified path pattern + */ + public static String simplifyPathPattern(String pattern) { + if (StringUtils.isBlank(pattern)) { + return StringUtils.EMPTY; + } + String[] parts = StringUtils.split(pattern, '/'); + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + if (part.startsWith("{") && part.endsWith("}")) { + int colonIdx = part.indexOf(':'); + if (colonIdx != -1) { + parts[i] = part.substring(0, colonIdx) + part.charAt(part.length() - 1); + } + + } + } + return combinePath(parts); + } + + public Path getYamlPath(Path pluginPath, String yamlFileName) { + final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath(); + if (Files.isDirectory(pluginPath)) { + for (String location : PLUGIN_CLASSPATH.getClassesDirectories()) { + Path path = pluginPath.resolve(location).resolve(yamlFileName); + Resource propertyResource = new FileSystemResource(path); + if (propertyResource.exists()) { + return path; + } + } + throw new PluginRuntimeException( + "Unable to find plugin yaml file: " + yamlFileName); + } else { + try { + return FileUtils.getPath(pluginPath, yamlFileName); + } catch (IOException e) { + throw new PluginRuntimeException(e); + } + } + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginDescriptorUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginDescriptorUtils.java new file mode 100644 index 00000000..7ae202ac --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginDescriptorUtils.java @@ -0,0 +1,108 @@ +package cd.casic.plugin.utils; + +import cd.casic.plugin.OpsPluginDescriptor; +import cd.casic.plugin.dataobject.dto.PluginSpecStorage; +import cn.hutool.core.util.StrUtil; +import org.pf4j.DefaultPluginDescriptor; +import org.pf4j.PluginDependency; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description: 重构插件的信息 + * @author: mianbin + * @date: 2025/5/19 17:07 + * @version: 1.0 + */ +public class PluginDescriptorUtils { + public static final String PLUGIN_ID = "plugin.id"; + public static final String PLUGIN_DESCRIPTION = "plugin.description"; + public static final String PLUGIN_CLASS = "plugin.class"; + public static final String PLUGIN_VERSION = "plugin.version"; + public static final String PLUGIN_PROVIDER = "plugin.provider"; + public static final String PLUGIN_DEPENDENCIES = "plugin.dependencies"; + public static final String PLUGIN_REQUIRES = "plugin.requires"; + public static final String PLUGIN_LICENSE = "plugin.license"; + public static final String MYBATIS_MAPPER_LOCATION = "mybatis.mapper.location"; + public static final String STATIC_LOCATIONS = "static.locations"; + public static final String CONFIG_FILE_NAME = "plugin.config.file"; + public static final String CONFIG_FILE_ACTIVE = "plugin.config.active"; + + public static PluginSpecStorage propertiesToStorage(Properties properties) { + PluginSpecStorage pluginDescriptor = new PluginSpecStorage(); + // 设置插件 ID + pluginDescriptor.setPluginId(properties.getProperty(PLUGIN_ID)); + // 设置插件描述 + pluginDescriptor.setPluginDescription(StrUtil.emptyToDefault(properties.getProperty(PLUGIN_DESCRIPTION), "")); + // 设置插件类 + setIfNotEmpty(pluginDescriptor::setPluginClass, properties.getProperty(PLUGIN_CLASS)); + // 设置插件版本 + setIfNotEmpty(pluginDescriptor::setVersion, properties.getProperty(PLUGIN_VERSION)); + // 设置插件提供者 + pluginDescriptor.setProvider(properties.getProperty(PLUGIN_PROVIDER)); + // 设置插件依赖要求 + pluginDescriptor.setRequires(StrUtil.emptyToDefault(properties.getProperty(PLUGIN_REQUIRES), "0.0.0")); + // 设置 MyBatis Mapper 位置 + pluginDescriptor.setMapperXmlDir(properties.getProperty(MYBATIS_MAPPER_LOCATION)); + // 设置静态资源位置 + pluginDescriptor.setStaticDir(properties.getProperty(STATIC_LOCATIONS)); + // 设置插件许可证 + pluginDescriptor.setLicense(properties.getProperty(PLUGIN_LICENSE)); + // 设置插件依赖列表 + String dependencies = properties.getProperty(PLUGIN_DEPENDENCIES); + pluginDescriptor.setDependencies(StrUtil.isEmpty(dependencies) ? + Collections.emptyList() : + Arrays.stream(dependencies.trim().split(",")) + .map(dependency -> new PluginDependency(dependency.trim())) + .collect(Collectors.toList())); + // 设置配置文件名 + setIfNotEmpty(pluginDescriptor::setConfigFileName, properties.getProperty(CONFIG_FILE_NAME)); + // 设置配置文件激活列表 + List configFileActives = properties.stringPropertyNames().stream() + .filter(key -> key.startsWith(CONFIG_FILE_ACTIVE)) + .map(properties::getProperty) + .collect(Collectors.toList()); + pluginDescriptor.setConfigFileActive(configFileActives.isEmpty() ? + Collections.emptyList() : + configFileActives); + return pluginDescriptor; + } + + public static DefaultPluginDescriptor storageDefaultPluginDescriptor(PluginSpecStorage pluginSpectStorage) { + return new DefaultPluginDescriptor(pluginSpectStorage.getPluginId(), + pluginSpectStorage.getPluginDescription(), + pluginSpectStorage.getPluginClass(), + pluginSpectStorage.getVersion(), + pluginSpectStorage.getRequires(), + pluginSpectStorage.getProvider(), + pluginSpectStorage.getLicense()); + } + + public static OpsPluginDescriptor storageOpsPluginDescriptor(PluginSpecStorage pluginSpectStorage) { + return new OpsPluginDescriptor(pluginSpectStorage.getPluginId(), + pluginSpectStorage.getPluginDescription(), + pluginSpectStorage.getPluginClass(), + pluginSpectStorage.getVersion(), + pluginSpectStorage.getRequires(), + pluginSpectStorage.getProvider(), + pluginSpectStorage.getLicense(), + pluginSpectStorage.getConfigFileName(), + pluginSpectStorage.getConfigFileActive()); + } + + /** + * 如果值不为空,则调用 setter 方法设置值。 + * + * @param setter 设置器方法引用 + * @param value 要设置的值 + */ + private static void setIfNotEmpty(PdcConsumer setter, String value) { + Optional.of(value).ifPresent(setter::accept); + } + + @FunctionalInterface + private interface PdcConsumer { + void accept(T t); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginsUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginsUtils.java new file mode 100644 index 00000000..b93f9db0 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/PluginsUtils.java @@ -0,0 +1,87 @@ +package cd.casic.plugin.utils; + + +import cd.casic.plugin.dataobject.dao.PluginFacadeMemoryCache; +import cd.casic.plugin.dataobject.pojo.PluginFacade; +import cd.casic.plugin.dataobject.pojo.PluginInfo; +import cd.casic.plugin.exception.YmalNotExistsException; +import cn.hutool.core.util.StrUtil; +import cn.hutool.setting.dialect.Props; +import org.pf4j.PluginState; +import org.pf4j.PluginWrapper; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +/** + * @Author:mianbin + * @Package:cd.casic.plugin.utils + * @Project:ops + * @name:PluginsUtils + * @Date:2024/03/18 10:19 + * @Filename:PluginsUtils + * @description:插件工具类 + */ +public class PluginsUtils { + + public static Props getSetting(String pluginId) throws Exception { + Optional.of(PluginFacadeMemoryCache.getPluginSepc(pluginId)).map(descriptor -> { + Props props = new Props(); + if (!StrUtil.isEmptyIfStr(descriptor.getMapperXmlDir())) { + props.put("mybatis.mapper.location", descriptor.getMapperXmlDir()); + } + if (!StrUtil.isEmptyIfStr(descriptor.getStaticDir())) { + props.put("static.locations", descriptor.getStaticDir()); + } + return props; + }) + .orElseThrow(() -> new YmalNotExistsException(List.of("配置文件读取失败"))); + return new Props(); + } + + public static List getAllPluginProxyClass(Class clazz) { + List result = new ArrayList<>(); + Map allPlugin = PluginFacadeMemoryCache.getAllPlugin(); + for (String key : allPlugin.keySet()) { + PluginInfo pluginInfo = allPlugin.get(key).getPluginInfo(); + if (!pluginInfo.getPluginWrapper().getPluginState().equals(PluginState.STARTED)) { + continue; + } + Optional.of(pluginInfo.getPluginApplicationContext().getBean(clazz)).ifPresent(result::add); + } + return result; + } + + /** + * 扫描插件所在包,获取插件中所有的类名 + * + * @param basePackage 插件所在包名 + * @param pluginWrapper 插件对象包装类 + * @return 所有的类名集合 + */ + public static Set scanClassPackageName(String basePackage, + PluginWrapper pluginWrapper) throws IOException { + Path pluginPath = pluginWrapper.getPluginPath(); + if (pluginPath == null || !Files.exists(pluginPath)) { + throw new RuntimeException("错误的插件路径"); + } + File pluginFile = pluginPath.toFile(); + Set classPackageNames = new HashSet<>(); + try (JarFile jar = new JarFile(pluginFile)) { + // 使用 Enumeration 转 Stream 工具方法将 jarEntries 转换为 Stream + return Collections.list(jar.entries()).stream() + .map(JarEntry::getName) + // 过滤出 .class 文件且包名以 basePackage 开头的条目 + .filter(jarEntryName -> jarEntryName.contains(".class") && jarEntryName.replaceAll("/", ".").startsWith(basePackage)) + .map(jarEntryName -> jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replace("/", ".")) + .collect(Collectors.toSet()); + } + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ResourceUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ResourceUtils.java new file mode 100644 index 00000000..f7938e56 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/ResourceUtils.java @@ -0,0 +1,92 @@ +package cd.casic.plugin.utils; + +import cd.casic.plugin.dataobject.pojo.PluginInfo; +import org.pf4j.PluginWrapper; +import org.pf4j.RuntimeMode; +import org.pf4j.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 对资源解析的工具类 + */ +public class ResourceUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(ResourceUtils.class); + + public final static String TYPE_FILE = "file"; + public final static String TYPE_CLASSPATH = "classpath"; + public final static String TYPE_PACKAGE = "package"; + + public static final String ROOT_PLUGIN_SIGN = "~"; + + public final static String TYPE_SPLIT = ":"; + + /** + * 获取匹配路绝 + * @param locationMatch 原始匹配路径。规则为: file:xxx, classpath:xxx , package:xxx + * @return 整合出完整的匹配路绝 + */ + public static String getMatchLocation(String locationMatch){ + if(StringUtils.isNullOrEmpty(locationMatch)){ + return null; + } + String classPathType = TYPE_CLASSPATH + TYPE_SPLIT; + if(isClasspath(locationMatch)){ + return locationMatch.replaceFirst(classPathType, ""); + } + String fileType = TYPE_FILE + TYPE_SPLIT; + if(isFile(locationMatch)){ + return locationMatch.replaceFirst(fileType, ""); + } + String packageType = TYPE_PACKAGE + TYPE_SPLIT; + if(isPackage(locationMatch)){ + String location = locationMatch.replaceFirst(packageType, ""); + return location.replace(".", "/"); + } + LOGGER.error("locationMatch {} illegal", locationMatch); + return null; + } + + public static boolean isClasspath(String locationMatch){ + return locationMatch.startsWith(TYPE_CLASSPATH + TYPE_SPLIT); + } + + public static boolean isFile(String locationMatch){ + return locationMatch.startsWith(TYPE_FILE + TYPE_SPLIT); + } + + public static boolean isPackage(String locationMatch){ + return locationMatch.startsWith(TYPE_PACKAGE + TYPE_SPLIT); + } + + /** + * 根据 ~ 标记获取, 得到绝对路径 + * @param pluginRegistryInfo pluginRegistryInfo + * @param rootDir 根目录 + * @return java.lang.String + **/ + public static String getAbsolutePath(PluginInfo pluginRegistryInfo, String rootDir){ + if(StringUtils.isNullOrEmpty(rootDir)){ + return rootDir; + } + String home = null; + if(rootDir.startsWith(ResourceUtils.ROOT_PLUGIN_SIGN)){ + String pluginRootDir; + PluginWrapper pluginWrapper = pluginRegistryInfo.getPluginWrapper(); + RuntimeMode runtimeMode = pluginWrapper.getRuntimeMode(); + if(runtimeMode == RuntimeMode.DEVELOPMENT){ + pluginRootDir = pluginWrapper.getPluginPath().toString(); + } else { + pluginRootDir = System.getProperty("user.dir"); + } + // 如果root路径中开始存在ROOT_PLUGIN_SIGN,则说明进行插件根路替换 + home = rootDir.replaceFirst("\\" + ResourceUtils.ROOT_PLUGIN_SIGN, ""); + home = CommonUtils.joiningFilePath(pluginRootDir, home); + } else { + home = rootDir; + } + return home; + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/VersionUtils.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/VersionUtils.java similarity index 97% rename from framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/VersionUtils.java rename to framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/VersionUtils.java index 70d18d69..f782f009 100644 --- a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/VersionUtils.java +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/VersionUtils.java @@ -1,4 +1,4 @@ -package cd.casic.plugin; +package cd.casic.plugin.utils; import com.github.zafarkhaja.semver.Version; import lombok.experimental.UtilityClass; diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/YamlUnstructuredLoader.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/YamlUnstructuredLoader.java new file mode 100644 index 00000000..1ce5ef5e --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/YamlUnstructuredLoader.java @@ -0,0 +1,41 @@ +package cd.casic.plugin.utils; + +import cd.casic.plugin.dataobject.dto.PluginSpecStorage; +import org.springframework.beans.factory.config.YamlProcessor; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/12 19:33 + * @version: 1.0 + */ +@Component +public class YamlUnstructuredLoader extends YamlProcessor { + private static final DocumentMatcher DEFAULT_UNSTRUCTURED_MATCHER = properties -> { + if (properties.containsKey("id") + && properties.containsKey("version") + && (properties.containsKey("class") + || properties.containsKey("metadata"))) { + return YamlProcessor.MatchStatus.FOUND; + } + return MatchStatus.NOT_FOUND; + }; + + public YamlUnstructuredLoader(Resource... resources) { + setResources(resources); + setDocumentMatchers(DEFAULT_UNSTRUCTURED_MATCHER); + } + + public PluginSpecStorage load() { + List pluginDescriptor = new ArrayList<>(); + this.process(((properties, map) -> { + pluginDescriptor.add(PluginDescriptorUtils.propertiesToStorage(properties)); + })); + return pluginDescriptor.get(0); + } +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/AnnotationScanner.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/AnnotationScanner.java new file mode 100644 index 00000000..632c9b19 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/AnnotationScanner.java @@ -0,0 +1,72 @@ +package cd.casic.plugin.utils.dBUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class AnnotationScanner { + + public static List> scanPackageForAnnotatedClasses(String packageName, Class annotationClass, ClassLoader classLoader) throws IOException, ClassNotFoundException { + List> annotatedClasses = new ArrayList<>(); + // 获取包所在的URL + Enumeration urls = classLoader.getResources(packageName.replace('.', '/')); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + // 处理文件系统中的包 + File packageDir = new File(url.getFile()); + scanDirectoryForAnnotatedClasses(packageDir, packageName, annotationClass, annotatedClasses); + } else if ("jar".equals(protocol)) { + // 处理JAR文件中的包 + JarFile jarFile = ((JarURLConnection) url.openConnection()).getJarFile(); + scanJarFileForAnnotatedClasses(jarFile, packageName, annotationClass, annotatedClasses, classLoader); + } + } + return annotatedClasses; + } + + private static void scanDirectoryForAnnotatedClasses(File directory, String packageName, Class annotationClass, List> annotatedClasses) throws ClassNotFoundException { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + scanDirectoryForAnnotatedClasses(file, packageName + "." + file.getName(), annotationClass, annotatedClasses); + } else if (file.isFile() && file.getName().endsWith(".class")) { + String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6); + Class clazz = Class.forName(className); + if (clazz.isAnnotationPresent(annotationClass)) { + annotatedClasses.add(clazz); + } + } + } + } + } + + private static void scanJarFileForAnnotatedClasses(JarFile jarFile, String packageName, Class annotationClass, List> annotatedClasses, ClassLoader classLoader) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entryName.endsWith(".class") && entryName.startsWith(packageName.replace('.', '/')) && !entryName.contains("$")) { // 忽略内部类 + String className = entryName.substring(0, entryName.length() - 6).replace('/', '.'); + try { + Class clazz = classLoader.loadClass(className); + if (clazz.isAnnotationPresent(annotationClass)) { + annotatedClasses.add(clazz); + } + } catch (Exception e) { + System.err.println("Class not found: " + className); + ; + } + } + } + } +} \ No newline at end of file diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/SQLGenerator.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/SQLGenerator.java new file mode 100644 index 00000000..3eb4d82e --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/SQLGenerator.java @@ -0,0 +1,225 @@ +package cd.casic.plugin.utils.dBUtils; + +import cd.casic.plugin.core.register.database.DBEnums; +import cd.casic.plugin.core.register.database.DataBaseProperty; +import cd.casic.plugin.utils.dBUtils.annotation.Entity; +import cd.casic.plugin.utils.dBUtils.annotation.SQL; +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.mysql.cj.jdbc.MysqlDataSource; +import jakarta.annotation.Resource; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.stereotype.Component; +import org.sqlite.SQLiteDataSource; + +import javax.sql.DataSource; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +/** + * @description: TODO + * @author: mianbin + * @date: 2025/5/22 17:09 + * @version: 1.0 + */ +@Data +@Slf4j +@Component +public class SQLGenerator { + + @Resource + private DynamicRoutingDataSource dynamicRoutingDataSource; + + private String mapToSqlType(Class fieldType) { + // 简化的类型映射,针对MySQL数据库 + if (fieldType == int.class || fieldType == Integer.class) { + return "INTEGER"; + } else if (fieldType == long.class || fieldType == Long.class) { + return "BIGINT"; + } else if (fieldType == float.class || fieldType == Float.class) { + return "FLOAT"; + } else if (fieldType == double.class || fieldType == Double.class) { + return "DOUBLE"; + } else if (fieldType == boolean.class || fieldType == Boolean.class) { + return "BOOLEAN"; // 或者 "TINYINT(1)" + } else if (fieldType == String.class) { + return "VARCHAR(255)"; + } else if (fieldType == java.util.Date.class || fieldType == java.sql.Date.class) { + return "DATETIME"; // 或者 "TIMESTAMP" + } else if (fieldType == java.sql.Timestamp.class) { + return "TIMESTAMP"; + } else if (fieldType == byte[].class) { + return "BLOB"; + } else if (fieldType.isEnum()) { + return "ENUM"; // 需要进一步处理,如列出所有枚举值 + } else if (fieldType.isArray() || Collection.class.isAssignableFrom(fieldType)) { + throw new IllegalArgumentException("Arrays and Collections are not directly supported. Consider using a join table or serialization."); + } else { + throw new IllegalArgumentException("Unsupported field type: " + fieldType.getName()); + } + } + + public void sqlDeleteGenerator(Class c, DataBaseProperty dataBaseProperty, String pluginId) { + StringBuilder sql = new StringBuilder("\n"); + String tableName = "plugin_" + (c.toString().substring(c.toString().lastIndexOf(".") + 1)).toLowerCase(); + + sql.append("drop table if exists ").append(tableName); + String dbPath = "ops-module-plugins/plugins/" + pluginId + "/datasource/database.db"; + + this.executeSql(sql.toString(), dataBaseProperty, pluginId); + log.info(sql.toString()); + } + + public void sqlGenerator(Class c, DataBaseProperty dataBaseProperty, String pluginId) { + String dbPath = "ops-module-plugins/plugins/" + pluginId + "/datasource/database.db"; + Entity entityAnnotation = c.getAnnotation(Entity.class); + if (ObjectUtils.isEmpty(entityAnnotation)) { + return; + } + StringBuilder sql = new StringBuilder("\n"); + String tableName = "plugin_" + (c.toString().substring(c.toString().lastIndexOf(".") + 1)).toLowerCase(); + + sql.append("create table ").append(tableName).append("(").append("\n"); + int count = c.getDeclaredFields().length; + int index = 0; + boolean pkFlag = false; + for (Field field : c.getDeclaredFields()) { + SQL annotation = field.getAnnotation(SQL.class); + index += 1; + sql.append(field.getName()).append(" ").append(mapToSqlType(field.getType())).append(" "); + if (ObjectUtils.isNotEmpty(annotation) && ObjectUtils.isNotEmpty(annotation.defaultValue())) { + sql.append(" default " + "'").append(annotation.defaultValue()).append("'"); + } + if (ObjectUtils.isNotEmpty(annotation) && annotation.notNull()) { + sql.append(" not null "); + } + if (ObjectUtils.isNotEmpty(annotation) && annotation.pK()) { + // TODO 这是如果dataBaseProperty是null(即使用主程序的数据源时),数据库类型先写死。 + DBEnums type = dataBaseProperty == null ? DBEnums.MYSQL : dataBaseProperty.type(); + if (pkFlag) { + log.error("Duplicate Primary Key"); + throw new RuntimeException("Duplicate Primary Key"); + } + this.genPrimaryKey(sql, type, tableName); + pkFlag = true; + if (annotation.autoIncrement()) { + this.genAutoIncrement(sql, type); + } + } + if (index != count) { + sql.append("," + "\n"); + } else { + sql.append("\n"); + } + } + sql.append(")"); + log.info(sql.toString()); + this.executeSql(sql.toString(), dataBaseProperty, pluginId); + } + + public void destroyDataSource(DataBaseProperty dataBaseProperty, String pluginId) { + if (dataBaseProperty == null) return; + String dataSourceId = pluginId + "-" + dataBaseProperty.type().getDBType(); + if (dynamicRoutingDataSource.getDataSources().containsKey(dataSourceId)) { + dynamicRoutingDataSource.removeDataSource(dataSourceId); + } + } + + private void executeSql(String sql, DataBaseProperty dataBaseProperty, String pluginId) { + try { + Connection connection; + if (Objects.isNull(dataBaseProperty)) { + connection = dynamicRoutingDataSource.getConnection(); + } else { + String dataSourceId = pluginId + "-" + dataBaseProperty.type().getDBType(); + if (!dynamicRoutingDataSource.getDataSources().containsKey(dataSourceId)) { + DataSource dataSource; + if (dataBaseProperty.type().equals(DBEnums.SQLITE)) { + dataSource = DataSourceBuilder.create().type(SQLiteDataSource.class) + .driverClassName(dataBaseProperty.driver()) + .url(dataBaseProperty.url()).build(); + } else { + dataSource = DataSourceBuilder.create() + .type(MysqlDataSource.class) + .driverClassName(dataBaseProperty.driver()) + .username(dataBaseProperty.username()).password(dataBaseProperty.password()) + .url(dataBaseProperty.url()).build(); + } + dynamicRoutingDataSource.addDataSource(dataSourceId, dataSource); + } + connection = dynamicRoutingDataSource.getDataSource(dataSourceId).getConnection(); + } + connection.createStatement().execute(sql); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + private void genPrimaryKey(StringBuilder sql, DBEnums dbEnums, String tableName) { + switch (dbEnums) { + case MYSQL: + sql.append("primary key "); + break; + case SQLITE: + sql.append("constraint ").append(tableName).append("_pk primary key "); + break; + default: + log.error("Unsupported Database"); + throw new RuntimeException("Unsupported Database"); + } + } + + private void genAutoIncrement(StringBuilder sql, DBEnums dbEnums) { + switch (dbEnums) { + case MYSQL: + sql.append(" AUTO_INCREMENT"); + break; + case SQLITE: + sql.append(" AUTOINCREMENT"); + break; + default: + log.error("Unsupported Database"); + throw new RuntimeException("Unsupported Database"); + } + } + + private String getSQLiteDBLocation(String pluginRoot, ClassLoader classLoader, String pluginId) { + List pluginFolders = new ArrayList<>(); + try { + Path rootPath = Paths.get(pluginRoot); + if (!Files.isDirectory(rootPath)) { + System.out.println("提供的路径不是一个有效的目录:" + pluginRoot); + } + this.findPluginFoldersRecursively(rootPath, pluginFolders); + } catch (IOException e) { + throw new RuntimeException(e); + } + return "s"; + } + + private void findPluginFoldersRecursively(Path folder, List pluginFolders) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(folder)) { + for (Path entry : stream) { + if (Files.isDirectory(entry)) { + String folderName = entry.getFileName().toString(); + if (folderName.contains("datasource")) { + pluginFolders.add(entry.toString()); + } + findPluginFoldersRecursively(entry, pluginFolders); // 递归查找子目录 + } + } + } + } + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/Entity.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/Entity.java new file mode 100644 index 00000000..d2e0e959 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/Entity.java @@ -0,0 +1,10 @@ +package cd.casic.plugin.utils.dBUtils.annotation; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Entity { + +} diff --git a/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/SQL.java b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/SQL.java new file mode 100644 index 00000000..bd4c0c55 --- /dev/null +++ b/framework/spring-boot-starter-plugin/src/main/java/cd/casic/plugin/utils/dBUtils/annotation/SQL.java @@ -0,0 +1,14 @@ +package cd.casic.plugin.utils.dBUtils.annotation; + +import java.lang.annotation.*; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SQL { + boolean pK() default false; + boolean notNull() default false; + String defaultValue() default ""; + boolean autoIncrement() default false; +} + diff --git a/framework/spring-boot-starter-security/pom.xml b/framework/spring-boot-starter-security/pom.xml index 59e30cbf..c954a6f4 100644 --- a/framework/spring-boot-starter-security/pom.xml +++ b/framework/spring-boot-starter-security/pom.xml @@ -42,6 +42,23 @@ org.springframework.boot spring-boot-starter-security + + + org.springframework.security + spring-security-oauth2-jose + + + + org.springframework.security + spring-security-oauth2-client + + + + org.springframework.security + spring-security-oauth2-resource-server + + + com.google.guava diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/module/process/process/setting/impl/AuthHostServiceImpl.java b/modules/module-ci-process-biz/src/main/java/cd/casic/module/process/process/setting/impl/AuthHostServiceImpl.java index 1d8b1882..982241fd 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/module/process/process/setting/impl/AuthHostServiceImpl.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/module/process/process/setting/impl/AuthHostServiceImpl.java @@ -12,7 +12,6 @@ import cd.casic.module.process.process.setting.dataobject.AuthHostDo; import cn.hutool.core.collection.CollectionUtil; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; - import java.util.Collections; import java.util.List; import java.util.Objects;